From d796f9e08a9c4c666af938833cfb2afeed343d32 Mon Sep 17 00:00:00 2001 From: Andrew-71 Date: Fri, 15 Mar 2024 18:34:24 +0300 Subject: [PATCH] Init --- .gitignore | 21 ++++++++++ README.md | 7 ++++ auth.go | 37 +++++++++++++++++ files.go | 95 ++++++++++++++++++++++++++++++++++++++++++++ go.mod | 5 +++ go.sum | 2 + log.go | 47 ++++++++++++++++++++++ log_curl.sh | 1 + main.go | 5 +++ pages.go | 39 ++++++++++++++++++ pages/error/404.html | 17 ++++++++ pages/index.html | 29 ++++++++++++++ public/css/main.css | 55 +++++++++++++++++++++++++ serve.go | 51 ++++++++++++++++++++++++ 14 files changed, 411 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 auth.go create mode 100644 files.go create mode 100644 go.mod create mode 100644 go.sum create mode 100644 log.go create mode 100644 log_curl.sh create mode 100644 main.go create mode 100644 pages.go create mode 100644 pages/error/404.html create mode 100644 pages/index.html create mode 100644 public/css/main.css create mode 100644 serve.go diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3972c75 --- /dev/null +++ b/.gitignore @@ -0,0 +1,21 @@ +# Binaries for programs and plugins +*.exe +*.exe~ +*.dll +*.so +*.dylib + +# Test binary, built with `go test -c` +*.test + +# Output of the go coverage tool, specifically when used with LiteIDE +*.out + +# Dependency directories (remove the comment below to include it) +# vendor/ + +# Go workspace file +go.work + +# Executable +hibiscus \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..c35b596 --- /dev/null +++ b/README.md @@ -0,0 +1,7 @@ +# 🌺 Hibiscus.txt + +Simple plaintext journaling server. + +## Features: +* Lack of features. No, really. +* Does one thing and does it... decently? \ No newline at end of file diff --git a/auth.go b/auth.go new file mode 100644 index 0000000..e966d53 --- /dev/null +++ b/auth.go @@ -0,0 +1,37 @@ +package main + +import ( + "crypto/sha256" + "crypto/subtle" + "net/http" +) + +// This middleware handles authentication & authorization for the app. +// It uses BasicAuth because I doubt there is a need for something sophisticated in a small hobby project +// Originally taken from https://www.alexedwards.net/blog/basic-authentication-in-go (13.03.2024) +// TODO: why did I have to convert Handler from HandlerFunc? +func basicAuth(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + username, password, ok := r.BasicAuth() + if ok { + // Calculate SHA-256 hashes for equal length in ConstantTimeCompare + usernameHash := sha256.Sum256([]byte(username)) + passwordHash := sha256.Sum256([]byte(password)) + expectedUsernameHash := sha256.Sum256([]byte("test")) // TODO: put user & pass outside + expectedPasswordHash := sha256.Sum256([]byte("pass")) + + usernameMatch := subtle.ConstantTimeCompare(usernameHash[:], expectedUsernameHash[:]) == 1 + passwordMatch := subtle.ConstantTimeCompare(passwordHash[:], expectedPasswordHash[:]) == 1 + + if usernameMatch && passwordMatch { + next.ServeHTTP(w, r) + return + } + // TODO: Note failed login attempt? + } + + // Unauthorized, inform client that we have auth and return 401 + w.Header().Set("WWW-Authenticate", `Basic realm="restricted", charset="UTF-8"`) + http.Error(w, "Unauthorized", http.StatusUnauthorized) + }) +} diff --git a/files.go b/files.go new file mode 100644 index 0000000..ed9ed5a --- /dev/null +++ b/files.go @@ -0,0 +1,95 @@ +package main + +import ( + "encoding/json" + "fmt" + "io" + "net/http" + "os" + "path/filepath" + "strconv" + "strings" +) + +var DEFAULT_EXTENSION = "txt" + +func GetFile(filename string, w http.ResponseWriter, r *http.Request) { + filenames, err := filepath.Glob("data/" + filename + ".*") // .txt, .md, anything + if err != nil { + http.Error(w, "error finding file", http.StatusInternalServerError) + return + } + + if len(filenames) == 0 { + http.Error(w, "no matching files found", http.StatusNotFound) + return + } else if len(filenames) > 1 { + http.Error(w, "several matching files found ("+strconv.Itoa(len(filenames))+")", http.StatusInternalServerError) // TODO: Better handling, duh + return + } + + fileContents, err := os.ReadFile(filenames[0]) + if err != nil { + http.Error(w, "error reading file", http.StatusInternalServerError) + return + } + + _, err = w.Write(fileContents) + if err != nil { + http.Error(w, "error sending file", http.StatusInternalServerError) + } +} + +// PostFile TODO: Save to trash to prevent malicious/accidental ovverrides? +func PostFile(filename string, w http.ResponseWriter, r *http.Request) { + body, err := io.ReadAll(r.Body) + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + w.Write([]byte("error reading body")) + return + } + + filenames, err := filepath.Glob("data/" + filename + ".*") // .txt, .md, anything + if err != nil { + http.Error(w, "error searching for file", http.StatusInternalServerError) + return + } + + var filenameFinal string + if len(filenames) == 0 { + // Create new file and write + filenameFinal = "data/" + filename + "." + DEFAULT_EXTENSION + } else if len(filenames) > 1 { + http.Error(w, "several matching files found ("+strconv.Itoa(len(filenames))+")", http.StatusInternalServerError) // TODO: Better handling, duh + return + } else { + filenameFinal = filenames[0] + fmt.Println(filenameFinal) + fmt.Println(filenames) + } + + f, err := os.OpenFile(filenameFinal, os.O_CREATE|os.O_WRONLY, 0644) + if err != nil { + fmt.Println("Error opening/making file") + return + } + + if _, err := f.Write(body); err != nil { + fmt.Println("Error writing to the file") + } +} + +// ListFiles returns JSON of filenames in a directory without extensions or path +func ListFiles(directory string, w http.ResponseWriter, r *http.Request) { + filenames, err := filepath.Glob("data/" + directory + "/*") // .txt, .md, anything + if err != nil { + http.Error(w, "error searching for files", http.StatusInternalServerError) + return + } + for i, file := range filenames { + file, _ := strings.CutSuffix(filepath.Base(file), filepath.Ext(file)) + filenames[i] = file + } + filenamesJson, err := json.Marshal(filenames) + w.Write(filenamesJson) +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..dd50501 --- /dev/null +++ b/go.mod @@ -0,0 +1,5 @@ +module hibiscus + +go 1.20 + +require github.com/go-chi/chi/v5 v5.0.12 diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..bfc9174 --- /dev/null +++ b/go.sum @@ -0,0 +1,2 @@ +github.com/go-chi/chi/v5 v5.0.12 h1:9euLV5sTrTNTRUU9POmDUvfxyj6LAABLUcEWO+JJb4s= +github.com/go-chi/chi/v5 v5.0.12/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= diff --git a/log.go b/log.go new file mode 100644 index 0000000..e28249d --- /dev/null +++ b/log.go @@ -0,0 +1,47 @@ +package main + +import ( + "fmt" + "io" + "net/http" + "os" + "time" +) + +func AppendLog(input string) error { + t := time.Now().Format("02-01-2006 15:04") // dd-mm-yyyy HH:MM + + f, err := os.OpenFile("./data/log.txt", os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) + if err != nil { + fmt.Println("Error opening/making file") + return err + } + + if _, err := f.Write([]byte(t + " | " + input + "\n")); err != nil { + fmt.Println("Error appending to the file") + return err + } + if err := f.Close(); err != nil { + fmt.Println("Error closing the file") + return err + } + return nil +} + +func PostLog(w http.ResponseWriter, r *http.Request) { + body, err := io.ReadAll(r.Body) + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + w.Write([]byte("error reading body")) + return + } + err = AppendLog(string(body)) + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + w.Write([]byte("error appending to log")) + return + } + + w.WriteHeader(http.StatusOK) + w.Write([]byte("appended to log")) +} diff --git a/log_curl.sh b/log_curl.sh new file mode 100644 index 0000000..626d47a --- /dev/null +++ b/log_curl.sh @@ -0,0 +1 @@ +curl -XPOST -d 'need to \n remove newlines \n' -u 'test:pass' 'localhost:7101/log' diff --git a/main.go b/main.go new file mode 100644 index 0000000..fe93d34 --- /dev/null +++ b/main.go @@ -0,0 +1,5 @@ +package main + +func main() { + Serve() +} diff --git a/pages.go b/pages.go new file mode 100644 index 0000000..2081889 --- /dev/null +++ b/pages.go @@ -0,0 +1,39 @@ +package main + +import ( + "github.com/go-chi/chi/v5" + "net/http" + "time" +) + +// NotFound returns a user-friendly 404 error page +func NotFound(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(404) + http.ServeFile(w, r, "./pages/error/404.html") +} + +// GetDay gets... a day... page +func GetDay(w http.ResponseWriter, r *http.Request) { + // TODO: This will be *very* different, `today` func will be needed + dayString := chi.URLParam(r, "day") + if dayString == "" { + dayString = time.Now().Format("02-01-2006") // By default, use today + } + GetFile("day/"+dayString, w, r) +} + +// GetNote gets... a day... page +func GetNote(w http.ResponseWriter, r *http.Request) { + // TODO: This will be *very* different, `today` func will be needed + noteString := chi.URLParam(r, "note") + if noteString == "" { + w.WriteHeader(http.StatusNotFound) // TODO: maybe different status fits better? + w.Write([]byte("note name not given")) + return + } + GetFile("notes/"+noteString, w, r) +} + +func PostDayPage(w http.ResponseWriter, r *http.Request) { + +} diff --git a/pages/error/404.html b/pages/error/404.html new file mode 100644 index 0000000..1c876dd --- /dev/null +++ b/pages/error/404.html @@ -0,0 +1,17 @@ + + + + + + + + Error 404 - Hibiscus + + +
+

