This commit is contained in:
Andrew-71 2024-03-15 18:34:24 +03:00
commit d796f9e08a
14 changed files with 411 additions and 0 deletions

21
.gitignore vendored Normal file
View file

@ -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

7
README.md Normal file
View file

@ -0,0 +1,7 @@
# 🌺 Hibiscus.txt
Simple plaintext journaling server.
## Features:
* Lack of features. No, really.
* Does one thing and does it... decently?

37
auth.go Normal file
View file

@ -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)
})
}

95
files.go Normal file
View file

@ -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)
}

5
go.mod Normal file
View file

@ -0,0 +1,5 @@
module hibiscus
go 1.20
require github.com/go-chi/chi/v5 v5.0.12

2
go.sum Normal file
View file

@ -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=

47
log.go Normal file
View file

@ -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"))
}

1
log_curl.sh Normal file
View file

@ -0,0 +1 @@
curl -XPOST -d 'need to \n remove newlines \n' -u 'test:pass' 'localhost:7101/log'

5
main.go Normal file
View file

@ -0,0 +1,5 @@
package main
func main() {
Serve()
}

39
pages.go Normal file
View file

@ -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) {
}

17
pages/error/404.html Normal file
View file

@ -0,0 +1,17 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<!-- <link rel="icon" type="image/x-icon" href="/public/img/favicon.ico">-->
<!-- <link rel="stylesheet" href="/public/css/main.css">-->
<title>Error 404 - Hibiscus</title>
</head>
<body>
<main>
<h1>Error 404 - Not Found</h1>
<p>The page you were looking for doesn't exist or was moved</p>
<h3><a href="/">Go home?</a></h3>
</main>
</body>
</html>

29
pages/index.html Normal file
View file

@ -0,0 +1,29 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<!-- <link rel="stylesheet" href="/public/css/main.css">-->
<link rel="stylesheet" href="/public/css/pico.red.min.css">
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Hibiscus.txt</title>
</head>
<body>
<header class="container">
<h1 style="margin-bottom:0;">Hibiscus.txt</h1>
<p id="status" style="margin-top:0;">Today is 14/03/2024</p>
</header>
<main class="container">
<h2 style="margin-bottom:0;"><label for="day">Your day so far:</label></h2>
<textarea id="day" name="Day entry" cols="40" rows="10" style="max-width: 640px; width: 100%"></textarea>
<div class="grid"><button onclick="console.log('Test save')">Save</button></div>
<h2><label for="log">Log</label></h2>
<input type="text" id="log" name="log" style="max-width: 640px; width: 100%"/>
<div class="grid"><button onclick="console.log('Test log save')">Save</button></div>
</main>
<footer class="container">
<p><a href="/page/sitemap" class="no-accent"></a> v0.0.1 | PoC using <a href="https://picocss.com">PicoCSS</a></p>
</footer>
</body>
</html>

55
public/css/main.css Normal file
View file

@ -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; }*/
/*}*/

51
serve.go Normal file
View file

@ -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)
}