commit d796f9e08a9c4c666af938833cfb2afeed343d32 Author: Andrew-71 Date: Fri Mar 15 18:34:24 2024 +0300 Init 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) +}