Compare commits
4
Commits
814949bfb0
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
93ca9ea33b | ||
|
|
4d53a6b471 | ||
|
|
d5c4cfa9d0 | ||
|
|
9f6c5b2880 |
+1
-1
@@ -1,5 +1,5 @@
|
||||
data/
|
||||
config.json
|
||||
config.toml
|
||||
*~
|
||||
*.kate-swp
|
||||
.*.kate-swp
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
)
|
||||
|
||||
func HashPassword(password string) (string, error) {
|
||||
hash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return string(hash), nil
|
||||
}
|
||||
|
||||
func CheckPassword(hash, password string) bool {
|
||||
return bcrypt.CompareHashAndPassword([]byte(hash), []byte(password)) == nil
|
||||
}
|
||||
|
||||
func NeedsSetup(adminUser string) bool {
|
||||
return adminUser == ""
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"encoding/hex"
|
||||
"net/http"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
var (
|
||||
sessions = map[string]time.Time{}
|
||||
mu sync.Mutex
|
||||
)
|
||||
|
||||
func GenerateSession() (string, error) {
|
||||
bytes := make([]byte, 32)
|
||||
_, err := rand.Read(bytes)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
token := hex.EncodeToString(bytes)
|
||||
|
||||
mu.Lock()
|
||||
sessions[token] = time.Now()
|
||||
mu.Unlock()
|
||||
|
||||
return token, nil
|
||||
}
|
||||
|
||||
func IsLoggedIn(r *http.Request) bool {
|
||||
cookie, err := r.Cookie("session")
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
mu.Lock()
|
||||
_, exists := sessions[cookie.Value]
|
||||
mu.Unlock()
|
||||
|
||||
return exists
|
||||
}
|
||||
|
||||
func SetSessionCookie(w http.ResponseWriter, token string) {
|
||||
http.SetCookie(w, &http.Cookie{
|
||||
Name: "session",
|
||||
Value: token,
|
||||
Path: "/",
|
||||
HttpOnly: true,
|
||||
SameSite: http.SameSiteLaxMode,
|
||||
})
|
||||
}
|
||||
|
||||
func ClearSession(w http.ResponseWriter, r *http.Request) {
|
||||
cookie, err := r.Cookie("session")
|
||||
if err == nil {
|
||||
mu.Lock()
|
||||
delete(sessions, cookie.Value)
|
||||
mu.Unlock()
|
||||
}
|
||||
|
||||
http.SetCookie(w, &http.Cookie{
|
||||
Name: "session",
|
||||
Value: "",
|
||||
Path: "/",
|
||||
MaxAge: -1,
|
||||
HttpOnly: true,
|
||||
})
|
||||
}
|
||||
+32
-11
@@ -1,36 +1,57 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"log"
|
||||
"os"
|
||||
|
||||
"github.com/BurntSushi/toml"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
SiteName string `json:"site_name"`
|
||||
Port string `json:"port"`
|
||||
DataDir string `json:"data_dir"`
|
||||
SiteName string `toml:"site_name"`
|
||||
Port string `toml:"port"`
|
||||
DataDir string `toml:"data_dir"`
|
||||
AdminUser string `toml:"admin_user"`
|
||||
AdminPasswordHash string `toml:"admin_password_hash"`
|
||||
}
|
||||
|
||||
func Load(path string) Config {
|
||||
configData := Config{
|
||||
SiteName: "Unnamed Download Listing",
|
||||
Port: "8080",
|
||||
Port: ":8080",
|
||||
DataDir: "data",
|
||||
}
|
||||
|
||||
file, err := os.Open(path)
|
||||
// Overwrite the default configuration (configData) with data from the file at 'path'
|
||||
_, err := toml.DecodeFile(path, &configData)
|
||||
if err != nil {
|
||||
log.Println("No config file found, using defaults. Consider setting up a Site Name at the very least!")
|
||||
}
|
||||
|
||||
return configData
|
||||
}
|
||||
|
||||
func Save(path string, configData Config) error {
|
||||
file, err := os.Create(path)
|
||||
if err != nil {
|
||||
log.Println("An error occured while trying to save configuration data.")
|
||||
return err
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
// Overwrite the default configuration with data from 'file'
|
||||
err = json.NewDecoder(file).Decode(&configData)
|
||||
if err != nil {
|
||||
log.Fatal("Config file exists but has errors:", err)
|
||||
file.WriteString("site_name = \"" + configData.SiteName + "\"\n")
|
||||
file.WriteString("port = \"" + configData.Port + "\"\n")
|
||||
file.WriteString("data_dir = \"" + configData.DataDir + "\"\n")
|
||||
|
||||
if configData.AdminUser != "" {
|
||||
file.WriteString("admin_user = \"" + configData.AdminUser + "\"\n")
|
||||
file.WriteString("admin_password_hash = \"" + configData.AdminPasswordHash + "\"\n")
|
||||
file.WriteString("# This admin_password_hash above is a hashed password, not your actual\n")
|
||||
file.WriteString("# written password. You can't change your admin password here.\n")
|
||||
file.WriteString("# If you've lost your password, delete admin_user and admin_hash\n")
|
||||
file.WriteString("# from this file and restart the app to create a new account.\n")
|
||||
file.WriteString("# Your uploaded content will stay intact.\n")
|
||||
}
|
||||
|
||||
return configData
|
||||
return nil
|
||||
}
|
||||
|
||||
Binary file not shown.
Executable
BIN
Binary file not shown.
@@ -2,4 +2,9 @@ module 192.168.1.180/odonax/filehost-athome
|
||||
|
||||
go 1.26.2
|
||||
|
||||
require github.com/mattn/go-sqlite3 v1.14.44
|
||||
require (
|
||||
github.com/mattn/go-sqlite3 v1.14.44
|
||||
golang.org/x/crypto v0.51.0
|
||||
)
|
||||
|
||||
require github.com/BurntSushi/toml v1.6.0
|
||||
|
||||
@@ -1,2 +1,6 @@
|
||||
github.com/BurntSushi/toml v1.6.0 h1:dRaEfpa2VI55EwlIW72hMRHdWouJeRF7TPYhI+AUQjk=
|
||||
github.com/BurntSushi/toml v1.6.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
|
||||
github.com/mattn/go-sqlite3 v1.14.44 h1:3VSe+xafpbzsLbdr2AWlAZk9yRHiBhTBakioXaCKTF8=
|
||||
github.com/mattn/go-sqlite3 v1.14.44/go.mod h1:pjEuOr8IwzLJP2MfGeTb0A35jauH+C2kbHKBr7yXKVQ=
|
||||
golang.org/x/crypto v0.51.0 h1:IBPXwPfKxY7cWQZ38ZCIRPI50YLeevDLlLnyC5wRGTI=
|
||||
golang.org/x/crypto v0.51.0/go.mod h1:8AdwkbraGNABw2kOX6YFPs3WM22XqI4EXEd8g+x7Oc8=
|
||||
|
||||
@@ -0,0 +1,95 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"html/template"
|
||||
"net/http"
|
||||
|
||||
"192.168.1.180/odonax/filehost-athome/auth"
|
||||
"192.168.1.180/odonax/filehost-athome/config"
|
||||
)
|
||||
|
||||
var Cfg *config.Config
|
||||
var CfgPath string
|
||||
|
||||
func Setup(tmpl *template.Template) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
if !auth.NeedsSetup(Cfg.AdminUser) {
|
||||
http.Redirect(w, r, "/", http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
|
||||
if r.Method == "GET" {
|
||||
tmpl.Execute(w, nil)
|
||||
return
|
||||
}
|
||||
|
||||
username := r.FormValue("username")
|
||||
password := r.FormValue("password")
|
||||
confirm := r.FormValue("confirm")
|
||||
|
||||
if username == "" || password == "" {
|
||||
tmpl.Execute(w, map[string]string{"Error": "Please fill in all fields."})
|
||||
return
|
||||
}
|
||||
|
||||
if password != confirm {
|
||||
tmpl.Execute(w, map[string]string{"Error": "Passwords do not match."})
|
||||
return
|
||||
}
|
||||
|
||||
hash, err := auth.HashPassword(password)
|
||||
if err != nil {
|
||||
tmpl.Execute(w, map[string]string{"Error": "Something went wrong. Refresh the page and try again. If the issue persists, contact the developer of this application and tell them what happened."})
|
||||
return
|
||||
}
|
||||
|
||||
Cfg.AdminUser = username
|
||||
Cfg.AdminPasswordHash = hash
|
||||
config.Save(CfgPath, *Cfg)
|
||||
|
||||
http.Redirect(w, r, "/admin", http.StatusSeeOther)
|
||||
}
|
||||
}
|
||||
|
||||
func Login(tmpl *template.Template) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
if auth.NeedsSetup(Cfg.AdminUser) {
|
||||
http.Redirect(w, r, "/setup", http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
|
||||
if r.Method == "GET" {
|
||||
tmpl.Execute(w, map[string]string{"SiteName": Cfg.SiteName})
|
||||
return
|
||||
}
|
||||
|
||||
username := r.FormValue("username")
|
||||
password := r.FormValue("password")
|
||||
|
||||
if username != Cfg.AdminUser || !auth.CheckPassword(Cfg.AdminPasswordHash, password) {
|
||||
tmpl.Execute(w, map[string]string{
|
||||
"SiteName": Cfg.SiteName,
|
||||
"Error": "Invalid credentials.",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
token, err := auth.GenerateSession()
|
||||
if err != nil {
|
||||
tmpl.Execute(w, map[string]string{
|
||||
"SiteName": Cfg.SiteName,
|
||||
"Error": "Something went wrong. Please try again.",
|
||||
})
|
||||
return
|
||||
}
|
||||
auth.SetSessionCookie(w, token)
|
||||
http.Redirect(w, r, "/admin", http.StatusSeeOther)
|
||||
}
|
||||
}
|
||||
|
||||
func Logout() http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
auth.ClearSession(w, r)
|
||||
http.Redirect(w, r, "/", http.StatusSeeOther)
|
||||
}
|
||||
}
|
||||
@@ -8,29 +8,40 @@ import (
|
||||
|
||||
"192.168.1.180/odonax/filehost-athome/config"
|
||||
"192.168.1.180/odonax/filehost-athome/database"
|
||||
"192.168.1.180/odonax/filehost-athome/handlers"
|
||||
)
|
||||
|
||||
func main() {
|
||||
cfg := config.Load("config.json")
|
||||
cfgPath := "config.toml"
|
||||
cfg := config.Load(cfgPath)
|
||||
|
||||
handlers.Cfg = &cfg
|
||||
handlers.CfgPath = cfgPath
|
||||
|
||||
os.MkdirAll(cfg.DataDir+"/files", 0755)
|
||||
|
||||
db := database.Open(cfg.DataDir+"/hub.db")
|
||||
defer db.Close()
|
||||
|
||||
tmpl, err := template.ParseFiles("templates/home.html")
|
||||
if err != nil {
|
||||
log.Fatal("Failed to load template:", err)
|
||||
}
|
||||
tmpl := template.Must(template.ParseGlob("templates/*.html"))
|
||||
|
||||
// tmpl, err := template.ParseFiles("templates/home.html")
|
||||
// if err != nil {
|
||||
// log.Fatal("Failed to load template:", err)
|
||||
// }
|
||||
|
||||
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
||||
data := map[string]string{
|
||||
"Name": cfg.SiteName,
|
||||
"Description": "This is where you'll own your files and share them freely.",
|
||||
}
|
||||
tmpl.Execute(w, data)
|
||||
tmpl.ExecuteTemplate(w, "home.html", data)
|
||||
})
|
||||
|
||||
log.Println("Starting filehost on :8080")
|
||||
log.Fatal(http.ListenAndServe(":8080", nil))
|
||||
http.HandleFunc("/setup", handlers.Setup(tmpl.Lookup("setup.html")))
|
||||
http.HandleFunc("/login", handlers.Login(tmpl.Lookup("login.html")))
|
||||
http.HandleFunc("/logout", handlers.Logout())
|
||||
|
||||
log.Printf("Starting filehost on %s\n", cfg.Port)
|
||||
log.Fatal(http.ListenAndServe(cfg.Port, nil))
|
||||
}
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Log in - {{ .SiteName }}</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Log in</h1>
|
||||
{{ if .Error }}
|
||||
<p><strong>{{ .Error }}</strong></p>
|
||||
{{ end }}
|
||||
<form method="POST" action="/login">
|
||||
<label for="username">Username</label>
|
||||
<input type="text" id="username" name="username" required>
|
||||
|
||||
<label for="password">Password</label>
|
||||
<input type="password" id="password" name="password" required>
|
||||
|
||||
<button type="submit">Log in</button>
|
||||
</form>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,28 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Set up your filehost</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Welcome! It's time to get you set up.</h1>
|
||||
<p>Create your admin account to start managing your site. Consider picking a username different than 'admin' or your standard internet alias to make it harder for people to guess your admin credentials.</p>
|
||||
<p>If you ever lose your credentials or your admin account is compromised, you can reset it by editing the 'config.toml' file and removing the credentials there. You can then recreate your admin account without losing your listings.</p>
|
||||
{{ if .Error }}
|
||||
<p><strong>{{ .Error }}</strong></p>
|
||||
{{ end }}
|
||||
<form method="POST" action="/setup">
|
||||
<label for="username">Username</label>
|
||||
<input type="text" id="username" name="username" required>
|
||||
|
||||
<label for="password">Password</label>
|
||||
<input type="password" id="password" name="password" required>
|
||||
|
||||
<label for="confirm">Confirm password</label>
|
||||
<input type="password" id="confirm" name="confirm" required>
|
||||
|
||||
<button type="submit">Create account</button>
|
||||
</form>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user