From 5122000d4862745095fb149997ec2dad2ece1501 Mon Sep 17 00:00:00 2001 From: Julien Riou Date: Wed, 17 Sep 2025 10:24:35 +0200 Subject: [PATCH 01/28] feat: Create releases with make releases Fixes #22. Signed-off-by: Julien Riou --- Makefile | 61 ++++++++++++++++++++++++++++++++++++++----------- docker/build.sh | 20 ++++++++++++++++ 2 files changed, 68 insertions(+), 13 deletions(-) create mode 100755 docker/build.sh diff --git a/Makefile b/Makefile index d73d30f..cb3b93c 100644 --- a/Makefile +++ b/Makefile @@ -4,26 +4,61 @@ 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} +DOCKER_IMAGE = golang:1.24-trixie -.PHONY: clean test +.PHONY: clean clean_for_releases test build: - (cd src \ - && go build -ldflags "${LDFLAGS}" -o ../bin/collerd cmd/collerd/main.go \ - && go build -ldflags "${LDFLAGS}" -o ../bin/copier cmd/copier/main.go \ - && go build -ldflags "${LDFLAGS}" -o ../bin/coller cmd/coller/main.go \ - ) + cd src \ + && go build -ldflags "${LDFLAGS}" -o ../bin/collerd cmd/collerd/main.go \ + && go build -ldflags "${LDFLAGS}" -o ../bin/copier cmd/copier/main.go \ + && 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) +build_linux_amd64: + cd src \ + && GOOS=linux GOARCH=amd64 go build -ldflags "${LDFLAGS}" -o ../bin/collerd-${APPVERSION}-linux-amd64 cmd/collerd/main.go \ + && GOOS=linux GOARCH=amd64 go build -ldflags "${LDFLAGS}" -o ../bin/coller-${APPVERSION}-linux-amd64 cmd/coller/main.go \ + && GOOS=linux GOARCH=amd64 go build -ldflags "${LDFLAGS}" -o ../bin/copier-${APPVERSION}-linux-amd64 cmd/copier/main.go -release: build archive +archive_linux_amd64: + mkdir -p releases/coller-${APPVERSION}-linux-amd64 \ + && cp bin/collerd-${APPVERSION}-linux-amd64 releases/coller-${APPVERSION}-linux-amd64/collerd \ + && cp bin/coller-${APPVERSION}-linux-amd64 releases/coller-${APPVERSION}-linux-amd64/coller \ + && cp bin/copier-${APPVERSION}-linux-amd64 releases/coller-${APPVERSION}-linux-amd64/copier \ + && cd releases/ \ + && tar cvpzf coller-${APPVERSION}-linux-amd64.tar.gz coller-${APPVERSION}-linux-amd64 + +build_darwin_arm64: + cd src \ + && GOOS=darwin GOARCH=arm64 go build -ldflags "${LDFLAGS}" -o ../bin/collerd-${APPVERSION}-darwin-arm64 cmd/collerd/main.go \ + && GOOS=darwin GOARCH=arm64 go build -ldflags "${LDFLAGS}" -o ../bin/coller-${APPVERSION}-darwin-arm64 cmd/coller/main.go \ + && GOOS=darwin GOARCH=arm64 go build -ldflags "${LDFLAGS}" -o ../bin/copier-${APPVERSION}-darwin-arm64 cmd/copier/main.go + +archive_darwin_arm64: + mkdir -p releases/coller-${APPVERSION}-darwin-arm64 \ + && cp bin/collerd-${APPVERSION}-darwin-arm64 releases/coller-${APPVERSION}-darwin-arm64/collerd \ + && cp bin/coller-${APPVERSION}-darwin-arm64 releases/coller-${APPVERSION}-darwin-arm64/coller \ + && cp bin/copier-${APPVERSION}-darwin-arm64 releases/coller-${APPVERSION}-darwin-arm64/copier \ + && cd releases/ \ + && tar cvpzf coller-${APPVERSION}-darwin-arm64.tar.gz coller-${APPVERSION}-darwin-arm64 + +checksum: + cd releases \ + && sha256sum *.tar.gz > checksums.txt + +clean_for_releases: + rm -rf releases/coller-${APPVERSION}-linux-amd64 \ + && rm -rf releases/coller-${APPVERSION}-darwin-arm64 + +releases: build_linux_amd64 build_darwin_arm64 archive_linux_amd64 archive_darwin_arm64 checksum clean_for_releases + +releases_with_docker: + docker run -it -v $(shell pwd):/mnt -w /mnt -e "UID=$(shell id -u)" -e "GID=$(shell id -g)" ${DOCKER_IMAGE} ./docker/build.sh test: - (cd src \ - && go test internal/*.go \ - && go test server/*.go \ - ) + cd src \ + && go test internal/*.go \ + && go test server/*.go clean: rm -rf bin releases diff --git a/docker/build.sh b/docker/build.sh new file mode 100755 index 0000000..783ba44 --- /dev/null +++ b/docker/build.sh @@ -0,0 +1,20 @@ +#!/bin/bash +# Script to create releases with Docker +# Used by `make releases_with_docker` +set -e + +if [ -z "UID" ] ; then + echo "UID not defined" + exit 1 +fi +if [ -z "GID" ] ; then + echo "GID not defined" + exit 1 +fi + +apt-get update +apt-get install -y libx11-dev + +make releases + +chown ${UID}:${GID} -R releases From b62a807f89c3c9d46c199fcc6be8b41036c79e31 Mon Sep 17 00:00:00 2001 From: Julien Riou Date: Fri, 19 Sep 2025 07:12:21 +0200 Subject: [PATCH 02/28] feat: Add HTTPS support Fixes #19. Signed-off-by: Julien Riou --- src/cmd/collerd/README.md | 2 ++ src/server/config.go | 6 ++++++ src/server/server.go | 13 ++++++++++--- 3 files changed, 18 insertions(+), 3 deletions(-) diff --git a/src/cmd/collerd/README.md b/src/cmd/collerd/README.md index d99a273..114604e 100644 --- a/src/cmd/collerd/README.md +++ b/src/cmd/collerd/README.md @@ -34,6 +34,8 @@ The file format is **JSON**: * **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 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 8ba51ca..060f693 100644 --- a/src/server/config.go +++ b/src/server/config.go @@ -26,6 +26,8 @@ type Config struct { 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"` } func NewConfig() *Config { @@ -94,3 +96,7 @@ func (c *Config) Check() error { } return nil } + +func (c *Config) HasTLS() bool { + return c.TLSCertFile != "" && c.TLSKeyFile != "" +} diff --git a/src/server/server.go b/src/server/server.go index 703714d..26f8e75 100644 --- a/src/server/server.go +++ b/src/server/server.go @@ -13,9 +13,10 @@ import ( "strconv" "strings" - "git.riou.xyz/jriou/coller/internal" "github.com/gorilla/mux" "github.com/prometheus/client_golang/prometheus/promhttp" + + "git.riou.xyz/jriou/coller/internal" ) var passwordLength = internal.MIN_PASSWORD_LENGTH @@ -472,6 +473,12 @@ func (s *Server) Start() error { r.Path("/").Handler(&HomeHandler{Templates: templates, PageData: p}).Methods("GET") addr := fmt.Sprintf("%s:%d", s.config.ListenAddress, s.config.ListenPort) - s.logger.Info(fmt.Sprintf("listening to %s:%d", s.config.ListenAddress, s.config.ListenPort)) - return http.ListenAndServe(addr, r) + + if s.config.HasTLS() { + s.logger.Info(fmt.Sprintf("listening to %s:%d (https)", s.config.ListenAddress, s.config.ListenPort)) + return http.ListenAndServeTLS(addr, s.config.TLSCertFile, s.config.TLSKeyFile, r) + } else { + s.logger.Info(fmt.Sprintf("listening to %s:%d (http)", s.config.ListenAddress, s.config.ListenPort)) + return http.ListenAndServe(addr, r) + } } From ff92e302324a5e3bccb3d626bf8892c559cf2dff Mon Sep 17 00:00:00 2001 From: Julien Riou Date: Tue, 16 Sep 2025 09:36:10 +0200 Subject: [PATCH 03/28] feat: Option to serve Bootstrap from the filesystem Signed-off-by: Julien Riou --- .gitignore | 1 + Makefile | 3 +++ package-lock.json | 40 ++++++++++++++++++++++++++++++++++ package.json | 5 +++++ src/cmd/collerd/README.md | 34 ++++++++++++++++++++++++++++- src/server/config.go | 1 + src/server/server.go | 14 ++++++++---- src/server/templates/head.html | 2 +- 8 files changed, 94 insertions(+), 6 deletions(-) create mode 100644 package-lock.json create mode 100644 package.json diff --git a/.gitignore b/.gitignore index cf8b1ff..d2fe01d 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,4 @@ releases collerd.db !docker/collerd.json collerd.json +node_modules diff --git a/Makefile b/Makefile index cb3b93c..547f06e 100644 --- a/Makefile +++ b/Makefile @@ -55,6 +55,9 @@ releases: build_linux_amd64 build_darwin_arm64 archive_linux_amd64 archive_darwi releases_with_docker: docker run -it -v $(shell pwd):/mnt -w /mnt -e "UID=$(shell id -u)" -e "GID=$(shell id -g)" ${DOCKER_IMAGE} ./docker/build.sh +dependencies: + npm install + test: cd src \ && go test internal/*.go \ diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..4cf810c --- /dev/null +++ b/package-lock.json @@ -0,0 +1,40 @@ +{ + "name": "coller", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "dependencies": { + "bootstrap": "^5.3.8" + } + }, + "node_modules/@popperjs/core": { + "version": "2.11.8", + "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz", + "integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==", + "peer": true, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/popperjs" + } + }, + "node_modules/bootstrap": { + "version": "5.3.8", + "resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-5.3.8.tgz", + "integrity": "sha512-HP1SZDqaLDPwsNiqRqi5NcP0SSXciX2s9E+RyqJIIqGo+vJeN5AJVM98CXmW/Wux0nQ5L7jeWUdplCEf0Ee+tg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/twbs" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/bootstrap" + } + ], + "peerDependencies": { + "@popperjs/core": "^2.11.8" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..719e5df --- /dev/null +++ b/package.json @@ -0,0 +1,5 @@ +{ + "dependencies": { + "bootstrap": "^5.3.8" + } +} diff --git a/src/cmd/collerd/README.md b/src/cmd/collerd/README.md index 114604e..5f9a06d 100644 --- a/src/cmd/collerd/README.md +++ b/src/cmd/collerd/README.md @@ -36,6 +36,7 @@ The file format is **JSON**: * **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. The configuration file is not required but the service might not be exposed to the public. @@ -78,4 +79,35 @@ If the note is encrypted, the encrypted value is returned (application/octet-str Errors return **500 Server Internal Error** with the **JSON** payload: * **message** (string): context of the error -* **error** (string): error message \ No newline at end of file +* **error** (string): error message + +## Dependencies + +The web interface depends on: + +- [Bootstrap](https://getbootstrap.com/) +- [Monaco Editor](https://github.com/microsoft/monaco-editor/) + +By default, those dependencies are fetched from **remote CDN** services by the client. + +If you would like to download them to serve them locally: + +``` +npm install +``` + +or via `make`: + +``` +make dependencies +``` + +Then configure the local directories: + +```json +{ + "bootstrap_directory": "./node_modules/bootstrap/dist" +} +``` + +Downloading Monaco Editor is not supported yet. diff --git a/src/server/config.go b/src/server/config.go index 060f693..f07c9a7 100644 --- a/src/server/config.go +++ b/src/server/config.go @@ -28,6 +28,7 @@ type Config struct { 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/server.go b/src/server/server.go index 26f8e75..f8a2d3f 100644 --- a/src/server/server.go +++ b/src/server/server.go @@ -196,6 +196,7 @@ type PageData struct { URL string Note *Note EnableUploadFileButton bool + BootstrapDirectory string } type HomeHandler struct { @@ -430,10 +431,11 @@ 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, + Title: s.config.Title, + Expirations: s.config.Expirations, + Expiration: s.config.Expiration, + Languages: s.config.Languages, + BootstrapDirectory: s.config.BootstrapDirectory, } if s.config.ShowVersion { @@ -470,6 +472,10 @@ func (s *Server) Start() error { } r.Path("/{id:[a-zA-Z0-9]+}.html").Handler(webNoteHandler).Methods("GET") + if s.config.BootstrapDirectory != "" { + r.PathPrefix("/static/bootstrap/").Handler(http.StripPrefix("/static/bootstrap/", http.FileServer(http.Dir(s.config.BootstrapDirectory)))) + } + r.Path("/").Handler(&HomeHandler{Templates: templates, PageData: p}).Methods("GET") addr := fmt.Sprintf("%s:%d", s.config.ListenAddress, s.config.ListenPort) diff --git a/src/server/templates/head.html b/src/server/templates/head.html index bd8edba..e5f3544 100644 --- a/src/server/templates/head.html +++ b/src/server/templates/head.html @@ -4,6 +4,6 @@ {{.Title}} - + {{end}} \ No newline at end of file From 70d3892b1570b45ac5aa6373879b4055c4382d3b Mon Sep 17 00:00:00 2001 From: Julien Riou Date: Sat, 20 Sep 2025 06:36:28 +0200 Subject: [PATCH 04/28] feat: Add Command-line clients link and web page Signed-off-by: Julien Riou --- Makefile | 45 +++++++++------------- docker/build.sh | 2 +- src/internal/utils.go | 9 +++++ src/internal/utils_test.go | 22 +++++++++++ src/server/server.go | 51 ++++++++++++++++++++++++- src/server/templates/clients.html | 62 +++++++++++++++++++++++++++++++ src/server/templates/header.html | 5 +++ 7 files changed, 166 insertions(+), 30 deletions(-) create mode 100644 src/server/templates/clients.html diff --git a/Makefile b/Makefile index 547f06e..8e56de7 100644 --- a/Makefile +++ b/Makefile @@ -16,41 +16,30 @@ build: build_linux_amd64: cd src \ - && GOOS=linux GOARCH=amd64 go build -ldflags "${LDFLAGS}" -o ../bin/collerd-${APPVERSION}-linux-amd64 cmd/collerd/main.go \ - && GOOS=linux GOARCH=amd64 go build -ldflags "${LDFLAGS}" -o ../bin/coller-${APPVERSION}-linux-amd64 cmd/coller/main.go \ - && GOOS=linux GOARCH=amd64 go build -ldflags "${LDFLAGS}" -o ../bin/copier-${APPVERSION}-linux-amd64 cmd/copier/main.go - -archive_linux_amd64: - mkdir -p releases/coller-${APPVERSION}-linux-amd64 \ - && cp bin/collerd-${APPVERSION}-linux-amd64 releases/coller-${APPVERSION}-linux-amd64/collerd \ - && cp bin/coller-${APPVERSION}-linux-amd64 releases/coller-${APPVERSION}-linux-amd64/coller \ - && cp bin/copier-${APPVERSION}-linux-amd64 releases/coller-${APPVERSION}-linux-amd64/copier \ - && cd releases/ \ - && tar cvpzf coller-${APPVERSION}-linux-amd64.tar.gz coller-${APPVERSION}-linux-amd64 + && GOOS=linux GOARCH=amd64 go build -ldflags "${LDFLAGS}" -o ../bin/collerd-linux-amd64 cmd/collerd/main.go \ + && GOOS=linux GOARCH=amd64 go build -ldflags "${LDFLAGS}" -o ../bin/coller-linux-amd64 cmd/coller/main.go \ + && GOOS=linux GOARCH=amd64 go build -ldflags "${LDFLAGS}" -o ../bin/copier-linux-amd64 cmd/copier/main.go build_darwin_arm64: cd src \ - && GOOS=darwin GOARCH=arm64 go build -ldflags "${LDFLAGS}" -o ../bin/collerd-${APPVERSION}-darwin-arm64 cmd/collerd/main.go \ - && GOOS=darwin GOARCH=arm64 go build -ldflags "${LDFLAGS}" -o ../bin/coller-${APPVERSION}-darwin-arm64 cmd/coller/main.go \ - && GOOS=darwin GOARCH=arm64 go build -ldflags "${LDFLAGS}" -o ../bin/copier-${APPVERSION}-darwin-arm64 cmd/copier/main.go + && GOOS=darwin GOARCH=arm64 go build -ldflags "${LDFLAGS}" -o ../bin/collerd-darwin-arm64 cmd/collerd/main.go \ + && GOOS=darwin GOARCH=arm64 go build -ldflags "${LDFLAGS}" -o ../bin/coller-darwin-arm64 cmd/coller/main.go \ + && GOOS=darwin GOARCH=arm64 go build -ldflags "${LDFLAGS}" -o ../bin/copier-darwin-arm64 cmd/copier/main.go -archive_darwin_arm64: - mkdir -p releases/coller-${APPVERSION}-darwin-arm64 \ - && cp bin/collerd-${APPVERSION}-darwin-arm64 releases/coller-${APPVERSION}-darwin-arm64/collerd \ - && cp bin/coller-${APPVERSION}-darwin-arm64 releases/coller-${APPVERSION}-darwin-arm64/coller \ - && cp bin/copier-${APPVERSION}-darwin-arm64 releases/coller-${APPVERSION}-darwin-arm64/copier \ - && cd releases/ \ - && tar cvpzf coller-${APPVERSION}-darwin-arm64.tar.gz coller-${APPVERSION}-darwin-arm64 +create_release: + mkdir -p releases/${APPVERSION} \ + && cp -p bin/collerd-linux-amd64 releases/${APPVERSION}/collerd-linux-amd64 \ + && cp -p bin/coller-linux-amd64 releases/${APPVERSION}/coller-linux-amd64 \ + && cp -p bin/copier-linux-amd64 releases/${APPVERSION}/copier-linux-amd64 \ + && cp -p bin/collerd-darwin-arm64 releases/${APPVERSION}/collerd-darwin-arm64 \ + && cp -p bin/coller-darwin-arm64 releases/${APPVERSION}/coller-darwin-arm64 \ + && cp -p bin/copier-darwin-arm64 releases/${APPVERSION}/copier-darwin-arm64 checksum: - cd releases \ - && sha256sum *.tar.gz > checksums.txt + cd releases/${APPVERSION} \ + && sha256sum collerd-* coller-* copier-* > checksums.txt -clean_for_releases: - rm -rf releases/coller-${APPVERSION}-linux-amd64 \ - && rm -rf releases/coller-${APPVERSION}-darwin-arm64 - -releases: build_linux_amd64 build_darwin_arm64 archive_linux_amd64 archive_darwin_arm64 checksum clean_for_releases +releases: build_linux_amd64 build_darwin_arm64 create_release checksum releases_with_docker: docker run -it -v $(shell pwd):/mnt -w /mnt -e "UID=$(shell id -u)" -e "GID=$(shell id -g)" ${DOCKER_IMAGE} ./docker/build.sh diff --git a/docker/build.sh b/docker/build.sh index 783ba44..47c817d 100755 --- a/docker/build.sh +++ b/docker/build.sh @@ -17,4 +17,4 @@ apt-get install -y libx11-dev make releases -chown ${UID}:${GID} -R releases +chown ${UID}:${GID} -R bin releases diff --git a/src/internal/utils.go b/src/internal/utils.go index c98c2e3..9db5ece 100644 --- a/src/internal/utils.go +++ b/src/internal/utils.go @@ -117,3 +117,12 @@ func ToLowerStringSlice(src []string) (dst []string) { } return } + +func InSlice(s []string, elem string) bool { + for _, v := range s { + if v == elem { + return true + } + } + return false +} diff --git a/src/internal/utils_test.go b/src/internal/utils_test.go index cc251f0..eef7c03 100644 --- a/src/internal/utils_test.go +++ b/src/internal/utils_test.go @@ -85,3 +85,25 @@ func TestToLowerStringsSlice(t *testing.T) { t.Logf("got '%s', want '%s'", got, expected) } } + +func TestInSlice(t *testing.T) { + tests := []struct { + elem string + s []string + expected bool + }{ + {"linux", []string{"linux", "darwin"}, true}, + {"windows", []string{"linux", "darwin"}, false}, + } + + for _, tc := range tests { + t.Run(fmt.Sprintf("TestInSlice#%s", tc.elem), func(t *testing.T) { + got := InSlice(tc.s, tc.elem) + if got != tc.expected { + t.Errorf("got '%t', want '%t'", got, tc.expected) + } else { + t.Logf("got '%t', want '%t'", got, tc.expected) + } + }) + } +} diff --git a/src/server/server.go b/src/server/server.go index f8a2d3f..4cef956 100644 --- a/src/server/server.go +++ b/src/server/server.go @@ -19,7 +19,12 @@ import ( "git.riou.xyz/jriou/coller/internal" ) -var passwordLength = internal.MIN_PASSWORD_LENGTH +var ( + passwordLength = internal.MIN_PASSWORD_LENGTH + supportedOSes = []string{"linux", "darwin"} + supportedArches = []string{"amd64", "arm64"} + supportedClients = []string{"coller", "copier"} +) type Server struct { logger *slog.Logger @@ -405,6 +410,42 @@ func (h *GetProtectedWebNoteHandler) ServeHTTP(w http.ResponseWriter, r *http.Re h.Templates.ExecuteTemplate(w, "note", h.PageData) } +type ClientsHandler struct { + Templates *template.Template + PageData PageData + logger *slog.Logger +} + +func (h *ClientsHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { + h.logger.Debug("rendering clients web page") + h.Templates.ExecuteTemplate(w, "clients", h.PageData) +} + +type ClientHandler struct { + logger *slog.Logger + version string +} + +func (h *ClientHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { + h.logger.Debug("rendering client redirection") + vars := mux.Vars(r) + os := vars["os"] + arch := vars["arch"] + clientName := vars["clientName"] + + if !internal.InSlice(supportedOSes, os) || !internal.InSlice(supportedArches, arch) || !internal.InSlice(supportedClients, clientName) { + w.WriteHeader(http.StatusNotFound) + return + } + + version := h.version + if version == "" { + version = "latest" + } + + http.Redirect(w, r, fmt.Sprintf("https://git.riou.xyz/jriou/%s/releases/download/%s/%s-%s-%s", clientName, version, clientName, os, arch), http.StatusMovedPermanently) +} + //go:embed templates/* var templatesFS embed.FS @@ -456,6 +497,14 @@ func (s *Server) Start() error { } r.Path("/create").Handler(createNoteWithFormHandler).Methods("POST") + clientsHandler := &ClientsHandler{ + Templates: templates, + PageData: p, + 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") + protectedWebNoteHandler := &GetProtectedWebNoteHandler{ Templates: templates, PageData: p, diff --git a/src/server/templates/clients.html b/src/server/templates/clients.html new file mode 100644 index 0000000..6e57267 --- /dev/null +++ b/src/server/templates/clients.html @@ -0,0 +1,62 @@ +{{define "clients"}} + + + +{{block "head" .}}{{end}} + + + {{block "header" .}}{{end}} +
+

Command-line clients

+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameDocsOSArchDownload
coller📄 + Linuxx86-64💾
macOSARM64💾
copier📄 + Linuxx86-64💾
macOSARM64💾
+
+ + {{block "footer" .}}{{end}} + + + +{{end}} \ No newline at end of file diff --git a/src/server/templates/header.html b/src/server/templates/header.html index def42eb..091455d 100644 --- a/src/server/templates/header.html +++ b/src/server/templates/header.html @@ -5,6 +5,11 @@ {{.Title}} + + {{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 14/28] 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 15/28] 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 @@