diff options
| author | Jan Wolff <janw@mailbox.org> | 2025-09-12 09:34:01 +0200 |
|---|---|---|
| committer | Jan Wolff <janw@mailbox.org> | 2025-09-12 09:34:01 +0200 |
| commit | 283c31564a9d5dab4b8b71d7498886b0cd20a999 (patch) | |
| tree | bc15867a389f25b1855042b5af20efba96c68dbb | |
initial commit
| -rw-r--r-- | config/config.go | 46 | ||||
| -rw-r--r-- | config_example.json | 10 | ||||
| -rw-r--r-- | controller/app.go | 39 | ||||
| -rw-r--r-- | controller/controller.go | 8 | ||||
| -rw-r--r-- | controller/file.go | 79 | ||||
| -rw-r--r-- | controller/index.go | 13 | ||||
| -rw-r--r-- | controller/template/404.html | 7 | ||||
| -rw-r--r-- | controller/template/base.html | 49 | ||||
| -rw-r--r-- | controller/template/index.html | 28 | ||||
| -rw-r--r-- | controller/template/uploaded.html | 10 | ||||
| -rw-r--r-- | go.mod | 3 | ||||
| -rw-r--r-- | main.go | 66 | ||||
| -rw-r--r-- | storage/file.go | 21 | ||||
| -rw-r--r-- | storage/storage.go | 57 |
14 files changed, 436 insertions, 0 deletions
diff --git a/config/config.go b/config/config.go new file mode 100644 index 0000000..691e756 --- /dev/null +++ b/config/config.go @@ -0,0 +1,46 @@ +package config + +import ( + "encoding/json" + "io" + "os" +) + +type Configuration struct { + Http struct { + ListenAddress string `json:"listen"` + BaseAddress string `json:"base"` + } `json:"http"` + Authentication struct { + Username string `json:"username"` + Password string `json:"password"` + } `json:"auth"` +} + +func Must(config Configuration, err error) Configuration { + if err != nil { + panic(err) + } + + return config +} + +func Open(filename string) (Configuration, error) { + file, err := os.Open(filename) + if err != nil { + return Configuration{}, err + } + + buf, err := io.ReadAll(file) + if err != nil { + return Configuration{}, err + } + + config := Configuration{} + + if err := json.Unmarshal(buf, &config); err != nil { + return Configuration{}, err + } else { + return config, nil + } +} diff --git a/config_example.json b/config_example.json new file mode 100644 index 0000000..99eff96 --- /dev/null +++ b/config_example.json @@ -0,0 +1,10 @@ +{ + "http": { + "listen": "127.0.0.1:8080", + "base": "http://127.0.0.1:8080" + }, + "auth": { + "username": "jwolff", + "password": "changeme" + } +} diff --git a/controller/app.go b/controller/app.go new file mode 100644 index 0000000..f145ce2 --- /dev/null +++ b/controller/app.go @@ -0,0 +1,39 @@ +package controller + +import ( + "embed" + "html/template" + "net/http" + + "drop.janw.name/config" + "drop.janw.name/storage" +) + +//go:embed template/* +var templateFs embed.FS + +type App struct { + storage *storage.Storage + tmpl *template.Template + config config.Configuration +} + +func NewApp(configFilename string) *App { + return &App{ + storage: storage.NewStorage(), + tmpl: template.Must(template.ParseFS(templateFs, "template/*.html")), + config: config.Must(config.Open(configFilename)), + } +} + +func (app App) requireAuth(res http.ResponseWriter, req *http.Request) bool { + username, password, ok := req.BasicAuth() + if ok && username == app.config.Authentication.Username && password == app.config.Authentication.Password { + return true + } + + res.Header().Add("WWW-Authenticate", "Basic realm=\"Authentication Required\", charset=\"UTF-8\"") + res.WriteHeader(http.StatusUnauthorized) + + return false +} diff --git a/controller/controller.go b/controller/controller.go new file mode 100644 index 0000000..1f28562 --- /dev/null +++ b/controller/controller.go @@ -0,0 +1,8 @@ +package controller + +import "net/http" + +type Controller struct { + GET http.HandlerFunc + POST http.HandlerFunc +} diff --git a/controller/file.go b/controller/file.go new file mode 100644 index 0000000..53121d5 --- /dev/null +++ b/controller/file.go @@ -0,0 +1,79 @@ +package controller + +import ( + "fmt" + "io" + "net/http" + "time" + + "drop.janw.name/storage" +) + +const MAX_FILESIZE = (1 << 19) * 100 + +func (app App) FilePost(res http.ResponseWriter, req *http.Request) { + if !app.requireAuth(res, req) { + return + } + + if err := req.ParseMultipartForm(MAX_FILESIZE); err != nil { + panic(err) + } + + formFile, formFileHeader, err := req.FormFile("file") + if err != nil { + panic(err) + } + + protected := req.FormValue("protect") == "on" + + fileData, err := io.ReadAll(formFile) + if err != nil { + panic(err) + } + + file := storage.File{ + Protected: protected, + Filename: formFileHeader.Filename, + Data: fileData, + AvailableUntil: time.Now().Add(time.Hour * 2), + } + + key, err := app.storage.Put(file) + if err != nil { + panic(err) + } + + app.tmpl.ExecuteTemplate(res, "uploaded", struct { + DownloadURL string + }{ + DownloadURL: fmt.Sprintf("%s/%s", app.config.Http.BaseAddress, key), + }) +} + +func (app App) FileGet(res http.ResponseWriter, req *http.Request) { + fileId := req.PathValue("file") + + file, _ := app.storage.Get(fileId) + + if file == nil || !file.IsAvailable() { + if !app.requireAuth(res, req) { + return + } + + res.WriteHeader(http.StatusNotFound) + app.tmpl.ExecuteTemplate(res, "404", nil) + return + } + + if file.Protected && !app.requireAuth(res, req) { + return + } + + res.Header().Add("Content-Type", "application/octet-stream") + res.Header().Add("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s\"", file.Filename)) + + if _, err := res.Write(file.Data); err != nil { + panic(err) + } +} diff --git a/controller/index.go b/controller/index.go new file mode 100644 index 0000000..6f927ed --- /dev/null +++ b/controller/index.go @@ -0,0 +1,13 @@ +package controller + +import ( + "net/http" +) + +func (app App) Index(res http.ResponseWriter, req *http.Request) { + if !app.requireAuth(res, req) { + return + } + + app.tmpl.ExecuteTemplate(res, "index", nil) +} diff --git a/controller/template/404.html b/controller/template/404.html new file mode 100644 index 0000000..fa19547 --- /dev/null +++ b/controller/template/404.html @@ -0,0 +1,7 @@ +{{define "404"}} +{{template "header" .}} +<main> + <h1>no such file</h1> +</main> +{{template "footer" .}} +{{end}} diff --git a/controller/template/base.html b/controller/template/base.html new file mode 100644 index 0000000..c91245d --- /dev/null +++ b/controller/template/base.html @@ -0,0 +1,49 @@ +{{define "header"}} +<!DOCTYPE html> +<html lang="en"> + <head> + <title>drop.janw.name</title> + <meta charset="UTF-8"> + <meta name="author" content="Jan Wolff"> + <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=yes"> + <link href="data:," rel="icon"> + <style> + html, body { + padding: 0; + margin: 0; + width: 100%; + height: 100%; + } + + body { + display: flex; + justify-content: center; + align-items: center; + background: #ebebeb; + font-family: sans-serif; + } + + main { + padding: 2rem; + border-radius: 2rem; + background: #fefefe; + } + + form { + display: flex; + flex-direction: column; + gap: 1rem; + } + + progress { + width: 100%; + } + </style> + </head> + <body> +{{end}} + +{{define "footer"}} + </body> +</html> +{{end}} diff --git a/controller/template/index.html b/controller/template/index.html new file mode 100644 index 0000000..f50be97 --- /dev/null +++ b/controller/template/index.html @@ -0,0 +1,28 @@ +{{define "index"}} +{{template "header" .}} +<main> + <h1>drop.janw.name</h1> + <form method="POST" action="/" enctype="multipart/form-data"> + <div> + <label for="file"> + File: + </label> + <input id="file" type="file" name="file"> + </div> + <div> + <label for="protect"> + Password Protect: + </label> + <input id="protect" type="checkbox" name="protect"> + </div> + <div> + <label for="until"> + Available Until: + </label> + <input id="until" type="datetime-local" name="until"> + </div> + <input type="submit" id="upload" value="Upload"> + </form> +</main> +{{template "footer" .}} +{{end}} diff --git a/controller/template/uploaded.html b/controller/template/uploaded.html new file mode 100644 index 0000000..1324082 --- /dev/null +++ b/controller/template/uploaded.html @@ -0,0 +1,10 @@ +{{define "uploaded"}} +{{template "header" .}} +<main> + <h1>File uploaded successfully</h1> + <p> + <a href="{{.DownloadURL}}">{{.DownloadURL}}</a> + </p> +</main> +{{template "footer" .}} +{{end}} @@ -0,0 +1,3 @@ +module drop.janw.name + +go 1.24.7 @@ -0,0 +1,66 @@ +package main + +import ( + "flag" + "fmt" + "log" + "net/http" + "runtime/debug" + + "drop.janw.name/controller" +) + +var CONFIG_FILENAME string + +func registerHandler(pattern string, controller controller.Controller) { + http.HandleFunc(pattern, func(res http.ResponseWriter, req *http.Request) { + defer func() { + err := recover() + if err != nil { + res.WriteHeader(http.StatusInternalServerError) + fmt.Fprintln(res, "internal server error") + fmt.Fprintln(res, err) + fmt.Fprintln(res, string(debug.Stack())) + } + }() + + var methodFunc http.HandlerFunc + + switch req.Method { + case http.MethodGet: + methodFunc = controller.GET + case http.MethodPost: + methodFunc = controller.POST + } + + log.Println(req.Method, req.RemoteAddr, req.RequestURI) + + if methodFunc != nil { + methodFunc(res, req) + } else { + res.WriteHeader(http.StatusMethodNotAllowed) + fmt.Fprintln(res, "method not allowed") + } + }) +} + +func init() { + flag.StringVar(&CONFIG_FILENAME, "c", "config_example.json", "path to the configuration file") + flag.Parse() +} + +func main() { + app := controller.NewApp(CONFIG_FILENAME) + + registerHandler("/", controller.Controller{ + GET: app.Index, + POST: app.FilePost, + }) + + registerHandler("/{file}", controller.Controller{ + GET: app.FileGet, + }) + + log.Println(fmt.Sprintf("serving on http://%s\n", "127.0.0.1:8080")) + log.Panicln(http.ListenAndServe("127.0.0.1:8080", nil)) +} diff --git a/storage/file.go b/storage/file.go new file mode 100644 index 0000000..6fa5070 --- /dev/null +++ b/storage/file.go @@ -0,0 +1,21 @@ +package storage + +import ( + "bytes" + "time" +) + +type File struct { + Protected bool + Filename string + Data []byte + AvailableUntil time.Time +} + +func (f File) IsAvailable() bool { + return time.Now().Before(f.AvailableUntil) +} + +func (f File) Reader() *bytes.Reader { + return bytes.NewReader(f.Data) +} diff --git a/storage/storage.go b/storage/storage.go new file mode 100644 index 0000000..6dc4a37 --- /dev/null +++ b/storage/storage.go @@ -0,0 +1,57 @@ +package storage + +import ( + "crypto/rand" + "errors" + "math/big" + "strings" +) + +var ErrNoSuchFile = errors.New("no such file") + +type Storage struct { + files map[string]File +} + +func NewStorage() *Storage { + return &Storage{ + files: make(map[string]File), + } +} + +func generateKey(length uint) (string, error) { + key := make([]string, 0, 0) + alphabet := "ABCDEFGHIJKLMNOPQRSTUVWXYZ123456789abcdefghkmnpqrstuvwxyz" + + for i := uint(0); i < length; i++ { + n, err := rand.Int(rand.Reader, big.NewInt(int64(len(alphabet)))) + + if err != nil { + return "", err + } + + key = append(key, string(alphabet[n.Int64()])) + } + + return strings.Join(key, ""), nil +} + +func (s *Storage) Get(key string) (*File, error) { + if file, ok := s.files[key]; ok { + return &file, nil + } else { + return nil, ErrNoSuchFile + } +} + +func (s *Storage) Put(file File) (string, error) { + key, err := generateKey(8) + + if err != nil { + return "", err + } + + s.files[key] = file + + return key, nil +} |
