Init
This commit is contained in:
commit
d796f9e08a
14 changed files with 411 additions and 0 deletions
21
.gitignore
vendored
Normal file
21
.gitignore
vendored
Normal 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
7
README.md
Normal 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
37
auth.go
Normal 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
95
files.go
Normal 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
5
go.mod
Normal 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
2
go.sum
Normal 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
47
log.go
Normal 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
1
log_curl.sh
Normal file
|
@ -0,0 +1 @@
|
||||||
|
curl -XPOST -d 'need to \n remove newlines \n' -u 'test:pass' 'localhost:7101/log'
|
5
main.go
Normal file
5
main.go
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
Serve()
|
||||||
|
}
|
39
pages.go
Normal file
39
pages.go
Normal 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
17
pages/error/404.html
Normal 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
29
pages/index.html
Normal 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
55
public/css/main.css
Normal 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
51
serve.go
Normal 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)
|
||||||
|
}
|
Loading…
Reference in a new issue