package main import ( // External "git.gibonuddevalla.se/go/webservice" // Internal "git.gibonuddevalla.se/go/webservice/session" // Standard "crypto/md5" "embed" "encoding/hex" "flag" "fmt" "io" "log/slog" "net/http" "os" "path/filepath" "strconv" "strings" ) const LISTEN_HOST = "0.0.0.0" var ( flagPort int flagVersion bool flagCreateUser bool flagCheckLocal bool flagConfig string service *webservice.Service connectionManager ConnectionManager static http.Handler config Config logger *slog.Logger VERSION string //go:embed version sql/* embeddedSQL embed.FS //go:embed static staticFS embed.FS ) func sqlProvider(dbname string, version int) (sql []byte, found bool) { var err error sql, err = embeddedSQL.ReadFile(fmt.Sprintf("sql/%05d.sql", version)) if err != nil { return } found = true return } func init() { // {{{ version, _ := embeddedSQL.ReadFile("version") VERSION = strings.TrimSpace(string(version)) opt := slog.HandlerOptions{} opt.Level = slog.LevelDebug 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.BoolVar(&flagCheckLocal, "checklocal", false, "Check for local static file before embedded") 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("application", "error", err) os.Exit(1) } service, err = webservice.New(flagConfig, VERSION, logger) if err != nil { logger.Error("application", "error", err) os.Exit(1) } service.SetDatabase(sqlProvider) service.SetStaticDirectory("static", true) service.SetStaticFS(staticFS, "static") service.Register("/node/upload", true, true, nodeUpload) service.Register("/node/tree", true, true, nodeTree) service.Register("/node/retrieve", true, true, nodeRetrieve) service.Register("/node/create", true, true, nodeCreate) service.Register("/node/update", true, true, nodeUpdate) service.Register("/node/rename", true, true, nodeRename) service.Register("/node/delete", true, true, nodeDelete) service.Register("/node/download", true, true, nodeDownload) service.Register("/node/search", true, true, nodeSearch) service.Register("/key/retrieve", true, true, keyRetrieve) service.Register("/key/create", true, true, keyCreate) service.Register("/key/counter", true, true, keyCounter) service.Register("/", false, false, service.StaticHandler) if flagCreateUser { service.CreateUserPrompt() os.Exit(0) } err = service.Start() if err != nil { logger.Error("webserver", "error", err) os.Exit(1) } } // }}} /* 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, sess *session.T) { // {{{ logger.Info("webserver", "request", "/node/tree") var err error req := struct{ StartNodeID int }{} if err = parseRequest(r, &req); err != nil { responseError(w, err) return } nodes, err := NodeTree(sess.UserID, 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, sess *session.T) { // {{{ logger.Info("webserver", "request", "/node/retrieve") var err error req := struct{ ID int }{} if err = parseRequest(r, &req); err != nil { responseError(w, err) return } node, err := RetrieveNode(sess.UserID, 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, sess *session.T) { // {{{ logger.Info("webserver", "request", "/node/create") var err error req := struct { Name string ParentID int }{} if err = parseRequest(r, &req); err != nil { responseError(w, err) return } node, err := CreateNode(sess.UserID, 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, sess *session.T) { // {{{ logger.Info("webserver", "request", "/node/update") var err error req := struct { NodeID int Content string CryptoKeyID int Markdown bool }{} if err = parseRequest(r, &req); err != nil { responseError(w, err) return } err = UpdateNode(sess.UserID, req.NodeID, req.Content, req.CryptoKeyID, req.Markdown) if err != nil { responseError(w, err) return } responseData(w, map[string]interface{}{ "OK": true, }) } // }}} func nodeRename(w http.ResponseWriter, r *http.Request, sess *session.T) { // {{{ var err error logger.Info("webserver", "request", "/node/rename") req := struct { NodeID int Name string }{} if err = parseRequest(r, &req); err != nil { responseError(w, err) return } err = RenameNode(sess.UserID, 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, sess *session.T) { // {{{ var err error logger.Info("webserver", "request", "/node/delete") req := struct { NodeID int }{} if err = parseRequest(r, &req); err != nil { responseError(w, err) return } err = DeleteNode(sess.UserID, req.NodeID) if err != nil { responseError(w, err) return } responseData(w, map[string]interface{}{ "OK": true, }) } // }}} func nodeUpload(w http.ResponseWriter, r *http.Request, sess *session.T) { // {{{ logger.Info("webserver", "request", "/node/upload") var err error // 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 = AddFile(sess.UserID, &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, sess *session.T) { // {{{ logger.Info("webserver", "request", "/node/download") var err error var files []File req := struct { NodeID int FileID int }{} if err = parseRequest(r, &req); err != nil { responseError(w, err) return } files, err = Files(sess.UserID, 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, sess *session.T) { // {{{ logger.Info("webserver", "request", "/node/files") var err error var files []File req := struct { NodeID int }{} if err = parseRequest(r, &req); err != nil { responseError(w, err) return } files, err = Files(sess.UserID, 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, sess *session.T) { // {{{ logger.Info("webserver", "request", "/node/search") var err error var nodes []Node req := struct { Search string }{} if err = parseRequest(r, &req); err != nil { responseError(w, err) return } nodes, err = SearchNodes(sess.UserID, 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, sess *session.T) { // {{{ logger.Info("webserver", "request", "/key/retrieve") var err error keys, err := Keys(sess.UserID) if err != nil { responseError(w, err) return } responseData(w, map[string]interface{}{ "OK": true, "Keys": keys, }) } // }}} func keyCreate(w http.ResponseWriter, r *http.Request, sess *session.T) { // {{{ logger.Info("webserver", "request", "/key/create") var err error req := struct { Description string Key string }{} if err = parseRequest(r, &req); err != nil { responseError(w, err) return } key, err := KeyCreate(sess.UserID, 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, sess *session.T) { // {{{ logger.Info("webserver", "request", "/key/counter") var err error counter, err := 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), }) } // }}} // vim: foldmethod=marker