package main import ( // Standard "crypto/md5" "encoding/hex" "path/filepath" "flag" "fmt" "html/template" "io" "log" "net/http" "os" "path" "regexp" "strings" "strconv" "time" _ "embed" ) const VERSION = "v11"; const LISTEN_HOST = "0.0.0.0"; const DB_SCHEMA = 13 var ( flagPort int connectionManager ConnectionManager static http.Handler config Config ) func init() {// {{{ flag.IntVar( &flagPort, "port", 1371, "TCP port to listen on", ) flag.Parse() if false { time.Sleep(time.Second*1) } }// }}} func main() {// {{{ var err error log.Printf("\x1b[32mNotes\x1b[0m %s\n", VERSION) config, err = ConfigRead(os.Getenv("HOME")+"/.config/notes.yaml") if err != nil { fmt.Printf("%s\n", err) os.Exit(1) } if err = dbInit(); err != nil { fmt.Printf("%s\n", err) os.Exit(1) } 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) log.Printf("\x1b[32mNotes\x1b[0m Listening on %s\n", listen) log.Printf("\x1b[32mNotes\x1b[0m Answer for domains %s\n", strings.Join(config.Websocket.Domains, ", ")) http.ListenAndServe(listen, nil) }// }}} func cssUpdateHandler(w http.ResponseWriter, r *http.Request) {// {{{ log.Println("[BROADCAST] 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 { log.Printf("[Connection] %s\n", 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. log.Printf("static: %s", 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. log.Printf("template: %s", 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) {// {{{ log.Println("/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) {// {{{ log.Println("/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) {// {{{ log.Println("/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) {// {{{ log.Println("/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) {// {{{ log.Println("/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) {// {{{ log.Println("/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) {// {{{ 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) {// {{{ 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) {// {{{ log.Println("/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) {// {{{ log.Println("/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) {// {{{ 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) {// {{{ log.Println("/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) {// {{{ log.Println("/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) {// {{{ log.Println("/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) {// {{{ log.Println("/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