Error 404 - Not Found

+

The page you were looking for doesn't exist or was moved

+

Go home?

+
+ + \ No newline at end of file diff --git a/pages/index.html b/pages/index.html new file mode 100644 index 0000000..f56e789 --- /dev/null +++ b/pages/index.html @@ -0,0 +1,29 @@ + + + + + + + + + Hibiscus.txt + + +
+

Hibiscus.txt

+

Today is 14/03/2024

+
+
+

+ +
+ +

+ +
+
+ + + \ No newline at end of file diff --git a/public/css/main.css b/public/css/main.css new file mode 100644 index 0000000..c632233 --- /dev/null +++ b/public/css/main.css @@ -0,0 +1,55 @@ +body { + color: #454545; + background-color: #f5f0e1; /*f6f4ee*/ + font-size: 16px; + margin: 2em auto; + max-width: 950px; + padding: 1em; + line-height: 1.4; + text-align: justify; + font-family: serif; + min-height: 85vh; + display: flex; + flex-direction: column; +} + +/* Links can be blue or look like plaintext */ +a, a:visited { color: #f85552; } +a:hover, a:visited:hover { color: #e66868; } +a.no-accent, a.no-accent:visited { color: #454545; } +a.no-accent:hover, a.no-accent:visited:hover { color: #656565; } + +textarea, input { + background: #f5f2ee; +} + +footer { margin-top: auto; } + +button { + background-color: #f85552; + border: none; + color: #f5f2ee; + padding: 10px; + text-align: center; + text-decoration: none; + display: inline-block; + font-size: 16px; + margin: 4px 2px; + border-radius: 12px; + max-width: 100px; + cursor: pointer; +} +button:hover { background-color: #e66868; } + + +/* Dark theme TODO*/ +/*@media (prefers-color-scheme: dark) {*/ +/* body {*/ +/* color: #f5f0e1;*/ +/* background-color: #2c2825;*/ +/* }*/ +/* a.no-accent, a.no-accent:visited { color: #f5f0e1; }*/ +/* a.no-accent:hover, a.no-accent:visited:hover { color: #a9a8a4; }*/ +/* a, a:visited { color: #24a5ea; }*/ +/* a:hover, a:visited:hover { color: #1b74cc; }*/ +/*}*/ \ No newline at end of file diff --git a/serve.go b/serve.go new file mode 100644 index 0000000..db650d2 --- /dev/null +++ b/serve.go @@ -0,0 +1,51 @@ +package main + +import ( + "fmt" + "github.com/go-chi/chi/v5" + "github.com/go-chi/chi/v5/middleware" + "net/http" + "strconv" +) + +const PORT = 7101 // TODO: Obviously don't just declare port here + +func Serve() { + r := chi.NewRouter() + r.Use(middleware.Logger, middleware.CleanPath, middleware.StripSlashes) + r.Use(basicAuth) // TODO: ..duh! + + // Home page + r.Get("/", func(w http.ResponseWriter, r *http.Request) { + http.ServeFile(w, r, "./pages/index.html") + }) + + // API ============= + apiRouter := chi.NewRouter() + + apiRouter.Get("/readme", func(w http.ResponseWriter, r *http.Request) { GetFile("readme", w, r) }) + apiRouter.Get("/log", func(w http.ResponseWriter, r *http.Request) { GetFile("log", w, r) }) + apiRouter.Get("/agenda", func(w http.ResponseWriter, r *http.Request) { GetFile("agenda", w, r) }) + + apiRouter.Post("/readme", func(w http.ResponseWriter, r *http.Request) { PostFile("readme", w, r) }) + apiRouter.Post("/agenda", func(w http.ResponseWriter, r *http.Request) { PostFile("agenda", w, r) }) + + apiRouter.Get("/day", func(w http.ResponseWriter, r *http.Request) { ListFiles("day", w, r) }) + apiRouter.Get("/day/{day}", func(w http.ResponseWriter, r *http.Request) { GetDay(w, r) }) + + apiRouter.Get("/notes", func(w http.ResponseWriter, r *http.Request) { ListFiles("notes", w, r) }) + apiRouter.Get("/notes/{note}", func(w http.ResponseWriter, r *http.Request) { GetNote(w, r) }) + + apiRouter.Post("/log", func(w http.ResponseWriter, r *http.Request) { PostLog(w, r) }) + + r.Mount("/api", apiRouter) + + r.NotFound(NotFound) + + // Static files + fs := http.FileServer(http.Dir("public")) + r.Handle("/public/*", http.StripPrefix("/public/", fs)) + + fmt.Println("Website working on port: ", PORT) + _ = http.ListenAndServe(":"+strconv.Itoa(PORT), r) +}