1
0
Fork 0
forked from jriou/coller

feat: Add password protection

Fixes #37.

BREAKING CHANGE: API routes are prefixed by /api/note.

Signed-off-by: Julien Riou <julien@riou.xyz>
This commit is contained in:
Julien Riou 2025-09-27 08:35:26 +02:00
commit 9e0254c0b5
Signed by: jriou
GPG key ID: 9A099EDA51316854
16 changed files with 713 additions and 135 deletions

View file

@ -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"
}
}

View file

@ -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 /\<id\>/\<encryptionKey\>
### GET /api/note/\<id\>/\<encryptionKey\>
> [!WARNING]
> Potential encryption key leak
Return content of a note encrypted by the given encryption key.
### GET /\<id\>
### POST /api/note/\<id\>/\<encryptionKey\>
> [!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/\<id\>
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/\<id\>
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:

View file

@ -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 {