From 792eff78cc154d73a5412c4e6cfec9c57e22fdb6 Mon Sep 17 00:00:00 2001 From: Julien Riou Date: Thu, 28 Aug 2025 16:49:27 +0200 Subject: [PATCH 01/39] chore: Add release action in Makefile Signed-off-by: Julien Riou --- .gitignore | 1 + Makefile | 9 ++++++++- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index fb79096..cf8b1ff 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ bin +releases collerd.db !docker/collerd.json collerd.json diff --git a/Makefile b/Makefile index f459409..d73d30f 100644 --- a/Makefile +++ b/Makefile @@ -1,6 +1,8 @@ APPVERSION := $(shell cat ./VERSION) GOVERSION := $(shell go version | awk '{print $$3}') GITCOMMIT := $(shell git log -1 --oneline | awk '{print $$1}') +OS := $(shell uname -s | tr [A-Z] [a-z]) +ARCH := $(shell uname -m | tr [A-Z] [a-z]) LDFLAGS = -X main.AppVersion=${APPVERSION} -X main.GoVersion=${GOVERSION} -X main.GitCommit=${GITCOMMIT} .PHONY: clean test @@ -12,6 +14,11 @@ build: && go build -ldflags "${LDFLAGS}" -o ../bin/coller cmd/coller/main.go \ ) +archive: + (mkdir -p releases && cd bin && tar cvzpf ../releases/coller-${APPVERSION}-${OS}-${ARCH}.tar.gz * && cd ../releases && sha256sum *.tar.gz) + +release: build archive + test: (cd src \ && go test internal/*.go \ @@ -19,4 +26,4 @@ test: ) clean: - rm -rf bin + rm -rf bin releases From af5baa999c14b2ef9a6e1adb17dd2f18103b5adb Mon Sep 17 00:00:00 2001 From: Thibault Piron Date: Tue, 9 Sep 2025 15:57:23 +0200 Subject: [PATCH 02/39] fix: display error page if note not found (#24) Fix panic when trying to access a non existing note with the "pretty" url (.html) Reviewed-on: https://git.riou.xyz/jriou/coller/pulls/24 Reviewed-by: Julien Riou Co-authored-by: Thibault Piron Co-committed-by: Thibault Piron --- src/server/server.go | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/server/server.go b/src/server/server.go index 419c553..20e91c0 100644 --- a/src/server/server.go +++ b/src/server/server.go @@ -377,6 +377,12 @@ func (h *GetProtectedWebNoteHandler) ServeHTTP(w http.ResponseWriter, r *http.Re return } + if note == nil { + h.PageData.Err = fmt.Errorf("Note doesn't exist or has been deleted") + h.Templates.ExecuteTemplate(w, templateName, h.PageData) + return + } + if password != "" && note.Encrypted { note.Content, err = internal.Decrypt(note.Content, password) if err != nil { From 401397241b9c93f42854eb411c8a391da011d81f Mon Sep 17 00:00:00 2001 From: Julien Riou Date: Wed, 10 Sep 2025 07:02:48 +0200 Subject: [PATCH 03/39] fix: Upload text files other than plain text Fixes #23 Signed-off-by: Julien Riou --- src/server/server.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/server/server.go b/src/server/server.go index 20e91c0..224c46e 100644 --- a/src/server/server.go +++ b/src/server/server.go @@ -246,7 +246,7 @@ func (h *CreateNoteWithFormHandler) ServeHTTP(w http.ResponseWriter, r *http.Req } h.logger.Debug("checking file content type") - if handler.Header.Get("Content-Type") != "text/plain" { + if !strings.HasPrefix(handler.Header.Get("Content-Type"), "text/") { h.PageData.Err = fmt.Errorf("text file expected (got %s)", handler.Header.Get("Content-Type")) h.Templates.ExecuteTemplate(w, templateName, h.PageData) return From 938d10c3a6770c78125af56a55fdaab921e9bbb2 Mon Sep 17 00:00:00 2001 From: tapiron Date: Sat, 13 Sep 2025 19:10:50 +0200 Subject: [PATCH 04/39] fix: remove double rendering and test error (#26) Remove double template rendering and test error for non password protected note Co-authored-by: Thibault Piron Reviewed-on: https://git.riou.xyz/jriou/coller/pulls/26 Reviewed-by: Julien Riou Co-authored-by: tapiron Co-committed-by: tapiron --- src/server/server.go | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/server/server.go b/src/server/server.go index 224c46e..28bd435 100644 --- a/src/server/server.go +++ b/src/server/server.go @@ -345,9 +345,6 @@ func (h *GetWebNoteHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { return } - h.PageData.Err = fmt.Errorf("jriou") - h.Templates.ExecuteTemplate(w, templateName, h.PageData) - h.PageData.Note = note h.logger.Debug("rendering note web page") From 33628331f4330712781e2b3170236b16b24d9bc2 Mon Sep 17 00:00:00 2001 From: tapiron Date: Mon, 15 Sep 2025 06:51:16 +0200 Subject: [PATCH 05/39] feat: set default expiration in select (#28) No default value in select could be interpreted as ever lasting note. Default value in the select makes that clearer. Co-authored-by: Thibault Piron Reviewed-on: https://git.riou.xyz/jriou/coller/pulls/28 Reviewed-by: Julien Riou Co-authored-by: tapiron Co-committed-by: tapiron --- src/server/server.go | 2 ++ src/server/templates/index.html | 6 +++--- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/server/server.go b/src/server/server.go index 28bd435..92dc217 100644 --- a/src/server/server.go +++ b/src/server/server.go @@ -189,6 +189,7 @@ type PageData struct { Title string Version string Expirations []int + Expiration int Languages []string Err error URL string @@ -423,6 +424,7 @@ func (s *Server) Start() error { p := PageData{ Title: s.config.Title, Expirations: s.config.Expirations, + Expiration: s.config.Expiration, Languages: s.config.Languages, } diff --git a/src/server/templates/index.html b/src/server/templates/index.html index 42faef1..0cf1d6d 100644 --- a/src/server/templates/index.html +++ b/src/server/templates/index.html @@ -36,9 +36,9 @@
From e03aa6b87abcba0e2b929a922de4ef5b87eca664 Mon Sep 17 00:00:00 2001 From: tapiron Date: Mon, 15 Sep 2025 09:20:50 +0200 Subject: [PATCH 06/39] feat: add possibility to hide upload button (#25) I was asked to add the possibility to hide the upload button to avoid users confuse this tool for a file sharing app. This is only on the front. No breaking changes, by default the button is displayed. Co-authored-by: Thibault Piron Reviewed-on: https://git.riou.xyz/jriou/coller/pulls/25 Reviewed-by: Julien Riou Co-authored-by: tapiron Co-committed-by: tapiron --- src/cmd/collerd/README.md | 1 + src/server/config.go | 40 +++++++++++++++++---------------- src/server/server.go | 18 ++++++++------- src/server/templates/index.html | 2 ++ 4 files changed, 34 insertions(+), 27 deletions(-) diff --git a/src/cmd/collerd/README.md b/src/cmd/collerd/README.md index 30eb167..d99a273 100644 --- a/src/cmd/collerd/README.md +++ b/src/cmd/collerd/README.md @@ -33,6 +33,7 @@ The file format is **JSON**: * **observation_internal** (int): Number of seconds to wait between two observations (default 60) * **languages** ([]string): List of supported [languages](https://github.com/microsoft/monaco-editor/tree/main/src/basic-languages) * **language** (string): Default language (default "text") +* **enable_upload_file_button** (bool): Display the upload file button in the UI (default true) The configuration file is not required but the service might not be exposed to the public. diff --git a/src/server/config.go b/src/server/config.go index 4e6cdc7..8ba51ca 100644 --- a/src/server/config.go +++ b/src/server/config.go @@ -7,24 +7,25 @@ import ( ) type Config struct { - Title string `json:"title"` - DatabaseType string `json:"database_type"` - DatabaseDsn string `json:"database_dsn"` - IDLength int `json:"id_length"` - PasswordLength int `json:"password_length"` - ExpirationInterval int `json:"expiration_interval"` - ListenAddress string `json:"listen_address"` - ListenPort int `json:"listen_port"` - Expirations []int `json:"expirations"` - Expiration int `json:"expiration"` - MaxUploadSize int64 `json:"max_upload_size"` - ShowVersion bool `json:"show_version"` - EnableMetrics bool `json:"enable_metrics"` - PrometheusRoute string `json:"prometheus_route"` - PrometheusNotesMetric string `json:"prometheus_notes_metric"` - ObservationInterval int `json:"observation_internal"` - Languages []string `json:"languages"` - Language string `json:"language"` + Title string `json:"title"` + DatabaseType string `json:"database_type"` + DatabaseDsn string `json:"database_dsn"` + IDLength int `json:"id_length"` + PasswordLength int `json:"password_length"` + ExpirationInterval int `json:"expiration_interval"` + ListenAddress string `json:"listen_address"` + ListenPort int `json:"listen_port"` + Expirations []int `json:"expirations"` + Expiration int `json:"expiration"` + MaxUploadSize int64 `json:"max_upload_size"` + ShowVersion bool `json:"show_version"` + EnableMetrics bool `json:"enable_metrics"` + PrometheusRoute string `json:"prometheus_route"` + PrometheusNotesMetric string `json:"prometheus_notes_metric"` + ObservationInterval int `json:"observation_internal"` + Languages []string `json:"languages"` + Language string `json:"language"` + EnableUploadFileButton bool `json:"enable_upload_file_button"` } func NewConfig() *Config { @@ -69,7 +70,8 @@ func NewConfig() *Config { "SQL", "YAML", }, - Language: "text", + Language: "text", + EnableUploadFileButton: true, } } diff --git a/src/server/server.go b/src/server/server.go index 92dc217..1b406fe 100644 --- a/src/server/server.go +++ b/src/server/server.go @@ -186,14 +186,15 @@ func (h *GetProtectedNoteHandler) ServeHTTP(w http.ResponseWriter, r *http.Reque } type PageData struct { - Title string - Version string - Expirations []int - Expiration int - Languages []string - Err error - URL string - Note *Note + Title string + Version string + Expirations []int + Expiration int + Languages []string + Err error + URL string + Note *Note + EnableUploadFileButton bool } type HomeHandler struct { @@ -431,6 +432,7 @@ func (s *Server) Start() error { if s.config.ShowVersion { p.Version = s.version } + p.EnableUploadFileButton = s.config.EnableUploadFileButton templates, err := template.New("templates").Funcs(funcs).ParseFS(templatesFS, "templates/*.html") if err != nil { diff --git a/src/server/templates/index.html b/src/server/templates/index.html index 0cf1d6d..4c18a78 100644 --- a/src/server/templates/index.html +++ b/src/server/templates/index.html @@ -31,9 +31,11 @@ value="delete-after-read" name="delete-after-read"> + {{if .EnableUploadFileButton}}
+ {{end}}
+ title="Letters and numbers with length from 16 to 256" class="form-control" id="encryption-key" + name="encryption-key">
- - + +
Date: Wed, 24 Sep 2025 09:50:13 +0200 Subject: [PATCH 18/39] refactor: Rename protected to encrypted Signed-off-by: Julien Riou --- src/server/handlers_api.go | 4 ++-- src/server/handlers_web.go | 4 ++-- src/server/server.go | 8 ++++---- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/server/handlers_api.go b/src/server/handlers_api.go index feab64e..47bc512 100644 --- a/src/server/handlers_api.go +++ b/src/server/handlers_api.go @@ -89,12 +89,12 @@ func (h *GetNoteHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { } } -type GetProtectedNoteHandler struct { +type GetEncryptedNoteHandler struct { logger *slog.Logger db *Database } -func (h *GetProtectedNoteHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { +func (h *GetEncryptedNoteHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "text/plain; charset=utf-8") vars := mux.Vars(r) diff --git a/src/server/handlers_web.go b/src/server/handlers_web.go index d5e92ad..896169e 100644 --- a/src/server/handlers_web.go +++ b/src/server/handlers_web.go @@ -191,7 +191,7 @@ func (h *GetWebNoteHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { h.Templates.ExecuteTemplate(w, "note", h.PageData) } -func (h *GetProtectedWebNoteHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { +func (h *GetEncryptedWebNoteHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { h.PageData.Err = nil templateName := "note" @@ -224,7 +224,7 @@ func (h *GetProtectedWebNoteHandler) ServeHTTP(w http.ResponseWriter, r *http.Re h.PageData.Note = note - h.logger.Debug("rendering protected note web page") + h.logger.Debug("rendering encrypted note web page") h.Templates.ExecuteTemplate(w, "note", h.PageData) } diff --git a/src/server/server.go b/src/server/server.go index b8b3778..85e494c 100644 --- a/src/server/server.go +++ b/src/server/server.go @@ -71,7 +71,7 @@ func WriteError(w http.ResponseWriter, message string, err error) { }.ToJSON()) } -type GetProtectedWebNoteHandler struct { +type GetEncryptedWebNoteHandler struct { Templates *template.Template PageData PageData logger *slog.Logger @@ -100,7 +100,7 @@ func (s *Server) Start() error { // API r.Path("/api/note").Handler(&CreateNoteHandler{logger: s.logger, db: s.db, maxUploadSize: s.config.MaxUploadSize}).Methods("POST") - r.Path("/{id:[a-zA-Z0-9]+}/{encryptionKey:[a-zA-Z0-9]+}").Handler(&GetProtectedNoteHandler{logger: s.logger, db: s.db}).Methods("GET") + r.Path("/{id:[a-zA-Z0-9]+}/{encryptionKey:[a-zA-Z0-9]+}").Handler(&GetEncryptedNoteHandler{logger: s.logger, db: s.db}).Methods("GET") r.Path("/{id:[a-zA-Z0-9]+}").Handler(&GetNoteHandler{logger: s.logger, db: s.db}).Methods("GET") // Web pages @@ -144,13 +144,13 @@ func (s *Server) Start() error { r.Path("/clients.html").Handler(clientsHandler).Methods("GET") r.Path("/clients/{os:[a-z]+}-{arch:[a-z0-9]+}/{clientName:[a-z]+}").Handler(&ClientHandler{logger: s.logger, version: p.Version}).Methods("GET") - protectedWebNoteHandler := &GetProtectedWebNoteHandler{ + encryptedWebNoteHandler := &GetEncryptedWebNoteHandler{ Templates: templates, PageData: p, logger: s.logger, db: s.db, } - r.Path("/{id:[a-zA-Z0-9]+}/{encryptionKey:[a-zA-Z0-9]+}.html").Handler(protectedWebNoteHandler).Methods("GET") + r.Path("/{id:[a-zA-Z0-9]+}/{encryptionKey:[a-zA-Z0-9]+}.html").Handler(encryptedWebNoteHandler).Methods("GET") webNoteHandler := &GetWebNoteHandler{ Templates: templates, From 61ca30690b824de58022ab55b195e6dfa51bf7f7 Mon Sep 17 00:00:00 2001 From: Julien Riou Date: Wed, 24 Sep 2025 17:44:50 +0200 Subject: [PATCH 19/39] feat: Disable levels of encryptions by default - Add `allow_client_encryption_key` option to allow encryption key provided by the client on the web UI (false by default) - Add `allow_no_encryption` option to allow notes without encryption (disabled by default) Signed-off-by: Julien Riou --- src/cmd/collerd/README.md | 2 ++ src/server/config.go | 46 +++++++++++++++++---------------- src/server/handlers_api.go | 18 ++++++++++--- src/server/handlers_web.go | 34 ++++++++++++++++-------- src/server/server.go | 45 +++++++++++++++++++++++++------- src/server/templates/index.html | 7 ++++- 6 files changed, 105 insertions(+), 47 deletions(-) diff --git a/src/cmd/collerd/README.md b/src/cmd/collerd/README.md index ca8674d..4ad8b1e 100644 --- a/src/cmd/collerd/README.md +++ b/src/cmd/collerd/README.md @@ -20,6 +20,8 @@ The file format is **JSON**: * **database_dsn** (string): Connection string for the database (default "collerd.db") * **node_id** (int): Number between 0 and 1023 to define the node generating identifiers (see [snowflake](https://github.com/bwmarrin/snowflake)) * **encryption_key_length** (int): Number of characters for generated encryption key (default 16) +* **allow_client_encryption_key** (bool): Allow encryption key provided by the client on the web UI +* **allow_no_encryption** (bool): Allow notes without encryption * **expiration_interval** (int): Number of seconds to wait between two expiration runs * **listen_address** (string): Address to listen for the web server (default "0.0.0.0") * **listen_port** (int): Port to listen for the web server (default 8080) diff --git a/src/server/config.go b/src/server/config.go index f2a6a6b..ea5e5d2 100644 --- a/src/server/config.go +++ b/src/server/config.go @@ -7,28 +7,30 @@ import ( ) type Config struct { - Title string `json:"title"` - DatabaseType string `json:"database_type"` - DatabaseDsn string `json:"database_dsn"` - NodeID int64 `json:"node_id"` - EncryptionKeyLength int `json:"encryption_key_length"` - ExpirationInterval int `json:"expiration_interval"` - ListenAddress string `json:"listen_address"` - ListenPort int `json:"listen_port"` - Expirations []int `json:"expirations"` - Expiration int `json:"expiration"` - MaxUploadSize int64 `json:"max_upload_size"` - ShowVersion bool `json:"show_version"` - EnableMetrics bool `json:"enable_metrics"` - PrometheusRoute string `json:"prometheus_route"` - PrometheusNotesMetric string `json:"prometheus_notes_metric"` - ObservationInterval int `json:"observation_internal"` - Languages []string `json:"languages"` - Language string `json:"language"` - EnableUploadFileButton bool `json:"enable_upload_file_button"` - TLSCertFile string `json:"tls_cert_file"` - TLSKeyFile string `json:"tls_key_file"` - BootstrapDirectory string `json:"bootstrap_directory"` + Title string `json:"title"` + DatabaseType string `json:"database_type"` + DatabaseDsn string `json:"database_dsn"` + NodeID int64 `json:"node_id"` + EncryptionKeyLength int `json:"encryption_key_length"` + AllowClientEncryptionKey bool `json:"allow_client_encryption_key"` + AllowNoEncryption bool `json:"allow_no_encryption"` + ExpirationInterval int `json:"expiration_interval"` + ListenAddress string `json:"listen_address"` + ListenPort int `json:"listen_port"` + Expirations []int `json:"expirations"` + Expiration int `json:"expiration"` + MaxUploadSize int64 `json:"max_upload_size"` + ShowVersion bool `json:"show_version"` + EnableMetrics bool `json:"enable_metrics"` + PrometheusRoute string `json:"prometheus_route"` + PrometheusNotesMetric string `json:"prometheus_notes_metric"` + ObservationInterval int `json:"observation_internal"` + Languages []string `json:"languages"` + Language string `json:"language"` + EnableUploadFileButton bool `json:"enable_upload_file_button"` + TLSCertFile string `json:"tls_cert_file"` + TLSKeyFile string `json:"tls_key_file"` + BootstrapDirectory string `json:"bootstrap_directory"` } func NewConfig() *Config { diff --git a/src/server/handlers_api.go b/src/server/handlers_api.go index 47bc512..51852c3 100644 --- a/src/server/handlers_api.go +++ b/src/server/handlers_api.go @@ -16,9 +16,11 @@ func HealthHandler(w http.ResponseWriter, r *http.Request) { } type CreateNoteHandler struct { - logger *slog.Logger - db *Database - maxUploadSize int64 + logger *slog.Logger + db *Database + maxUploadSize int64 + allowClientEncryptionKey bool + allowNoEncryption bool } type CreateNotePayload struct { @@ -47,6 +49,16 @@ func (h *CreateNoteHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { return } + if !h.allowNoEncryption && !body.Encrypted { + WriteError(w, "could not create note", fmt.Errorf("encryption is mandatory")) + return + } + + if !h.allowClientEncryptionKey && body.EncryptionKey != "" { + WriteError(w, "could not create note", fmt.Errorf("client encryption key is not allowed")) + return + } + content, err := internal.Decode(body.Content) if err != nil { diff --git a/src/server/handlers_web.go b/src/server/handlers_web.go index 896169e..813929a 100644 --- a/src/server/handlers_web.go +++ b/src/server/handlers_web.go @@ -17,16 +17,18 @@ import ( ) type PageData struct { - Title string - Version string - Expirations []int - Expiration int - Languages []string - Err error - URL string - Note *Note - EnableUploadFileButton bool - BootstrapDirectory string + Title string + Version string + Expirations []int + Expiration int + Languages []string + Err error + URL string + Note *Note + EnableUploadFileButton bool + AllowClientEncryptionKey bool + AllowNoEncryption bool + BootstrapDirectory string } type HomeHandler struct { @@ -115,7 +117,17 @@ func (h *CreateNoteWithFormHandler) ServeHTTP(w http.ResponseWriter, r *http.Req deleteAfterRead := r.FormValue("delete-after-read") language := r.FormValue("language") - if encryptionKey == "" && noEncryption == "" { + if !h.PageData.AllowNoEncryption && noEncryption != "" { + h.PageData.Err = fmt.Errorf("encryption is mandatory") + h.Templates.ExecuteTemplate(w, templateName, h.PageData) + } + + if !h.PageData.AllowClientEncryptionKey && encryptionKey != "" { + h.PageData.Err = fmt.Errorf("client encryption key is not allowed") + h.Templates.ExecuteTemplate(w, templateName, h.PageData) + } + + if !h.PageData.AllowClientEncryptionKey && encryptionKey == "" && noEncryption == "" { h.logger.Debug("generating encryption key") encryptionKey = internal.GenerateChars(encryptionKeyLength) } diff --git a/src/server/server.go b/src/server/server.go index 85e494c..87b8aec 100644 --- a/src/server/server.go +++ b/src/server/server.go @@ -99,9 +99,26 @@ func (s *Server) Start() error { } // API - r.Path("/api/note").Handler(&CreateNoteHandler{logger: s.logger, db: s.db, maxUploadSize: s.config.MaxUploadSize}).Methods("POST") - r.Path("/{id:[a-zA-Z0-9]+}/{encryptionKey:[a-zA-Z0-9]+}").Handler(&GetEncryptedNoteHandler{logger: s.logger, db: s.db}).Methods("GET") - r.Path("/{id:[a-zA-Z0-9]+}").Handler(&GetNoteHandler{logger: s.logger, db: s.db}).Methods("GET") + createNoteHandler := &CreateNoteHandler{ + logger: s.logger, + db: s.db, + maxUploadSize: s.config.MaxUploadSize, + allowClientEncryptionKey: s.config.AllowClientEncryptionKey, + allowNoEncryption: s.config.AllowNoEncryption, + } + r.Path("/api/note").Handler(createNoteHandler).Methods("POST") + + getEncryptedNoteHandler := &GetEncryptedNoteHandler{ + logger: s.logger, + db: s.db, + } + r.Path("/{id:[a-zA-Z0-9]+}/{encryptionKey:[a-zA-Z0-9]+}").Handler(getEncryptedNoteHandler).Methods("GET") + + getNoteHandler := &GetNoteHandler{ + logger: s.logger, + db: s.db, + } + r.Path("/{id:[a-zA-Z0-9]+}").Handler(getNoteHandler).Methods("GET") // Web pages funcs := template.FuncMap{ @@ -111,22 +128,25 @@ func (s *Server) Start() error { "string": func(b []byte) string { return string(b) }, } p := PageData{ - Title: s.config.Title, - Expirations: s.config.Expirations, - Expiration: s.config.Expiration, - Languages: s.config.Languages, - BootstrapDirectory: s.config.BootstrapDirectory, + Title: s.config.Title, + Expirations: s.config.Expirations, + Expiration: s.config.Expiration, + Languages: s.config.Languages, + BootstrapDirectory: s.config.BootstrapDirectory, + EnableUploadFileButton: s.config.EnableUploadFileButton, + AllowClientEncryptionKey: s.config.AllowClientEncryptionKey, + AllowNoEncryption: s.config.AllowNoEncryption, } if s.config.ShowVersion { p.Version = s.version } - p.EnableUploadFileButton = s.config.EnableUploadFileButton templates, err := template.New("templates").Funcs(funcs).ParseFS(templatesFS, "templates/*.html") if err != nil { return err } + createNoteWithFormHandler := &CreateNoteWithFormHandler{ Templates: templates, PageData: p, @@ -142,7 +162,12 @@ func (s *Server) Start() error { logger: s.logger, } r.Path("/clients.html").Handler(clientsHandler).Methods("GET") - r.Path("/clients/{os:[a-z]+}-{arch:[a-z0-9]+}/{clientName:[a-z]+}").Handler(&ClientHandler{logger: s.logger, version: p.Version}).Methods("GET") + + clientHandler := &ClientHandler{ + logger: s.logger, + version: p.Version, + } + r.Path("/clients/{os:[a-z]+}-{arch:[a-z0-9]+}/{clientName:[a-z]+}").Handler(clientHandler).Methods("GET") encryptedWebNoteHandler := &GetEncryptedWebNoteHandler{ Templates: templates, diff --git a/src/server/templates/index.html b/src/server/templates/index.html index f7319e9..a6363c1 100644 --- a/src/server/templates/index.html +++ b/src/server/templates/index.html @@ -13,6 +13,7 @@
+ {{if .AllowClientEncryptionKey}}
@@ -21,11 +22,14 @@ title="Letters and numbers with length from 16 to 256" class="form-control" id="encryption-key" name="encryption-key">
+ {{end}} + {{if .AllowNoEncryption}}
+ {{end}}
@@ -40,7 +44,8 @@
From 9e0254c0b51e69bd9d890ed88d09f7d0e223c35e Mon Sep 17 00:00:00 2001 From: Julien Riou Date: Sat, 27 Sep 2025 08:35:26 +0200 Subject: [PATCH 20/39] feat: Add password protection Fixes #37. BREAKING CHANGE: API routes are prefixed by /api/note. Signed-off-by: Julien Riou --- src/cmd/coller/main.go | 30 +- src/cmd/collerd/README.md | 26 +- src/cmd/copier/main.go | 47 ++- src/go.mod | 14 +- src/go.sum | 10 + src/server/config.go | 8 +- src/server/db.go | 14 +- src/server/handlers_api.go | 45 ++- src/server/handlers_web.go | 384 +++++++++++++++++++++- src/server/note.go | 1 + src/server/server.go | 78 +++-- src/server/templates/error.html | 8 + src/server/templates/index.html | 14 +- src/server/templates/note.html | 106 +++--- src/server/templates/protectedNote.html | 21 ++ src/server/templates/unprotectedNote.html | 42 +++ 16 files changed, 713 insertions(+), 135 deletions(-) create mode 100644 src/server/templates/error.html create mode 100644 src/server/templates/protectedNote.html create mode 100644 src/server/templates/unprotectedNote.html diff --git a/src/cmd/coller/main.go b/src/cmd/coller/main.go index 445ea14..c300e0e 100644 --- a/src/cmd/coller/main.go +++ b/src/cmd/coller/main.go @@ -32,6 +32,7 @@ type NotePayload struct { Expiration int `json:"expiration,omitempty"` DeleteAfterRead bool `json:"delete_after_read,omitempty"` Language string `json:"language"` + Password string `json:"password"` } type NoteResponse struct { @@ -76,6 +77,7 @@ func handleMain() int { bearer := flag.String("bearer", os.Getenv("COLLER_BEARER"), "Bearer token") askBearer := flag.Bool("ask-bearer", false, "Read bearer token from input") language := flag.String("language", "", "Language of the note") + password := flag.String("password", os.Getenv("COLLER_PASSWORD"), "Password to access the note") flag.Parse() @@ -172,6 +174,9 @@ func handleMain() int { if *language != "" { p.Language = *language } + if *password != "" { + p.Password = *password + } if *encryptionKey != "" { logger.Debug("validating encryption key") @@ -242,21 +247,24 @@ func handleMain() int { logger.Debug("finding note location") var location string noteURL := *url + "/" + jsonBody.ID - if *encryptionKey != "" { - if *copier { - location = fmt.Sprintf("copier -encryption-key %s %s", *encryptionKey, noteURL) - } else { - if *html { - location = fmt.Sprintf("%s/%s.html", noteURL, *encryptionKey) - } else { - location = fmt.Sprintf("%s/%s", noteURL, *encryptionKey) - } + if *copier { + location = "copier" + if *encryptionKey != "" { + location += " -encryption-key " + *encryptionKey } + if *password != "" { + location += " -password '" + *password + "'" + } + location += " " + noteURL } else { + location = noteURL + if *encryptionKey != "" { + location += "/" + *encryptionKey + } if *html { - location = fmt.Sprintf("%s.html", noteURL) + location += ".html" } else { - location = noteURL + location += "/raw" } } diff --git a/src/cmd/collerd/README.md b/src/cmd/collerd/README.md index 4ad8b1e..b33d207 100644 --- a/src/cmd/collerd/README.md +++ b/src/cmd/collerd/README.md @@ -22,6 +22,8 @@ The file format is **JSON**: * **encryption_key_length** (int): Number of characters for generated encryption key (default 16) * **allow_client_encryption_key** (bool): Allow encryption key provided by the client on the web UI * **allow_no_encryption** (bool): Allow notes without encryption +* **enable_password_encryption** (bool): Enable password to protect notes (default true) +* **enable_upload_file_button** (bool): Display the upload file button in the UI (default true) * **expiration_interval** (int): Number of seconds to wait between two expiration runs * **listen_address** (string): Address to listen for the web server (default "0.0.0.0") * **listen_port** (int): Port to listen for the web server (default 8080) @@ -35,7 +37,6 @@ The file format is **JSON**: * **observation_internal** (int): Number of seconds to wait between two observations (default 60) * **languages** ([]string): List of supported [languages](https://github.com/microsoft/monaco-editor/tree/main/src/basic-languages) * **language** (string): Default language (default "text") -* **enable_upload_file_button** (bool): Display the upload file button in the UI (default true) * **tls_cert_file** (string): Path to TLS certificate file to enable HTTPS * **tls_key_file** (string): Path to TLS key file to enable HTTPS * **bootstrap_directory** (string): Serve [Bootstrap](https://getbootstrap.com/) assets from this local directory (ex: "./node_modules/bootstrap/dist"). See **Dependencies** for details. @@ -64,19 +65,38 @@ Response (JSON): * **id** (string): ID of the note -### GET /\/\ +### GET /api/note/\/\ > [!WARNING] > Potential encryption key leak Return content of a note encrypted by the given encryption key. -### GET /\ +### POST /api/note/\/\ + +> [!WARNING] +> Potential encryption key leak + +Return content of a protected note encrypted by the given encryption key. + +Body (JSON): +* **password** (string): password used to protect the note (required) + +### GET /api/note/\ Return content of a note. If the note is encrypted, the encrypted value is returned (application/octet-stream). Otherwise, the text is returned (text/plain). +### POST /api/note/\ + +Return content of a protected note. + +If the note is encrypted, the encrypted value is returned (application/octet-stream). Otherwise, the text is returned (text/plain). + +Body (JSON): +* **password** (string): password used to protect the note (required) + ### Errors Errors return **500 Server Internal Error** with the **JSON** payload: diff --git a/src/cmd/copier/main.go b/src/cmd/copier/main.go index 5924936..867f852 100644 --- a/src/cmd/copier/main.go +++ b/src/cmd/copier/main.go @@ -1,11 +1,14 @@ package main import ( + "bytes" + "encoding/json" "flag" "fmt" "io" "log/slog" "net/http" + "net/url" "os" "syscall" @@ -21,6 +24,10 @@ var ( GitCommit string ) +type NotePayload struct { + Password string `json:"password"` +} + func handleMain() int { flag.Usage = usage @@ -34,6 +41,7 @@ func handleMain() int { fileName := flag.String("file", "", "Write content of the note to a file") bearer := flag.String("bearer", os.Getenv("COLLER_BEARER"), "Bearer token") askBearer := flag.Bool("ask-bearer", false, "Read bearer token from input") + password := flag.String("password", os.Getenv("COLLER_PASSWORD"), "Password to access the note") flag.Parse() @@ -47,7 +55,7 @@ func handleMain() int { return internal.RC_ERROR } - url := flag.Args()[0] + rawURL := flag.Args()[0] var level slog.Level if *debug { @@ -81,21 +89,50 @@ func handleMain() int { fmt.Print("\n") } - logger.Debug("creating http request") - req, err := http.NewRequest("GET", url, nil) + logger.Debug("parsing url", slog.Any("url", rawURL)) + u, err := url.Parse(rawURL) if err != nil { - return internal.ReturnError(logger, "could not create request", err) + return internal.ReturnError(logger, "could not parse url", err) } + u.Path = "api/note" + u.Path + + rawURL = u.String() + + logger.Debug("creating http request") + var req *http.Request + if *password != "" { + body := &NotePayload{ + Password: *password, + } + payload, err := json.Marshal(body) + if err != nil { + return internal.ReturnError(logger, "could not create note payload", err) + } + req, err = http.NewRequest("POST", rawURL, bytes.NewBuffer(payload)) + if err != nil { + return internal.ReturnError(logger, "could not create request", err) + } + } else { + req, err = http.NewRequest("GET", rawURL, nil) + if err != nil { + return internal.ReturnError(logger, "could not create request", err) + } + } + if *bearer != "" { req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", *bearer)) } - logger.Debug("parsing url", slog.Any("url", url)) + logger.Debug("executing http request", slog.Any("method", req.Method), slog.Any("url", rawURL)) r, err := http.DefaultClient.Do(req) if err != nil { return internal.ReturnError(logger, "could not retreive note", err) } + if r.StatusCode >= 300 { + return internal.ReturnError(logger, "could not retreive note", fmt.Errorf("status code %d", r.StatusCode)) + } + logger.Debug("decoding body") body, err := io.ReadAll(r.Body) if err != nil { diff --git a/src/go.mod b/src/go.mod index cb3f6c5..92236c0 100644 --- a/src/go.mod +++ b/src/go.mod @@ -1,15 +1,15 @@ module git.riou.xyz/jriou/coller -go 1.24 +go 1.24.0 -toolchain go1.24.6 +toolchain go1.24.7 require ( github.com/gorilla/mux v1.8.1 github.com/prometheus/client_golang v1.23.0 golang.design/x/clipboard v0.7.1 - golang.org/x/crypto v0.41.0 - golang.org/x/term v0.34.0 + golang.org/x/crypto v0.42.0 + golang.org/x/term v0.35.0 gorm.io/driver/postgres v1.6.0 gorm.io/driver/sqlite v1.6.0 gorm.io/gorm v1.30.0 @@ -33,8 +33,8 @@ require ( golang.org/x/exp/shiny v0.0.0-20250606033433-dcc06ee1d476 // indirect golang.org/x/image v0.28.0 // indirect golang.org/x/mobile v0.0.0-20250606033058-a2a15c67f36f // indirect - golang.org/x/sync v0.16.0 // indirect - golang.org/x/sys v0.35.0 // indirect - golang.org/x/text v0.28.0 // indirect + golang.org/x/sync v0.17.0 // indirect + golang.org/x/sys v0.36.0 // indirect + golang.org/x/text v0.29.0 // indirect google.golang.org/protobuf v1.36.6 // indirect ) diff --git a/src/go.sum b/src/go.sum index 1d47cb0..455f4bf 100644 --- a/src/go.sum +++ b/src/go.sum @@ -52,6 +52,8 @@ golang.design/x/clipboard v0.7.1 h1:OEG3CmcYRBNnRwpDp7+uWLiZi3hrMRJpE9JkkkYtz2c= golang.design/x/clipboard v0.7.1/go.mod h1:i5SiIqj0wLFw9P/1D7vfILFK0KHMk7ydE72HRrUIgkg= golang.org/x/crypto v0.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4= golang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc= +golang.org/x/crypto v0.42.0 h1:chiH31gIWm57EkTXpwnqf8qeuMUi0yekh6mT2AvFlqI= +golang.org/x/crypto v0.42.0/go.mod h1:4+rDnOTJhQCx2q7/j6rAN5XDw8kPjeaXEUR2eL94ix8= golang.org/x/exp/shiny v0.0.0-20250606033433-dcc06ee1d476 h1:Wdx0vgH5Wgsw+lF//LJKmWOJBLWX6nprsMqnf99rYDE= golang.org/x/exp/shiny v0.0.0-20250606033433-dcc06ee1d476/go.mod h1:ygj7T6vSGhhm/9yTpOQQNvuAUFziTH7RUiH74EoE2C8= golang.org/x/image v0.28.0 h1:gdem5JW1OLS4FbkWgLO+7ZeFzYtL3xClb97GaUzYMFE= @@ -60,12 +62,20 @@ golang.org/x/mobile v0.0.0-20250606033058-a2a15c67f36f h1:/n+PL2HlfqeSiDCuhdBbRN golang.org/x/mobile v0.0.0-20250606033058-a2a15c67f36f/go.mod h1:ESkJ836Z6LpG6mTVAhA48LpfW/8fNR0ifStlH2axyfg= golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw= golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug= +golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI= golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k= +golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/term v0.34.0 h1:O/2T7POpk0ZZ7MAzMeWFSg6S5IpWd/RXDlM9hgM3DR4= golang.org/x/term v0.34.0/go.mod h1:5jC53AEywhIVebHgPVeg0mj8OD3VO9OzclacVrqpaAw= +golang.org/x/term v0.35.0 h1:bZBVKBudEyhRcajGcNc3jIfWPqV4y/Kt2XcoigOWtDQ= +golang.org/x/term v0.35.0/go.mod h1:TPGtkTLesOwf2DE8CgVYiZinHAOuy5AYUYT1lENIZnA= golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng= golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU= +golang.org/x/text v0.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk= +golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4= google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/src/server/config.go b/src/server/config.go index ea5e5d2..ba9529c 100644 --- a/src/server/config.go +++ b/src/server/config.go @@ -14,6 +14,8 @@ type Config struct { EncryptionKeyLength int `json:"encryption_key_length"` AllowClientEncryptionKey bool `json:"allow_client_encryption_key"` AllowNoEncryption bool `json:"allow_no_encryption"` + EnablePasswordProtection bool `json:"enable_password_protection"` + EnableUploadFileButton bool `json:"enable_upload_file_button"` ExpirationInterval int `json:"expiration_interval"` ListenAddress string `json:"listen_address"` ListenPort int `json:"listen_port"` @@ -27,7 +29,6 @@ type Config struct { ObservationInterval int `json:"observation_internal"` Languages []string `json:"languages"` Language string `json:"language"` - EnableUploadFileButton bool `json:"enable_upload_file_button"` TLSCertFile string `json:"tls_cert_file"` TLSKeyFile string `json:"tls_key_file"` BootstrapDirectory string `json:"bootstrap_directory"` @@ -75,8 +76,9 @@ func NewConfig() *Config { "SQL", "YAML", }, - Language: "text", - EnableUploadFileButton: true, + Language: "text", + EnableUploadFileButton: true, + EnablePasswordProtection: true, } } diff --git a/src/server/db.go b/src/server/db.go index 58982c9..3c29251 100644 --- a/src/server/db.go +++ b/src/server/db.go @@ -8,6 +8,7 @@ import ( "time" "github.com/bwmarrin/snowflake" + "golang.org/x/crypto/bcrypt" "gorm.io/driver/postgres" "gorm.io/driver/sqlite" "gorm.io/gorm" @@ -122,7 +123,7 @@ func (d *Database) Get(id string) (*Note, error) { return nil, nil } -func (d *Database) Create(content []byte, encryptionKey string, encrypted bool, expiration int, deleteAfterRead bool, language string) (note *Note, err error) { +func (d *Database) Create(content []byte, password string, encryptionKey string, encrypted bool, expiration int, deleteAfterRead bool, language string) (note *Note, err error) { if expiration == 0 { expiration = d.expiration } @@ -148,6 +149,7 @@ func (d *Database) Create(content []byte, encryptionKey string, encrypted bool, DeleteAfterRead: deleteAfterRead, Language: language, } + if encryptionKey != "" { if err = internal.ValidateEncryptionKey(encryptionKey); err != nil { return nil, err @@ -158,12 +160,22 @@ func (d *Database) Create(content []byte, encryptionKey string, encrypted bool, } note.Encrypted = true } + + if password != "" { + hash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) + if err != nil { + return nil, err + } + note.PasswordHash = hash + } + trx := d.db.Create(note) defer trx.Commit() if trx.Error != nil { d.logger.Warn("could not create note", slog.Any("error", trx.Error)) return nil, trx.Error } + return note, nil } diff --git a/src/server/handlers_api.go b/src/server/handlers_api.go index 51852c3..833d8ea 100644 --- a/src/server/handlers_api.go +++ b/src/server/handlers_api.go @@ -7,6 +7,7 @@ import ( "net/http" "github.com/gorilla/mux" + "golang.org/x/crypto/bcrypt" "git.riou.xyz/jriou/coller/internal" ) @@ -25,6 +26,7 @@ type CreateNoteHandler struct { type CreateNotePayload struct { Content string `json:"content"` + Password string `json:"password"` EncryptionKey string `json:"encryption_key"` Encrypted bool `json:"encrypted"` Expiration int `json:"expiration"` @@ -66,7 +68,7 @@ func (h *CreateNoteHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { return } - note, err := h.db.Create(content, body.EncryptionKey, body.Encrypted, body.Expiration, body.DeleteAfterRead, body.Language) + note, err := h.db.Create(content, body.Password, body.EncryptionKey, body.Encrypted, body.Expiration, body.DeleteAfterRead, body.Language) if err != nil { WriteError(w, "could not create note", err) return @@ -92,6 +94,10 @@ func (h *GetNoteHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { WriteError(w, "could not get note", err) } else if note == nil { w.WriteHeader(http.StatusNotFound) + h.logger.Error("note does not exists", slog.Any("note_id", id)) + } else if note.PasswordHash != nil { + w.WriteHeader(http.StatusBadRequest) + h.logger.Error("note is password protected", slog.Any("note_id", note.ID)) } else { if note.Encrypted { w.Header().Set("Content-Type", "application/octet-stream") @@ -101,17 +107,32 @@ func (h *GetNoteHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { } } -type GetEncryptedNoteHandler struct { - logger *slog.Logger - db *Database +type GetProtectedNoteHandler struct { + logger *slog.Logger + db *Database + maxUploadSize int64 } -func (h *GetEncryptedNoteHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { +type GetProtectedNotePayload struct { + EncryptionKey string `json:"encryption_key"` + Password string `json:"password"` +} + +func (h *GetProtectedNoteHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "text/plain; charset=utf-8") vars := mux.Vars(r) id := vars["id"] - encryptionKey := vars["encryptionKey"] + + bodyReader := http.MaxBytesReader(w, r.Body, h.maxUploadSize) + defer r.Body.Close() + + var body GetProtectedNotePayload + err := json.NewDecoder(bodyReader).Decode(&body) + if err != nil { + WriteError(w, "could not decode payload to read protected note", err) + return + } note, err := h.db.Get(id) @@ -123,14 +144,22 @@ func (h *GetEncryptedNoteHandler) ServeHTTP(w http.ResponseWriter, r *http.Reque return } - if encryptionKey != "" && note.Encrypted { - note.Content, err = internal.Decrypt(note.Content, encryptionKey) + if body.EncryptionKey != "" && note.Encrypted { + note.Content, err = internal.Decrypt(note.Content, body.EncryptionKey) if err != nil { WriteError(w, "could not decrypt note", err) return } } + if body.Password != "" && (note.PasswordHash != nil || len(note.PasswordHash) > 0) { + err := bcrypt.CompareHashAndPassword(note.PasswordHash, []byte(body.Password)) + if err != nil { + WriteError(w, "could not validate password", err) + return + } + } + w.WriteHeader(http.StatusOK) fmt.Fprint(w, string(note.Content)) } diff --git a/src/server/handlers_web.go b/src/server/handlers_web.go index 813929a..c7a6087 100644 --- a/src/server/handlers_web.go +++ b/src/server/handlers_web.go @@ -12,6 +12,7 @@ import ( "strings" "github.com/gorilla/mux" + "golang.org/x/crypto/bcrypt" "git.riou.xyz/jriou/coller/internal" ) @@ -25,6 +26,7 @@ type PageData struct { Err error URL string Note *Note + EnablePasswordProtection bool EnableUploadFileButton bool AllowClientEncryptionKey bool AllowNoEncryption bool @@ -111,6 +113,7 @@ func (h *CreateNoteWithFormHandler) ServeHTTP(w http.ResponseWriter, r *http.Req } h.logger.Debug("checking inputs") + password := r.FormValue("password") noEncryption := r.FormValue("no-encryption") encryptionKey := r.FormValue("encryption-key") expiration := r.FormValue("expiration") @@ -141,7 +144,7 @@ func (h *CreateNoteWithFormHandler) ServeHTTP(w http.ResponseWriter, r *http.Req } h.logger.Debug("saving note to the database") - note, err := h.db.Create(content, encryptionKey, encryptionKey != "", expirationInt, deleteAfterRead != "", language) + note, err := h.db.Create(content, password, encryptionKey, encryptionKey != "", expirationInt, deleteAfterRead != "", language) if err != nil { h.PageData.Err = err h.Templates.ExecuteTemplate(w, templateName, h.PageData) @@ -160,19 +163,19 @@ func (h *CreateNoteWithFormHandler) ServeHTTP(w http.ResponseWriter, r *http.Req } h.logger.Debug("rendering page") - h.Templates.ExecuteTemplate(w, "create", h.PageData) + h.Templates.ExecuteTemplate(w, templateName, h.PageData) } -type GetWebNoteHandler struct { +type GetRawWebNoteHandler struct { Templates *template.Template PageData PageData logger *slog.Logger db *Database } -func (h *GetWebNoteHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { +func (h *GetRawWebNoteHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { h.PageData.Err = nil - templateName := "note" + templateName := "unprotectedNote" vars := mux.Vars(r) id := vars["id"] @@ -180,7 +183,7 @@ func (h *GetWebNoteHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { note, err := h.db.Get(id) if err != nil { - h.PageData.Err = fmt.Errorf("could not find note: %v", err) + h.PageData.Err = fmt.Errorf("could not get raw note") h.Templates.ExecuteTemplate(w, templateName, h.PageData) return } @@ -199,13 +202,88 @@ func (h *GetWebNoteHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { h.PageData.Note = note - h.logger.Debug("rendering note web page") - h.Templates.ExecuteTemplate(w, "note", h.PageData) + h.logger.Debug("rendering page") + + if len(note.PasswordHash) > 0 { + h.Templates.ExecuteTemplate(w, templateName, h.PageData) + return + } + + w.Header().Set("Content-Type", "text/plain; charset=utf-8") + w.WriteHeader(http.StatusOK) + fmt.Fprint(w, string(note.Content)) } -func (h *GetEncryptedWebNoteHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { +type GetProtectedRawWebNoteHandler struct { + Templates *template.Template + PageData PageData + logger *slog.Logger + db *Database + maxUploadSize int64 +} + +func (h *GetProtectedRawWebNoteHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { h.PageData.Err = nil - templateName := "note" + templateName := "protectedNote" + + vars := mux.Vars(r) + id := vars["id"] + + h.logger.Debug("parsing multipart form") + err := r.ParseMultipartForm(h.maxUploadSize) + if err != nil { + h.PageData.Err = err + h.Templates.ExecuteTemplate(w, templateName, h.PageData) + return + } + + password := r.FormValue("password") + + note, err := h.db.Get(id) + + if err != nil { + h.PageData.Err = fmt.Errorf("could not get raw note") + h.Templates.ExecuteTemplate(w, templateName, h.PageData) + return + } + + if note == nil { + h.PageData.Err = fmt.Errorf("note doesn't exist or has been deleted") + h.Templates.ExecuteTemplate(w, templateName, h.PageData) + return + } + + if note.Encrypted { + h.PageData.Err = fmt.Errorf("note is encrypted") + h.Templates.ExecuteTemplate(w, templateName, h.PageData) + return + } + + if err := bcrypt.CompareHashAndPassword(note.PasswordHash, []byte(password)); err != nil { + h.PageData.Err = fmt.Errorf("invalid password") + h.Templates.ExecuteTemplate(w, templateName, h.PageData) + return + } + + h.PageData.Note = note + + h.logger.Debug("rendering page") + + w.Header().Set("Content-Type", "text/plain; charset=utf-8") + w.WriteHeader(http.StatusOK) + fmt.Fprint(w, string(note.Content)) +} + +type GetEncryptedRawWebNoteHandler struct { + Templates *template.Template + PageData PageData + logger *slog.Logger + db *Database +} + +func (h *GetEncryptedRawWebNoteHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { + h.PageData.Err = nil + templateName := "unprotectedNote" vars := mux.Vars(r) id := vars["id"] @@ -214,7 +292,7 @@ func (h *GetEncryptedWebNoteHandler) ServeHTTP(w http.ResponseWriter, r *http.Re note, err := h.db.Get(id) if err != nil { - h.PageData.Err = fmt.Errorf("could not find note: %v", err) + h.PageData.Err = fmt.Errorf("could not get raw note") h.Templates.ExecuteTemplate(w, templateName, h.PageData) return } @@ -228,7 +306,220 @@ func (h *GetEncryptedWebNoteHandler) ServeHTTP(w http.ResponseWriter, r *http.Re if encryptionKey != "" && note.Encrypted { note.Content, err = internal.Decrypt(note.Content, encryptionKey) if err != nil { - h.PageData.Err = fmt.Errorf("could not decrypt note: %v", err) + h.PageData.Err = fmt.Errorf("could not decrypt note") + h.Templates.ExecuteTemplate(w, templateName, h.PageData) + return + } + } + + h.PageData.Note = note + + h.logger.Debug("rendering page") + + if len(note.PasswordHash) > 0 { + h.Templates.ExecuteTemplate(w, templateName, h.PageData) + return + } + + w.Header().Set("Content-Type", "text/plain; charset=utf-8") + w.WriteHeader(http.StatusOK) + fmt.Fprint(w, string(note.Content)) +} + +type GetProtectedEncryptedRawWebNoteHandler struct { + Templates *template.Template + PageData PageData + logger *slog.Logger + db *Database + maxUploadSize int64 +} + +func (h *GetProtectedEncryptedRawWebNoteHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { + h.PageData.Err = nil + templateName := "protectedNote" + + vars := mux.Vars(r) + id := vars["id"] + encryptionKey := vars["encryptionKey"] + + h.logger.Debug("parsing multipart form") + err := r.ParseMultipartForm(h.maxUploadSize) + if err != nil { + h.PageData.Err = err + h.Templates.ExecuteTemplate(w, templateName, h.PageData) + return + } + + password := r.FormValue("password") + + note, err := h.db.Get(id) + + if err != nil { + h.PageData.Err = fmt.Errorf("could not get raw note") + h.Templates.ExecuteTemplate(w, templateName, h.PageData) + return + } + + if note == nil { + h.PageData.Err = fmt.Errorf("note doesn't exist or has been deleted") + h.Templates.ExecuteTemplate(w, templateName, h.PageData) + return + } + + if err := bcrypt.CompareHashAndPassword(note.PasswordHash, []byte(password)); err != nil { + h.PageData.Err = fmt.Errorf("invalid password") + h.Templates.ExecuteTemplate(w, templateName, h.PageData) + return + } + + if encryptionKey != "" && note.Encrypted { + note.Content, err = internal.Decrypt(note.Content, encryptionKey) + if err != nil { + h.PageData.Err = fmt.Errorf("could not decrypt note") + h.Templates.ExecuteTemplate(w, templateName, h.PageData) + return + } + } + + h.PageData.Note = note + + h.logger.Debug("rendering page") + + w.Header().Set("Content-Type", "text/plain; charset=utf-8") + w.WriteHeader(http.StatusOK) + fmt.Fprint(w, string(note.Content)) +} + +type GetWebNoteHandler struct { + Templates *template.Template + PageData PageData + logger *slog.Logger + db *Database +} + +func (h *GetWebNoteHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { + h.PageData.Err = nil + templateName := "unprotectedNote" + + vars := mux.Vars(r) + id := vars["id"] + + note, err := h.db.Get(id) + + if err != nil { + h.PageData.Err = err + h.Templates.ExecuteTemplate(w, templateName, h.PageData) + return + } + + if note == nil { + h.PageData.Err = fmt.Errorf("note doesn't exist or has been deleted") + h.Templates.ExecuteTemplate(w, templateName, h.PageData) + return + } + + if note.Encrypted { + h.PageData.Err = fmt.Errorf("note is encrypted") + h.Templates.ExecuteTemplate(w, templateName, h.PageData) + return + } + + h.PageData.Note = note + + h.logger.Debug("rendering page") + h.Templates.ExecuteTemplate(w, templateName, h.PageData) +} + +type GetProtectedWebNoteHandler struct { + Templates *template.Template + PageData PageData + logger *slog.Logger + db *Database + maxUploadSize int64 +} + +func (h *GetProtectedWebNoteHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { + h.PageData.Err = nil + templateName := "protectedNote" + + vars := mux.Vars(r) + id := vars["id"] + + h.logger.Debug("parsing multipart form") + err := r.ParseMultipartForm(h.maxUploadSize) + if err != nil { + h.PageData.Err = err + h.Templates.ExecuteTemplate(w, templateName, h.PageData) + return + } + + password := r.FormValue("password") + + note, err := h.db.Get(id) + + if err != nil { + h.PageData.Err = err + h.Templates.ExecuteTemplate(w, templateName, h.PageData) + return + } + + if note == nil { + h.PageData.Err = fmt.Errorf("note doesn't exist or has been deleted") + h.Templates.ExecuteTemplate(w, templateName, h.PageData) + return + } + + if note.Encrypted { + h.PageData.Err = fmt.Errorf("note is encrypted") + h.Templates.ExecuteTemplate(w, templateName, h.PageData) + return + } + + if err := bcrypt.CompareHashAndPassword(note.PasswordHash, []byte(password)); err != nil { + h.PageData.Err = fmt.Errorf("invalid password") + h.Templates.ExecuteTemplate(w, templateName, h.PageData) + return + } + + h.PageData.Note = note + + h.logger.Debug("rendering page") + h.Templates.ExecuteTemplate(w, templateName, h.PageData) +} + +type GetEncryptedWebNoteHandler struct { + Templates *template.Template + PageData PageData + logger *slog.Logger + db *Database +} + +func (h *GetEncryptedWebNoteHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { + h.PageData.Err = nil + templateName := "unprotectedNote" + + vars := mux.Vars(r) + id := vars["id"] + encryptionKey := vars["encryptionKey"] + + note, err := h.db.Get(id) + + if err != nil { + h.PageData.Err = err + h.Templates.ExecuteTemplate(w, templateName, h.PageData) + return + } + + if note == nil { + h.PageData.Err = fmt.Errorf("note doesn't exist or has been deleted") + h.Templates.ExecuteTemplate(w, templateName, h.PageData) + return + } + + if encryptionKey != "" && note.Encrypted { + note.Content, err = internal.Decrypt(note.Content, encryptionKey) + if err != nil { + h.PageData.Err = fmt.Errorf("could not decrypt note") h.Templates.ExecuteTemplate(w, templateName, h.PageData) return } @@ -237,7 +528,74 @@ func (h *GetEncryptedWebNoteHandler) ServeHTTP(w http.ResponseWriter, r *http.Re h.PageData.Note = note h.logger.Debug("rendering encrypted note web page") - h.Templates.ExecuteTemplate(w, "note", h.PageData) + h.Templates.ExecuteTemplate(w, templateName, h.PageData) +} + +type GetProtectedEncryptedWebNoteHandler struct { + Templates *template.Template + PageData PageData + logger *slog.Logger + db *Database + maxUploadSize int64 +} + +func (h *GetProtectedEncryptedWebNoteHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { + h.PageData.Err = nil + templateName := "protectedNote" + + vars := mux.Vars(r) + id := vars["id"] + encryptionKey := vars["encryptionKey"] + + h.logger.Debug("parsing multipart form") + err := r.ParseMultipartForm(h.maxUploadSize) + if err != nil { + h.PageData.Err = err + h.Templates.ExecuteTemplate(w, templateName, h.PageData) + return + } + + password := r.FormValue("password") + + note, err := h.db.Get(id) + + if err != nil { + h.PageData.Err = err + h.Templates.ExecuteTemplate(w, templateName, h.PageData) + return + } + + if note == nil { + h.PageData.Err = fmt.Errorf("note doesn't exist or has been deleted") + h.Templates.ExecuteTemplate(w, templateName, h.PageData) + return + } + + if err := bcrypt.CompareHashAndPassword(note.PasswordHash, []byte(password)); err != nil { + h.PageData.Err = fmt.Errorf("invalid password") + h.Templates.ExecuteTemplate(w, templateName, h.PageData) + return + } + + if encryptionKey != "" && note.Encrypted { + note.Content, err = internal.Decrypt(note.Content, encryptionKey) + if err != nil { + h.PageData.Err = fmt.Errorf("could not decrypt note") + h.Templates.ExecuteTemplate(w, templateName, h.PageData) + return + } + } + + h.PageData.Note = note + + h.logger.Debug("rendering encrypted note web page") + h.Templates.ExecuteTemplate(w, templateName, h.PageData) +} + +type ClientsHandler struct { + Templates *template.Template + PageData PageData + logger *slog.Logger } func (h *ClientsHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { diff --git a/src/server/note.go b/src/server/note.go index f6bf156..9464cf9 100644 --- a/src/server/note.go +++ b/src/server/note.go @@ -10,6 +10,7 @@ type Note struct { ID string `json:"id" gorm:"primaryKey"` Content []byte `json:"content" gorm:"not null"` Encrypted bool `json:"encrypted"` + PasswordHash []byte `json:"password_hash"` ExpiresAt time.Time `json:"expires_at" gorm:"index"` DeleteAfterRead bool `json:"delete_after_read"` Language string `json:"language"` diff --git a/src/server/server.go b/src/server/server.go index 87b8aec..431a620 100644 --- a/src/server/server.go +++ b/src/server/server.go @@ -71,20 +71,7 @@ func WriteError(w http.ResponseWriter, message string, err error) { }.ToJSON()) } -type GetEncryptedWebNoteHandler struct { - Templates *template.Template - PageData PageData - logger *slog.Logger - db *Database -} - -type ClientsHandler struct { - Templates *template.Template - PageData PageData - logger *slog.Logger -} - -//go:embed templates/* +//go:embed templates/*.html var templatesFS embed.FS func (s *Server) Start() error { @@ -108,17 +95,18 @@ func (s *Server) Start() error { } r.Path("/api/note").Handler(createNoteHandler).Methods("POST") - getEncryptedNoteHandler := &GetEncryptedNoteHandler{ - logger: s.logger, - db: s.db, + getProtectedNoteHandler := &GetProtectedNoteHandler{ + logger: s.logger, + db: s.db, + maxUploadSize: s.config.MaxUploadSize, } - r.Path("/{id:[a-zA-Z0-9]+}/{encryptionKey:[a-zA-Z0-9]+}").Handler(getEncryptedNoteHandler).Methods("GET") + r.Path("/api/note/{id:[a-zA-Z0-9]+}").Handler(getProtectedNoteHandler).Methods("POST") getNoteHandler := &GetNoteHandler{ logger: s.logger, db: s.db, } - r.Path("/{id:[a-zA-Z0-9]+}").Handler(getNoteHandler).Methods("GET") + r.Path("/api/note/{id:[a-zA-Z0-9]+}").Handler(getNoteHandler).Methods("GET") // Web pages funcs := template.FuncMap{ @@ -134,6 +122,7 @@ func (s *Server) Start() error { Languages: s.config.Languages, BootstrapDirectory: s.config.BootstrapDirectory, EnableUploadFileButton: s.config.EnableUploadFileButton, + EnablePasswordProtection: s.config.EnablePasswordProtection, AllowClientEncryptionKey: s.config.AllowClientEncryptionKey, AllowNoEncryption: s.config.AllowNoEncryption, } @@ -177,6 +166,48 @@ func (s *Server) Start() error { } r.Path("/{id:[a-zA-Z0-9]+}/{encryptionKey:[a-zA-Z0-9]+}.html").Handler(encryptedWebNoteHandler).Methods("GET") + protectedEncryptedWebNoteHandler := &GetProtectedEncryptedWebNoteHandler{ + Templates: templates, + PageData: p, + logger: s.logger, + db: s.db, + maxUploadSize: s.config.MaxUploadSize, + } + r.Path("/{id:[a-zA-Z0-9]+}/{encryptionKey:[a-zA-Z0-9]+}.html").Handler(protectedEncryptedWebNoteHandler).Methods("POST") + + encryptedRawWebNoteHandler := &GetEncryptedRawWebNoteHandler{ + Templates: templates, + PageData: p, + logger: s.logger, + db: s.db, + } + r.Path("/{id:[a-zA-Z0-9]+}/{encryptionKey:[a-zA-Z0-9]+}/raw").Handler(encryptedRawWebNoteHandler).Methods("GET") + + protectedEncryptedRawWebNoteHandler := &GetProtectedEncryptedRawWebNoteHandler{ + Templates: templates, + PageData: p, + logger: s.logger, + db: s.db, + } + r.Path("/{id:[a-zA-Z0-9]+}/{encryptionKey:[a-zA-Z0-9]+}/raw").Handler(protectedEncryptedRawWebNoteHandler).Methods("POST") + + rawWebNoteHandler := &GetRawWebNoteHandler{ + Templates: templates, + PageData: p, + logger: s.logger, + db: s.db, + } + r.Path("/{id:[a-zA-Z0-9]+}/raw").Handler(rawWebNoteHandler).Methods("GET") + + protectedRawWebNoteHandler := &GetProtectedRawWebNoteHandler{ + Templates: templates, + PageData: p, + logger: s.logger, + db: s.db, + maxUploadSize: s.config.MaxUploadSize, + } + r.Path("/{id:[a-zA-Z0-9]+}/raw").Handler(protectedRawWebNoteHandler).Methods("POST") + webNoteHandler := &GetWebNoteHandler{ Templates: templates, PageData: p, @@ -185,6 +216,15 @@ func (s *Server) Start() error { } r.Path("/{id:[a-zA-Z0-9]+}.html").Handler(webNoteHandler).Methods("GET") + protectedWebNoteHandler := &GetProtectedWebNoteHandler{ + Templates: templates, + PageData: p, + logger: s.logger, + db: s.db, + maxUploadSize: s.config.MaxUploadSize, + } + r.Path("/{id:[a-zA-Z0-9]+}.html").Handler(protectedWebNoteHandler).Methods("POST") + if s.config.BootstrapDirectory != "" { r.PathPrefix("/static/bootstrap/").Handler(http.StripPrefix("/static/bootstrap/", http.FileServer(http.Dir(s.config.BootstrapDirectory)))) } diff --git a/src/server/templates/error.html b/src/server/templates/error.html new file mode 100644 index 0000000..6a48b44 --- /dev/null +++ b/src/server/templates/error.html @@ -0,0 +1,8 @@ +{{define "error"}} +
+ +
+{{end}} \ No newline at end of file diff --git a/src/server/templates/index.html b/src/server/templates/index.html index a6363c1..b4b3c00 100644 --- a/src/server/templates/index.html +++ b/src/server/templates/index.html @@ -13,6 +13,14 @@
+ {{if .EnablePasswordProtection}} +
+ +
+
+ +
+ {{end}} {{if .AllowClientEncryptionKey}}
@@ -25,9 +33,9 @@ {{end}} {{if .AllowNoEncryption}}
- - + +
{{end}}
diff --git a/src/server/templates/note.html b/src/server/templates/note.html index 09f7b6a..344f38f 100644 --- a/src/server/templates/note.html +++ b/src/server/templates/note.html @@ -1,69 +1,51 @@ {{define "note"}} - - - -{{block "head" .}}{{end}} - - - {{block "header" .}}{{end}} - - {{if ne .Err nil}} -
-
@@ -69,11 +70,15 @@
+ {{if .DisableEditor}} + + {{else}}
+ style="min-height: 300px; resize: vertical; overflow: auto;">
+ {{end}}
@@ -82,38 +87,40 @@
- + {{if eq false .DisableEditor}} + + {{end}} {{block "footer" .}}{{end}} diff --git a/src/server/templates/note.html b/src/server/templates/note.html index 344f38f..977705c 100644 --- a/src/server/templates/note.html +++ b/src/server/templates/note.html @@ -20,32 +20,44 @@ {{end}}
+ {{if .DisableEditor}}
-
+
+{{string .Note.Content}}
+            
+
+ {{else}} +
+
- + + {{end}}
{{end}} {{end}} \ No newline at end of file From 1fcde736a8272aedb56f2afff7a3eff665dbb86e Mon Sep 17 00:00:00 2001 From: Julien Riou Date: Sat, 27 Sep 2025 10:05:58 +0200 Subject: [PATCH 22/39] fix: convert id to int64 Snowflake identifiers are integers, not strings. BREAKING CHANGE: notes that are not using snowflake identifiers will not be compatible anymore. Signed-off-by: Julien Riou --- src/cmd/coller/main.go | 4 ++-- src/cmd/collerd/README.md | 2 +- src/server/db.go | 6 +++--- src/server/handlers_api.go | 2 +- src/server/handlers_web.go | 2 +- src/server/note.go | 2 +- 6 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/cmd/coller/main.go b/src/cmd/coller/main.go index c300e0e..b710214 100644 --- a/src/cmd/coller/main.go +++ b/src/cmd/coller/main.go @@ -36,7 +36,7 @@ type NotePayload struct { } type NoteResponse struct { - ID string `json:"id"` + ID int64 `json:"id"` Message string `json:"message,omitempty"` Error string `json:"error,omitempty"` } @@ -246,7 +246,7 @@ func handleMain() int { logger.Debug("finding note location") var location string - noteURL := *url + "/" + jsonBody.ID + noteURL := *url + "/" + fmt.Sprintf("%d", jsonBody.ID) if *copier { location = "copier" if *encryptionKey != "" { diff --git a/src/cmd/collerd/README.md b/src/cmd/collerd/README.md index 1f62bab..e4defd7 100644 --- a/src/cmd/collerd/README.md +++ b/src/cmd/collerd/README.md @@ -64,7 +64,7 @@ Body (JSON): * **language** (string): language of the note (must be supported by the server) Response (JSON): -* **id** (string): ID of the note +* **id** (int): ID of the note ### GET /api/note/\/\ diff --git a/src/server/db.go b/src/server/db.go index 3c29251..f8c5e38 100644 --- a/src/server/db.go +++ b/src/server/db.go @@ -112,7 +112,7 @@ func (d *Database) Get(id string) (*Note, error) { d.logger.Warn("could not find note", slog.Any("error", trx.Error)) return nil, trx.Error } - if note.ID != "" { + if note.ID != 0 { if note.DeleteAfterRead { if err := d.Delete(note.ID); err != nil { return nil, err @@ -142,7 +142,7 @@ func (d *Database) Create(content []byte, password string, encryptionKey string, } note = &Note{ - ID: d.node.Generate().String(), + ID: d.node.Generate().Int64(), Content: content, ExpiresAt: time.Now().Add(time.Duration(expiration) * time.Second), Encrypted: encrypted, @@ -179,7 +179,7 @@ func (d *Database) Create(content []byte, password string, encryptionKey string, return note, nil } -func (d *Database) Delete(id string) error { +func (d *Database) Delete(id int64) error { trx := d.db.Where("id = ?", id).Delete(&Note{}) defer trx.Commit() if trx.Error != nil { diff --git a/src/server/handlers_api.go b/src/server/handlers_api.go index 833d8ea..4f03c5a 100644 --- a/src/server/handlers_api.go +++ b/src/server/handlers_api.go @@ -35,7 +35,7 @@ type CreateNotePayload struct { } type CreateNoteResponse struct { - ID string `json:"id"` + ID int64 `json:"id"` } func (h *CreateNoteHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { diff --git a/src/server/handlers_web.go b/src/server/handlers_web.go index aa91e59..91bfe9a 100644 --- a/src/server/handlers_web.go +++ b/src/server/handlers_web.go @@ -160,7 +160,7 @@ func (h *CreateNoteWithFormHandler) ServeHTTP(w http.ResponseWriter, r *http.Req scheme = "https://" } - h.PageData.URL = fmt.Sprintf("%s%s/%s", scheme, r.Host, note.ID) + h.PageData.URL = fmt.Sprintf("%s%s/%d", scheme, r.Host, note.ID) if encryptionKey != "" { h.PageData.URL += "/" + encryptionKey } diff --git a/src/server/note.go b/src/server/note.go index 9464cf9..6f20e1a 100644 --- a/src/server/note.go +++ b/src/server/note.go @@ -7,7 +7,7 @@ import ( ) type Note struct { - ID string `json:"id" gorm:"primaryKey"` + ID int64 `json:"id" gorm:"primaryKey"` Content []byte `json:"content" gorm:"not null"` Encrypted bool `json:"encrypted"` PasswordHash []byte `json:"password_hash"` From ee7b5f0c6edf4f7a512aec15e413f465b4ff4f99 Mon Sep 17 00:00:00 2001 From: Julien Riou Date: Wed, 1 Oct 2025 12:40:46 +0200 Subject: [PATCH 23/39] feat: Pass encryption key in URL fragment - Remove encryptionKey from URL - Use POST method to pass both password and encryption key - Parse URL fragment to extract the encryption key from the web (using javascript) and from the CLI Fixes #36. Signed-off-by: Julien Riou --- src/cmd/coller/main.go | 14 +- src/cmd/copier/main.go | 5 + src/server/handlers_web.go | 273 +++------------------- src/server/server.go | 67 ++---- src/server/templates/create.html | 16 +- src/server/templates/note.html | 2 +- src/server/templates/protectedNote.html | 2 +- src/server/templates/unprotectedNote.html | 40 +++- 8 files changed, 103 insertions(+), 316 deletions(-) diff --git a/src/cmd/coller/main.go b/src/cmd/coller/main.go index b710214..ce138c9 100644 --- a/src/cmd/coller/main.go +++ b/src/cmd/coller/main.go @@ -249,22 +249,20 @@ func handleMain() int { noteURL := *url + "/" + fmt.Sprintf("%d", jsonBody.ID) if *copier { location = "copier" - if *encryptionKey != "" { - location += " -encryption-key " + *encryptionKey - } if *password != "" { location += " -password '" + *password + "'" } location += " " + noteURL + if *encryptionKey != "" { + location += "#" + *encryptionKey + } } else { location = noteURL - if *encryptionKey != "" { - location += "/" + *encryptionKey - } if *html { location += ".html" - } else { - location += "/raw" + } + if *encryptionKey != "" { + location += "#" + *encryptionKey } } diff --git a/src/cmd/copier/main.go b/src/cmd/copier/main.go index 867f852..0f9c062 100644 --- a/src/cmd/copier/main.go +++ b/src/cmd/copier/main.go @@ -96,6 +96,11 @@ func handleMain() int { } u.Path = "api/note" + u.Path + if u.Fragment != "" { + *encryptionKey = u.Fragment + u.Fragment = "" + } + rawURL = u.String() logger.Debug("creating http request") diff --git a/src/server/handlers_web.go b/src/server/handlers_web.go index 91bfe9a..55720c9 100644 --- a/src/server/handlers_web.go +++ b/src/server/handlers_web.go @@ -160,9 +160,9 @@ func (h *CreateNoteWithFormHandler) ServeHTTP(w http.ResponseWriter, r *http.Req scheme = "https://" } - h.PageData.URL = fmt.Sprintf("%s%s/%d", scheme, r.Host, note.ID) + h.PageData.URL = fmt.Sprintf("%s%s/%d.html", scheme, r.Host, note.ID) if encryptionKey != "" { - h.PageData.URL += "/" + encryptionKey + h.PageData.URL += "#" + encryptionKey } h.logger.Debug("rendering page") @@ -197,17 +197,11 @@ func (h *GetRawWebNoteHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) return } - if note.Encrypted { - h.PageData.Err = fmt.Errorf("note is encrypted") - h.Templates.ExecuteTemplate(w, templateName, h.PageData) - return - } - h.PageData.Note = note h.logger.Debug("rendering page") - if len(note.PasswordHash) > 0 { + if note.Encrypted || len(note.PasswordHash) > 0 { h.Templates.ExecuteTemplate(w, templateName, h.PageData) return } @@ -241,11 +235,12 @@ func (h *GetProtectedRawWebNoteHandler) ServeHTTP(w http.ResponseWriter, r *http } password := r.FormValue("password") + encryptionKey := r.FormValue("encryption-key") note, err := h.db.Get(id) if err != nil { - h.PageData.Err = fmt.Errorf("could not get raw note") + h.PageData.Err = fmt.Errorf("could not find note") h.Templates.ExecuteTemplate(w, templateName, h.PageData) return } @@ -257,56 +252,11 @@ func (h *GetProtectedRawWebNoteHandler) ServeHTTP(w http.ResponseWriter, r *http } if note.Encrypted { - h.PageData.Err = fmt.Errorf("note is encrypted") - h.Templates.ExecuteTemplate(w, templateName, h.PageData) - return - } - - if err := bcrypt.CompareHashAndPassword(note.PasswordHash, []byte(password)); err != nil { - h.PageData.Err = fmt.Errorf("invalid password") - h.Templates.ExecuteTemplate(w, templateName, h.PageData) - return - } - - h.PageData.Note = note - - h.logger.Debug("rendering page") - - w.Header().Set("Content-Type", "text/plain; charset=utf-8") - w.WriteHeader(http.StatusOK) - fmt.Fprint(w, string(note.Content)) -} - -type GetEncryptedRawWebNoteHandler struct { - Templates *template.Template - PageData PageData - logger *slog.Logger - db *Database -} - -func (h *GetEncryptedRawWebNoteHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { - h.PageData.Err = nil - templateName := "unprotectedNote" - - vars := mux.Vars(r) - id := vars["id"] - encryptionKey := vars["encryptionKey"] - - note, err := h.db.Get(id) - - if err != nil { - h.PageData.Err = fmt.Errorf("could not get raw note") - h.Templates.ExecuteTemplate(w, templateName, h.PageData) - return - } - - if note == nil { - h.PageData.Err = fmt.Errorf("note doesn't exist or has been deleted") - h.Templates.ExecuteTemplate(w, templateName, h.PageData) - return - } - - if encryptionKey != "" && note.Encrypted { + if encryptionKey == "" { + h.PageData.Err = fmt.Errorf("encryption key not found") + h.Templates.ExecuteTemplate(w, templateName, h.PageData) + return + } note.Content, err = internal.Decrypt(note.Content, encryptionKey) if err != nil { h.PageData.Err = fmt.Errorf("could not decrypt note") @@ -315,70 +265,9 @@ func (h *GetEncryptedRawWebNoteHandler) ServeHTTP(w http.ResponseWriter, r *http } } - h.PageData.Note = note - - h.logger.Debug("rendering page") - if len(note.PasswordHash) > 0 { - h.Templates.ExecuteTemplate(w, templateName, h.PageData) - return - } - - w.Header().Set("Content-Type", "text/plain; charset=utf-8") - w.WriteHeader(http.StatusOK) - fmt.Fprint(w, string(note.Content)) -} - -type GetProtectedEncryptedRawWebNoteHandler struct { - Templates *template.Template - PageData PageData - logger *slog.Logger - db *Database - maxUploadSize int64 -} - -func (h *GetProtectedEncryptedRawWebNoteHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { - h.PageData.Err = nil - templateName := "protectedNote" - - vars := mux.Vars(r) - id := vars["id"] - encryptionKey := vars["encryptionKey"] - - h.logger.Debug("parsing multipart form") - err := r.ParseMultipartForm(h.maxUploadSize) - if err != nil { - h.PageData.Err = err - h.Templates.ExecuteTemplate(w, templateName, h.PageData) - return - } - - password := r.FormValue("password") - - note, err := h.db.Get(id) - - if err != nil { - h.PageData.Err = fmt.Errorf("could not get raw note") - h.Templates.ExecuteTemplate(w, templateName, h.PageData) - return - } - - if note == nil { - h.PageData.Err = fmt.Errorf("note doesn't exist or has been deleted") - h.Templates.ExecuteTemplate(w, templateName, h.PageData) - return - } - - if err := bcrypt.CompareHashAndPassword(note.PasswordHash, []byte(password)); err != nil { - h.PageData.Err = fmt.Errorf("invalid password") - h.Templates.ExecuteTemplate(w, templateName, h.PageData) - return - } - - if encryptionKey != "" && note.Encrypted { - note.Content, err = internal.Decrypt(note.Content, encryptionKey) - if err != nil { - h.PageData.Err = fmt.Errorf("could not decrypt note") + if err := bcrypt.CompareHashAndPassword(note.PasswordHash, []byte(password)); err != nil { + h.PageData.Err = fmt.Errorf("invalid password") h.Templates.ExecuteTemplate(w, templateName, h.PageData) return } @@ -421,12 +310,6 @@ func (h *GetWebNoteHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { return } - if note.Encrypted { - h.PageData.Err = fmt.Errorf("note is encrypted") - h.Templates.ExecuteTemplate(w, templateName, h.PageData) - return - } - h.PageData.Note = note h.logger.Debug("rendering page") @@ -457,11 +340,12 @@ func (h *GetProtectedWebNoteHandler) ServeHTTP(w http.ResponseWriter, r *http.Re } password := r.FormValue("password") + encryptionKey := r.FormValue("encryption-key") note, err := h.db.Get(id) if err != nil { - h.PageData.Err = err + h.PageData.Err = fmt.Errorf("could not find note") h.Templates.ExecuteTemplate(w, templateName, h.PageData) return } @@ -473,15 +357,25 @@ func (h *GetProtectedWebNoteHandler) ServeHTTP(w http.ResponseWriter, r *http.Re } if note.Encrypted { - h.PageData.Err = fmt.Errorf("note is encrypted") - h.Templates.ExecuteTemplate(w, templateName, h.PageData) - return + if encryptionKey == "" { + h.PageData.Err = fmt.Errorf("encryption key not found") + h.Templates.ExecuteTemplate(w, templateName, h.PageData) + return + } + note.Content, err = internal.Decrypt(note.Content, encryptionKey) + if err != nil { + h.PageData.Err = fmt.Errorf("could not decrypt note") + h.Templates.ExecuteTemplate(w, templateName, h.PageData) + return + } } - if err := bcrypt.CompareHashAndPassword(note.PasswordHash, []byte(password)); err != nil { - h.PageData.Err = fmt.Errorf("invalid password") - h.Templates.ExecuteTemplate(w, templateName, h.PageData) - return + if len(note.PasswordHash) > 0 { + if err := bcrypt.CompareHashAndPassword(note.PasswordHash, []byte(password)); err != nil { + h.PageData.Err = fmt.Errorf("invalid password") + h.Templates.ExecuteTemplate(w, templateName, h.PageData) + return + } } h.PageData.Note = note @@ -490,111 +384,6 @@ func (h *GetProtectedWebNoteHandler) ServeHTTP(w http.ResponseWriter, r *http.Re h.Templates.ExecuteTemplate(w, templateName, h.PageData) } -type GetEncryptedWebNoteHandler struct { - Templates *template.Template - PageData PageData - logger *slog.Logger - db *Database -} - -func (h *GetEncryptedWebNoteHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { - h.PageData.Err = nil - templateName := "unprotectedNote" - - vars := mux.Vars(r) - id := vars["id"] - encryptionKey := vars["encryptionKey"] - - note, err := h.db.Get(id) - - if err != nil { - h.PageData.Err = err - h.Templates.ExecuteTemplate(w, templateName, h.PageData) - return - } - - if note == nil { - h.PageData.Err = fmt.Errorf("note doesn't exist or has been deleted") - h.Templates.ExecuteTemplate(w, templateName, h.PageData) - return - } - - if encryptionKey != "" && note.Encrypted { - note.Content, err = internal.Decrypt(note.Content, encryptionKey) - if err != nil { - h.PageData.Err = fmt.Errorf("could not decrypt note") - h.Templates.ExecuteTemplate(w, templateName, h.PageData) - return - } - } - - h.PageData.Note = note - - h.logger.Debug("rendering encrypted note web page") - h.Templates.ExecuteTemplate(w, templateName, h.PageData) -} - -type GetProtectedEncryptedWebNoteHandler struct { - Templates *template.Template - PageData PageData - logger *slog.Logger - db *Database - maxUploadSize int64 -} - -func (h *GetProtectedEncryptedWebNoteHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { - h.PageData.Err = nil - templateName := "protectedNote" - - vars := mux.Vars(r) - id := vars["id"] - encryptionKey := vars["encryptionKey"] - - h.logger.Debug("parsing multipart form") - err := r.ParseMultipartForm(h.maxUploadSize) - if err != nil { - h.PageData.Err = err - h.Templates.ExecuteTemplate(w, templateName, h.PageData) - return - } - - password := r.FormValue("password") - - note, err := h.db.Get(id) - - if err != nil { - h.PageData.Err = err - h.Templates.ExecuteTemplate(w, templateName, h.PageData) - return - } - - if note == nil { - h.PageData.Err = fmt.Errorf("note doesn't exist or has been deleted") - h.Templates.ExecuteTemplate(w, templateName, h.PageData) - return - } - - if err := bcrypt.CompareHashAndPassword(note.PasswordHash, []byte(password)); err != nil { - h.PageData.Err = fmt.Errorf("invalid password") - h.Templates.ExecuteTemplate(w, templateName, h.PageData) - return - } - - if encryptionKey != "" && note.Encrypted { - note.Content, err = internal.Decrypt(note.Content, encryptionKey) - if err != nil { - h.PageData.Err = fmt.Errorf("could not decrypt note") - h.Templates.ExecuteTemplate(w, templateName, h.PageData) - return - } - } - - h.PageData.Note = note - - h.logger.Debug("rendering encrypted note web page") - h.Templates.ExecuteTemplate(w, templateName, h.PageData) -} - type ClientsHandler struct { Templates *template.Template PageData PageData diff --git a/src/server/server.go b/src/server/server.go index 025ffdf..dd6b6a0 100644 --- a/src/server/server.go +++ b/src/server/server.go @@ -161,56 +161,6 @@ func (s *Server) Start() error { } r.Path("/clients/{os:[a-z]+}-{arch:[a-z0-9]+}/{clientName:[a-z]+}").Handler(clientHandler).Methods("GET") - encryptedWebNoteHandler := &GetEncryptedWebNoteHandler{ - Templates: templates, - PageData: p, - logger: s.logger, - db: s.db, - } - r.Path("/{id:[a-zA-Z0-9]+}/{encryptionKey:[a-zA-Z0-9]+}.html").Handler(encryptedWebNoteHandler).Methods("GET") - - protectedEncryptedWebNoteHandler := &GetProtectedEncryptedWebNoteHandler{ - Templates: templates, - PageData: p, - logger: s.logger, - db: s.db, - maxUploadSize: s.config.MaxUploadSize, - } - r.Path("/{id:[a-zA-Z0-9]+}/{encryptionKey:[a-zA-Z0-9]+}.html").Handler(protectedEncryptedWebNoteHandler).Methods("POST") - - encryptedRawWebNoteHandler := &GetEncryptedRawWebNoteHandler{ - Templates: templates, - PageData: p, - logger: s.logger, - db: s.db, - } - r.Path("/{id:[a-zA-Z0-9]+}/{encryptionKey:[a-zA-Z0-9]+}/raw").Handler(encryptedRawWebNoteHandler).Methods("GET") - - protectedEncryptedRawWebNoteHandler := &GetProtectedEncryptedRawWebNoteHandler{ - Templates: templates, - PageData: p, - logger: s.logger, - db: s.db, - } - r.Path("/{id:[a-zA-Z0-9]+}/{encryptionKey:[a-zA-Z0-9]+}/raw").Handler(protectedEncryptedRawWebNoteHandler).Methods("POST") - - rawWebNoteHandler := &GetRawWebNoteHandler{ - Templates: templates, - PageData: p, - logger: s.logger, - db: s.db, - } - r.Path("/{id:[a-zA-Z0-9]+}/raw").Handler(rawWebNoteHandler).Methods("GET") - - protectedRawWebNoteHandler := &GetProtectedRawWebNoteHandler{ - Templates: templates, - PageData: p, - logger: s.logger, - db: s.db, - maxUploadSize: s.config.MaxUploadSize, - } - r.Path("/{id:[a-zA-Z0-9]+}/raw").Handler(protectedRawWebNoteHandler).Methods("POST") - webNoteHandler := &GetWebNoteHandler{ Templates: templates, PageData: p, @@ -228,6 +178,23 @@ func (s *Server) Start() error { } r.Path("/{id:[a-zA-Z0-9]+}.html").Handler(protectedWebNoteHandler).Methods("POST") + rawWebNoteHandler := &GetRawWebNoteHandler{ + Templates: templates, + PageData: p, + logger: s.logger, + db: s.db, + } + r.Path("/{id:[a-zA-Z0-9]+}").Handler(rawWebNoteHandler).Methods("GET") + + protectedRawWebNoteHandler := &GetProtectedRawWebNoteHandler{ + Templates: templates, + PageData: p, + logger: s.logger, + db: s.db, + maxUploadSize: s.config.MaxUploadSize, + } + r.Path("/{id:[a-zA-Z0-9]+}").Handler(protectedRawWebNoteHandler).Methods("POST") + if s.config.AceDirectory != "" { r.PathPrefix("/static/ace-builds/").Handler(http.StripPrefix("/static/ace-builds/", http.FileServer(http.Dir(s.config.AceDirectory)))) } diff --git a/src/server/templates/create.html b/src/server/templates/create.html index 9970fa9..569ebbf 100644 --- a/src/server/templates/create.html +++ b/src/server/templates/create.html @@ -8,18 +8,18 @@ {{block "header" .}}{{end}}
- {{if eq .Err nil}} - - {{else}} + {{if .Err}} + {{else}} + {{end}}
diff --git a/src/server/templates/note.html b/src/server/templates/note.html index 977705c..7972a3e 100644 --- a/src/server/templates/note.html +++ b/src/server/templates/note.html @@ -8,7 +8,7 @@