Notes/main.go
2023-12-28 09:09:56 +01:00

738 lines
16 KiB
Go

package main
import (
// Standard
"crypto/md5"
"embed"
"encoding/hex"
"flag"
"fmt"
"html/template"
"io"
"log/slog"
"net/http"
"os"
"path"
"path/filepath"
"regexp"
"strconv"
"strings"
)
const LISTEN_HOST = "0.0.0.0"
var (
flagPort int
flagVersion bool
flagCreateUser bool
flagConfig string
connectionManager ConnectionManager
static http.Handler
config Config
logger *slog.Logger
VERSION string
//go:embed version sql/*
embedded embed.FS
)
func init() { // {{{
version, _ := embedded.ReadFile("version")
VERSION = strings.TrimSpace(string(version))
opt := slog.HandlerOptions{}
logger = slog.New(slog.NewJSONHandler(os.Stdout, &opt))
configFilename := os.Getenv("HOME") + "/.config/notes.yaml"
flag.IntVar(&flagPort, "port", 1371, "TCP port to listen on")
flag.BoolVar(&flagVersion, "version", false, "Shows Notes version and exists")
flag.BoolVar(&flagCreateUser, "createuser", false, "Create a user and exit")
flag.StringVar(&flagConfig, "config", configFilename, "Filename of configuration file")
flag.Parse()
} // }}}
func main() { // {{{
var err error
if flagVersion {
fmt.Printf("%s\n", VERSION)
os.Exit(0)
}
logger.Info("application", "version", VERSION)
config, err = ConfigRead(flagConfig)
if err != nil {
logger.Error("config", "error", err)
os.Exit(1)
}
if err = dbInit(); err != nil {
logger.Error("db", "error", err)
os.Exit(1)
}
if flagCreateUser {
err = createUser()
if err != nil {
logger.Error("db", "error", err)
os.Exit(1)
}
os.Exit(0)
}
connectionManager = NewConnectionManager()
go connectionManager.BroadcastLoop()
static = http.FileServer(http.Dir(config.Application.Directories.Static))
http.HandleFunc("/css_updated", cssUpdateHandler)
http.HandleFunc("/session/create", sessionCreate)
http.HandleFunc("/session/retrieve", sessionRetrieve)
http.HandleFunc("/session/authenticate", sessionAuthenticate)
http.HandleFunc("/user/password", userPassword)
http.HandleFunc("/node/tree", nodeTree)
http.HandleFunc("/node/retrieve", nodeRetrieve)
http.HandleFunc("/node/create", nodeCreate)
http.HandleFunc("/node/update", nodeUpdate)
http.HandleFunc("/node/rename", nodeRename)
http.HandleFunc("/node/delete", nodeDelete)
http.HandleFunc("/node/upload", nodeUpload)
http.HandleFunc("/node/download", nodeDownload)
http.HandleFunc("/node/search", nodeSearch)
http.HandleFunc("/key/retrieve", keyRetrieve)
http.HandleFunc("/key/create", keyCreate)
http.HandleFunc("/key/counter", keyCounter)
http.HandleFunc("/ws", websocketHandler)
http.HandleFunc("/", staticHandler)
listen := fmt.Sprintf("%s:%d", LISTEN_HOST, flagPort)
logger.Info("webserver", "listen", listen, "domains", config.Websocket.Domains)
http.ListenAndServe(listen, nil)
} // }}}
func cssUpdateHandler(w http.ResponseWriter, r *http.Request) { // {{{
logger.Debug("webserver", "css", "updated")
connectionManager.Broadcast(struct {
Ok bool
ID string
Op string
}{Ok: true, Op: "css_reload"})
} // }}}
func websocketHandler(w http.ResponseWriter, r *http.Request) { // {{{
var err error
_, err = connectionManager.NewConnection(w, r)
if err != nil {
logger.Error("websocket", "error", err)
return
}
} // }}}
func staticHandler(w http.ResponseWriter, r *http.Request) { // {{{
data := struct {
VERSION string
}{
VERSION: VERSION,
}
// URLs with pattern /(css|images)/v1.0.0/foobar are stripped of the version.
// To get rid of problems with cached content in browser on a new version release,
// while also not disabling cache altogether.
logger.Debug("webserver", "request", r.URL.Path)
if r.URL.Path == "/favicon.ico" {
static.ServeHTTP(w, r)
return
}
rxp := regexp.MustCompile("^/(css|images|js|fonts)/v[0-9]+/(.*)$")
if comp := rxp.FindStringSubmatch(r.URL.Path); comp != nil {
r.URL.Path = fmt.Sprintf("/%s/%s", comp[1], comp[2])
static.ServeHTTP(w, r)
return
}
// Everything else is run through the template system.
// For now to get VERSION into files to fix caching.
logger.Debug("webserver", "template", r.URL.Path)
tmpl, err := newTemplate(r.URL.Path)
if err != nil {
if os.IsNotExist(err) {
w.WriteHeader(404)
}
w.Write([]byte(err.Error()))
return
}
if err = tmpl.Execute(w, data); err != nil {
w.Write([]byte(err.Error()))
}
} // }}}
func sessionCreate(w http.ResponseWriter, r *http.Request) { // {{{
logger.Info("webserver", "request", "/session/create")
session, err := CreateSession()
if err != nil {
responseError(w, err)
return
}
responseData(w, map[string]interface{}{
"OK": true,
"Session": session,
})
} // }}}
func sessionRetrieve(w http.ResponseWriter, r *http.Request) { // {{{
logger.Info("webserver", "request", "/session/retrieve")
var err error
var found bool
var session Session
if session, found, err = ValidateSession(r, false); err != nil {
responseError(w, err)
return
}
responseData(w, map[string]interface{}{
"OK": true,
"Valid": found,
"Session": session,
})
} // }}}
func sessionAuthenticate(w http.ResponseWriter, r *http.Request) { // {{{
logger.Info("webserver", "request", "/session/authenticate")
var err error
var session Session
var authenticated bool
// Validate session
if session, _, err = ValidateSession(r, true); err != nil {
responseError(w, err)
return
}
req := struct {
Username string
Password string
}{}
if err = parseRequest(r, &req); err != nil {
responseError(w, err)
return
}
if authenticated, err = session.Authenticate(req.Username, req.Password); err != nil {
responseError(w, err)
return
}
responseData(w, map[string]interface{}{
"OK": true,
"Authenticated": authenticated,
"Session": session,
})
} // }}}
func userPassword(w http.ResponseWriter, r *http.Request) { // {{{
var err error
var ok bool
var session Session
if session, _, err = ValidateSession(r, true); err != nil {
responseError(w, err)
return
}
req := struct {
CurrentPassword string
NewPassword string
}{}
if err = parseRequest(r, &req); err != nil {
responseError(w, err)
return
}
ok, err = session.UpdatePassword(req.CurrentPassword, req.NewPassword)
if err != nil {
responseError(w, err)
return
}
responseData(w, map[string]interface{}{
"OK": true,
"CurrentPasswordOK": ok,
})
} // }}}
func nodeTree(w http.ResponseWriter, r *http.Request) { // {{{
var err error
var session Session
if session, _, err = ValidateSession(r, true); err != nil {
responseError(w, err)
return
}
req := struct{ StartNodeID int }{}
if err = parseRequest(r, &req); err != nil {
responseError(w, err)
return
}
nodes, err := session.NodeTree(req.StartNodeID)
if err != nil {
responseError(w, err)
return
}
responseData(w, map[string]interface{}{
"OK": true,
"Nodes": nodes,
})
} // }}}
func nodeRetrieve(w http.ResponseWriter, r *http.Request) { // {{{
logger.Info("webserver", "request", "/node/retrieve")
var err error
var session Session
if session, _, err = ValidateSession(r, true); err != nil {
responseError(w, err)
return
}
req := struct{ ID int }{}
if err = parseRequest(r, &req); err != nil {
responseError(w, err)
return
}
node, err := session.Node(req.ID)
if err != nil {
responseError(w, err)
return
}
responseData(w, map[string]interface{}{
"OK": true,
"Node": node,
})
} // }}}
func nodeCreate(w http.ResponseWriter, r *http.Request) { // {{{
logger.Info("webserver", "request", "/node/create")
var err error
var session Session
if session, _, err = ValidateSession(r, true); err != nil {
responseError(w, err)
return
}
req := struct {
Name string
ParentID int
}{}
if err = parseRequest(r, &req); err != nil {
responseError(w, err)
return
}
node, err := session.CreateNode(req.ParentID, req.Name)
if err != nil {
responseError(w, err)
return
}
responseData(w, map[string]interface{}{
"OK": true,
"Node": node,
})
} // }}}
func nodeUpdate(w http.ResponseWriter, r *http.Request) { // {{{
logger.Info("webserver", "request", "/node/update")
var err error
var session Session
if session, _, err = ValidateSession(r, true); err != nil {
responseError(w, err)
return
}
req := struct {
NodeID int
Content string
CryptoKeyID int
}{}
if err = parseRequest(r, &req); err != nil {
responseError(w, err)
return
}
err = session.UpdateNode(req.NodeID, req.Content, req.CryptoKeyID)
if err != nil {
responseError(w, err)
return
}
responseData(w, map[string]interface{}{
"OK": true,
})
} // }}}
func nodeRename(w http.ResponseWriter, r *http.Request) { // {{{
logger.Info("webserver", "request", "/node/rename")
var err error
var session Session
if session, _, err = ValidateSession(r, true); err != nil {
responseError(w, err)
return
}
req := struct {
NodeID int
Name string
}{}
if err = parseRequest(r, &req); err != nil {
responseError(w, err)
return
}
err = session.RenameNode(req.NodeID, req.Name)
if err != nil {
responseError(w, err)
return
}
responseData(w, map[string]interface{}{
"OK": true,
})
} // }}}
func nodeDelete(w http.ResponseWriter, r *http.Request) { // {{{
logger.Info("webserver", "request", "/node/delete")
var err error
var session Session
if session, _, err = ValidateSession(r, true); err != nil {
responseError(w, err)
return
}
req := struct {
NodeID int
}{}
if err = parseRequest(r, &req); err != nil {
responseError(w, err)
return
}
err = session.DeleteNode(req.NodeID)
if err != nil {
responseError(w, err)
return
}
responseData(w, map[string]interface{}{
"OK": true,
})
} // }}}
func nodeUpload(w http.ResponseWriter, r *http.Request) { // {{{
logger.Info("webserver", "request", "/node/upload")
var err error
var session Session
if session, _, err = ValidateSession(r, true); err != nil {
responseError(w, err)
return
}
// Parse our multipart form, 10 << 20 specifies a maximum
// upload of 10 MB files.
r.Body = http.MaxBytesReader(w, r.Body, 128<<20+512)
r.ParseMultipartForm(128 << 20)
// FormFile returns the first file for the given key `myFile`
// it also returns the FileHeader so we can get the Filename,
// the Header and the size of the file
file, handler, err := r.FormFile("file")
if err != nil {
responseError(w, err)
return
}
defer file.Close()
// Read metadata of file for database, and also file contents
// for MD5, which is used to store the file on disk.
fileBytes, err := io.ReadAll(file)
if err != nil {
responseError(w, err)
return
}
md5sumBytes := md5.Sum(fileBytes)
md5sum := hex.EncodeToString(md5sumBytes[:])
var nodeID int
if nodeID, err = strconv.Atoi(r.PostFormValue("NodeID")); err != nil {
responseError(w, err)
return
}
nodeFile := File{
NodeID: nodeID,
Filename: handler.Filename,
Size: handler.Size,
MIME: handler.Header.Get("Content-Type"),
MD5: md5sum,
}
if err = session.AddFile(&nodeFile); err != nil {
responseError(w, err)
return
}
// Files are stored in a directory structure composed of
// the first three characters in the md5sum, which is statistically
// distributed by design, making sure there aren't too many files in
// a single directory.
path := filepath.Join(
config.Application.Directories.Upload,
md5sum[0:1],
md5sum[1:2],
md5sum[2:3],
)
if err = os.MkdirAll(path, 0755); err != nil {
responseError(w, err)
return
}
path = filepath.Join(
config.Application.Directories.Upload,
md5sum[0:1],
md5sum[1:2],
md5sum[2:3],
md5sum,
)
if err = os.WriteFile(path, fileBytes, 0644); err != nil {
responseError(w, err)
return
}
responseData(w, map[string]interface{}{
"OK": true,
"File": nodeFile,
})
} // }}}
func nodeDownload(w http.ResponseWriter, r *http.Request) { // {{{
logger.Info("webserver", "request", "/node/download")
var err error
var session Session
var files []File
if session, _, err = ValidateSession(r, true); err != nil {
responseError(w, err)
return
}
req := struct {
NodeID int
FileID int
}{}
if err = parseRequest(r, &req); err != nil {
responseError(w, err)
return
}
files, err = session.Files(req.NodeID, req.FileID)
if err != nil {
responseError(w, err)
return
}
if len(files) != 1 {
responseError(w, fmt.Errorf("File not found"))
return
}
var file *os.File
fname := filepath.Join(
config.Application.Directories.Upload,
files[0].MD5[0:1],
files[0].MD5[1:2],
files[0].MD5[2:3],
files[0].MD5,
)
file, err = os.Open(fname)
if err != nil {
responseError(w, err)
return
}
var finfo os.FileInfo
finfo, err = file.Stat()
if err != nil {
responseError(w, err)
return
}
w.Header().Add("Content-Type", files[0].MIME)
w.Header().Add("Content-Disposition", fmt.Sprintf(`inline; filename="%s"`, files[0].Filename))
w.Header().Add("Content-Length", strconv.Itoa(int(finfo.Size())))
read := 1
var buf []byte
for read > 0 {
buf = make([]byte, 65536)
read, err = file.Read(buf)
if read > 0 {
w.Write(buf[0:read])
}
}
} // }}}
func nodeFiles(w http.ResponseWriter, r *http.Request) { // {{{
logger.Info("webserver", "request", "/node/files")
var err error
var session Session
var files []File
if session, _, err = ValidateSession(r, true); err != nil {
responseError(w, err)
return
}
req := struct {
NodeID int
}{}
if err = parseRequest(r, &req); err != nil {
responseError(w, err)
return
}
files, err = session.Files(req.NodeID, 0)
if err != nil {
responseError(w, err)
return
}
responseData(w, map[string]interface{}{
"OK": true,
"Files": files,
})
} // }}}
func nodeSearch(w http.ResponseWriter, r *http.Request) { // {{{
logger.Info("webserver", "request", "/node/search")
var err error
var session Session
var nodes []Node
if session, _, err = ValidateSession(r, true); err != nil {
responseError(w, err)
return
}
req := struct {
Search string
}{}
if err = parseRequest(r, &req); err != nil {
responseError(w, err)
return
}
nodes, err = session.SearchNodes(req.Search)
if err != nil {
responseError(w, err)
return
}
responseData(w, map[string]interface{}{
"OK": true,
"Nodes": nodes,
})
} // }}}
func keyRetrieve(w http.ResponseWriter, r *http.Request) { // {{{
logger.Info("webserver", "request", "/key/retrieve")
var err error
var session Session
if session, _, err = ValidateSession(r, true); err != nil {
responseError(w, err)
return
}
keys, err := session.Keys()
if err != nil {
responseError(w, err)
return
}
responseData(w, map[string]interface{}{
"OK": true,
"Keys": keys,
})
} // }}}
func keyCreate(w http.ResponseWriter, r *http.Request) { // {{{
logger.Info("webserver", "request", "/key/create")
var err error
var session Session
if session, _, err = ValidateSession(r, true); err != nil {
responseError(w, err)
return
}
req := struct {
Description string
Key string
}{}
if err = parseRequest(r, &req); err != nil {
responseError(w, err)
return
}
key, err := session.KeyCreate(req.Description, req.Key)
if err != nil {
responseError(w, err)
return
}
responseData(w, map[string]interface{}{
"OK": true,
"Key": key,
})
} // }}}
func keyCounter(w http.ResponseWriter, r *http.Request) { // {{{
logger.Info("webserver", "request", "/key/counter")
var err error
var session Session
if session, _, err = ValidateSession(r, true); err != nil {
responseError(w, err)
return
}
counter, err := session.KeyCounter()
if err != nil {
responseError(w, err)
return
}
responseData(w, map[string]interface{}{
"OK": true,
// Javascript uses int32, thus getting a string counter for Javascript BigInt to parse.
"Counter": strconv.FormatInt(counter, 10),
})
} // }}}
func newTemplate(requestPath string) (tmpl *template.Template, err error) { // {{{
// Append index.html if needed for further reading of the file
p := requestPath
if p[len(p)-1] == '/' {
p += "index.html"
}
p = config.Application.Directories.Static + p
base := path.Base(p)
if tmpl, err = template.New(base).ParseFiles(p); err != nil {
return
}
return
} // }}}
// vim: foldmethod=marker