feat: Support Bearer token for clients
All checks were successful
/ pre-commit (push) Successful in 1m39s

Fixes #13.

Signed-off-by: Julien Riou <julien@riou.xyz>
This commit is contained in:
Julien Riou 2025-08-26 17:31:44 +02:00
parent d3b28143ea
commit b45c3e3253
Signed by: jriou
GPG key ID: 9A099EDA51316854
4 changed files with 83 additions and 50 deletions

View file

@ -58,7 +58,7 @@ func handleMain() int {
configFile := flag.String("config", filepath.Join(homeDir, ".config", AppName+".json"), "Configuration file") configFile := flag.String("config", filepath.Join(homeDir, ".config", AppName+".json"), "Configuration file")
reconfigure := flag.Bool("reconfigure", false, "Re-create configuration file") reconfigure := flag.Bool("reconfigure", false, "Re-create configuration file")
url := flag.String("url", "", "URL of the coller API") url := flag.String("url", "", "URL of the coller API")
password := flag.String("password", os.Getenv("COPIER_PASSWORD"), "Password to decrypt the note") password := flag.String("password", os.Getenv("COLLER_PASSWORD"), "Password to encrypt the note")
askPassword := flag.Bool("ask-password", false, "Read password from input") askPassword := flag.Bool("ask-password", false, "Read password from input")
noPassword := flag.Bool("no-password", false, "Allow notes without password") noPassword := flag.Bool("no-password", false, "Allow notes without password")
passwordLength := flag.Int("password-length", 16, "Length of the auto-generated password") passwordLength := flag.Int("password-length", 16, "Length of the auto-generated password")
@ -66,6 +66,8 @@ func handleMain() int {
expiration := flag.Int("expiration", 0, "Number of seconds before expiration") expiration := flag.Int("expiration", 0, "Number of seconds before expiration")
deleteAfterRead := flag.Bool("delete-after-read", false, "Delete the note after the first read") deleteAfterRead := flag.Bool("delete-after-read", false, "Delete the note after the first read")
copier := flag.Bool("copier", false, "Print the copier command to decrypt the note") copier := flag.Bool("copier", false, "Print the copier command to decrypt the note")
bearer := flag.String("bearer", os.Getenv("COLLER_BEARER"), "Bearer token")
askBearer := flag.Bool("b", false, "Read bearer token from input")
flag.Parse() flag.Parse()
@ -94,7 +96,7 @@ func handleMain() int {
logger.Debug("writing configuration file") logger.Debug("writing configuration file")
err := WriteConfig(config, *configFile) err := WriteConfig(config, *configFile)
if err != nil { if err != nil {
return ReturnError(logger, "could not create configuration file", err) return internal.ReturnError(logger, "could not create configuration file", err)
} }
} }
@ -102,7 +104,7 @@ func handleMain() int {
var config Config var config Config
err = internal.ReadConfig(*configFile, &config) err = internal.ReadConfig(*configFile, &config)
if err != nil { if err != nil {
return ReturnError(logger, "could not read configuration file", err) return internal.ReturnError(logger, "could not read configuration file", err)
} }
*url = config.URL *url = config.URL
@ -113,12 +115,12 @@ func handleMain() int {
logger.Debug("reading from file", slog.Any("file", *fileName)) logger.Debug("reading from file", slog.Any("file", *fileName))
content, err = os.ReadFile(*fileName) content, err = os.ReadFile(*fileName)
if err != nil { if err != nil {
return ReturnError(logger, "could not read from file", err) return internal.ReturnError(logger, "could not read from file", err)
} }
} else { } else {
err = clipboard.Init() err = clipboard.Init()
if err != nil { if err != nil {
return ReturnError(logger, "could not initialize clipboard library", err) return internal.ReturnError(logger, "could not initialize clipboard library", err)
} }
content = clipboard.Read(clipboard.FmtText) content = clipboard.Read(clipboard.FmtText)
} }
@ -127,7 +129,7 @@ func handleMain() int {
fmt.Print("Password: ") fmt.Print("Password: ")
p, err := term.ReadPassword(int(syscall.Stdin)) p, err := term.ReadPassword(int(syscall.Stdin))
if err != nil { if err != nil {
return ReturnError(logger, "could not read password", err) return internal.ReturnError(logger, "could not read password", err)
} }
*password = string(p) *password = string(p)
fmt.Print("\n") fmt.Print("\n")
@ -136,13 +138,13 @@ func handleMain() int {
if !*noPassword && *password == "" { if !*noPassword && *password == "" {
logger.Debug("generating random password") logger.Debug("generating random password")
if *passwordLength < internal.MIN_PASSWORD_LENGTH || *passwordLength > internal.MAX_PASSWORD_LENGTH { if *passwordLength < internal.MIN_PASSWORD_LENGTH || *passwordLength > internal.MAX_PASSWORD_LENGTH {
return ReturnError(logger, "invalid password length for auto-generated password", fmt.Errorf("password length must be between %d and %d", internal.MIN_PASSWORD_LENGTH, internal.MAX_PASSWORD_LENGTH)) return internal.ReturnError(logger, "invalid password length for auto-generated password", fmt.Errorf("password length must be between %d and %d", internal.MIN_PASSWORD_LENGTH, internal.MAX_PASSWORD_LENGTH))
} }
*password = internal.GenerateChars(*passwordLength) *password = internal.GenerateChars(*passwordLength)
} }
if len(content) == 0 { if len(content) == 0 {
return ReturnError(logger, "could not create empty note", nil) return internal.ReturnError(logger, "could not create empty note", nil)
} }
p := NotePayload{} p := NotePayload{}
@ -156,12 +158,12 @@ func handleMain() int {
if *password != "" { if *password != "" {
logger.Debug("validating password") logger.Debug("validating password")
if err = internal.ValidatePassword(*password); err != nil { if err = internal.ValidatePassword(*password); err != nil {
return ReturnError(logger, "invalid password", nil) return internal.ReturnError(logger, "invalid password", nil)
} }
logger.Debug("encrypting content") logger.Debug("encrypting content")
content, err = internal.Encrypt(content, *password) content, err = internal.Encrypt(content, *password)
if err != nil { if err != nil {
return ReturnError(logger, "could not encrypt note", err) return internal.ReturnError(logger, "could not encrypt note", err)
} }
p.Encrypted = true p.Encrypted = true
} }
@ -172,32 +174,51 @@ func handleMain() int {
payload, err := json.Marshal(p) payload, err := json.Marshal(p)
if err != nil { if err != nil {
return ReturnError(logger, "could not serialize note to json", err) return internal.ReturnError(logger, "could not serialize note to json", err)
} }
apiRoute := *url + "/api/note" apiRoute := *url + "/api/note"
logger.Debug("creating note", slog.Any("payload", payload), slog.Any("url", apiRoute)) if *askBearer {
fmt.Print("Bearer: ")
b, err := term.ReadPassword(int(syscall.Stdin))
if err != nil {
return internal.ReturnError(logger, "could not read bearer token", err)
}
*bearer = string(b)
fmt.Print("\n")
}
r, err := http.Post(apiRoute, "application/json", bytes.NewReader(payload)) logger.Debug("creating http request")
req, err := http.NewRequest("POST", apiRoute, bytes.NewReader(payload))
if err != nil { if err != nil {
return ReturnError(logger, "could not create note", err) return internal.ReturnError(logger, "could not create request", err)
}
if *bearer != "" {
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", *bearer))
}
logger.Debug("creating note", slog.Any("payload", payload), slog.Any("url", apiRoute))
r, err := http.DefaultClient.Do(req)
if err != nil {
return internal.ReturnError(logger, "could not create note", err)
} }
logger.Debug("reading response", slog.Any("response", r)) logger.Debug("reading response", slog.Any("response", r))
body, err := io.ReadAll(r.Body) body, err := io.ReadAll(r.Body)
if err != nil { if err != nil {
return ReturnError(logger, "could not read response", err) return internal.ReturnError(logger, "could not read response", err)
} }
jsonBody := &NoteResponse{} jsonBody := &NoteResponse{}
err = json.Unmarshal(body, jsonBody) err = json.Unmarshal(body, jsonBody)
if err != nil { if err != nil {
return ReturnError(logger, "could not decode response", err) return internal.ReturnError(logger, "could not decode response", err)
} }
if r.StatusCode != http.StatusOK { if r.StatusCode != http.StatusOK {
return ReturnError(logger, jsonBody.Message, fmt.Errorf("%s", jsonBody.Error)) return internal.ReturnError(logger, jsonBody.Message, fmt.Errorf("%s", jsonBody.Error))
} }
logger.Debug("finding note location") logger.Debug("finding note location")
@ -243,12 +264,3 @@ func WriteConfig(config Config, fileName string) error {
return nil return nil
} }
func ReturnError(logger *slog.Logger, message string, err error) int {
if err != nil {
logger.Error(message, slog.Any("error", err))
} else {
logger.Error(message)
}
return internal.RC_ERROR
}

View file

@ -50,27 +50,23 @@ func handleMain() int {
if *configFileName != "" { if *configFileName != "" {
err = internal.ReadConfig(*configFileName, config) err = internal.ReadConfig(*configFileName, config)
if err != nil { if err != nil {
logger.Error("cannot parse configuration file", slog.Any("error", err)) return internal.ReturnError(logger, "could not parse configuration file", err)
return internal.RC_ERROR
} }
logger.Debug("configuration file parsed", slog.Any("file", *configFileName)) logger.Debug("configuration file parsed", slog.Any("file", *configFileName))
} }
if err = config.Check(); err != nil { if err = config.Check(); err != nil {
logger.Error("invalid configuration", slog.Any("error", err)) return internal.ReturnError(logger, "invalid configuration", err)
return internal.RC_ERROR
} }
db, err := server.NewDatabase(logger, config) db, err := server.NewDatabase(logger, config)
if err != nil { if err != nil {
logger.Error("could not connect to the database", slog.Any("error", err)) return internal.ReturnError(logger, "could not connect to the database", err)
return internal.RC_ERROR
} }
srv, err := server.NewServer(logger, db, config, AppVersion) srv, err := server.NewServer(logger, db, config, AppVersion)
if err != nil { if err != nil {
logger.Error("could not create server", slog.Any("error", err)) return internal.ReturnError(logger, "could not create server", err)
return internal.RC_ERROR
} }
srv.SetIDLength(config.IDLength) srv.SetIDLength(config.IDLength)
@ -80,16 +76,14 @@ func handleMain() int {
reg := prometheus.NewRegistry() reg := prometheus.NewRegistry()
metrics, err := server.NewMetrics(logger, reg, config, db) metrics, err := server.NewMetrics(logger, reg, config, db)
if err != nil { if err != nil {
logger.Error("could not register metrics", slog.Any("error", err)) return internal.ReturnError(logger, "could not register metrics", err)
return internal.RC_ERROR
} }
srv.SetMetrics(metrics) srv.SetMetrics(metrics)
} }
err = srv.Start() err = srv.Start()
if err != nil { if err != nil {
logger.Error("could not start server", slog.Any("error", err)) return internal.ReturnError(logger, "could not start server", err)
return internal.RC_ERROR
} }
return internal.RC_OK return internal.RC_OK

View file

@ -28,9 +28,11 @@ func handleMain() int {
quiet := flag.Bool("quiet", false, "Log errors only") quiet := flag.Bool("quiet", false, "Log errors only")
verbose := flag.Bool("verbose", false, "Print more logs") verbose := flag.Bool("verbose", false, "Print more logs")
debug := flag.Bool("debug", false, "Print even more logs") debug := flag.Bool("debug", false, "Print even more logs")
password := flag.String("password", os.Getenv("COPIER_PASSWORD"), "Password to decrypt the note") password := flag.String("password", os.Getenv("COLLER_PASSWORD"), "Password to decrypt the note")
askPassword := flag.Bool("w", false, "Read password from input") askPassword := flag.Bool("w", false, "Read password from input")
fileName := flag.String("file", "", "Write content of the note to a file") 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("b", false, "Read bearer token from input")
flag.Parse() flag.Parse()
@ -62,24 +64,41 @@ func handleMain() int {
fmt.Print("Password: ") fmt.Print("Password: ")
p, err := term.ReadPassword(int(syscall.Stdin)) p, err := term.ReadPassword(int(syscall.Stdin))
if err != nil { if err != nil {
logger.Error("could not read password", slog.Any("error", err)) return internal.ReturnError(logger, "could not read password", err)
return internal.RC_ERROR
} }
*password = string(p) *password = string(p)
fmt.Print("\n")
}
if *askBearer {
fmt.Print("Bearer: ")
b, err := term.ReadPassword(int(syscall.Stdin))
if err != nil {
return internal.ReturnError(logger, "could not read bearer token", err)
}
*bearer = string(b)
fmt.Print("\n")
}
logger.Debug("creating http request")
req, err := http.NewRequest("GET", url, 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("parsing url", slog.Any("url", url))
r, err := http.Get(url) r, err := http.DefaultClient.Do(req)
if err != nil { if err != nil {
logger.Error("could not retreive note", slog.Any("error", err)) return internal.ReturnError(logger, "could not retreive note", err)
return internal.RC_ERROR
} }
logger.Debug("decoding body") logger.Debug("decoding body")
body, err := io.ReadAll(r.Body) body, err := io.ReadAll(r.Body)
if err != nil { if err != nil {
logger.Error("could not read response", slog.Any("error", err)) return internal.ReturnError(logger, "could not read response", err)
return internal.RC_ERROR
} }
var content []byte var content []byte
@ -87,8 +106,7 @@ func handleMain() int {
logger.Debug("decrypting note") logger.Debug("decrypting note")
content, err = internal.Decrypt(body, *password) content, err = internal.Decrypt(body, *password)
if err != nil { if err != nil {
logger.Error("could not decrypt paste", slog.Any("error", err)) return internal.ReturnError(logger, "could not decrypt paste", err)
return internal.RC_ERROR
} }
} else { } else {
content = body content = body
@ -98,8 +116,7 @@ func handleMain() int {
logger.Debug("writing output to file", slog.Any("file", *fileName)) logger.Debug("writing output to file", slog.Any("file", *fileName))
err = os.WriteFile(*fileName, content, 0644) err = os.WriteFile(*fileName, content, 0644)
if err != nil { if err != nil {
logger.Error("could not write output to file", slog.Any("error", err)) return internal.ReturnError(logger, "could not write output to file", err)
return internal.RC_ERROR
} }
} else { } else {
fmt.Printf("%s", content) fmt.Printf("%s", content)

View file

@ -3,6 +3,7 @@ package internal
import ( import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"log/slog"
"math/rand" "math/rand"
"os" "os"
"path/filepath" "path/filepath"
@ -99,3 +100,12 @@ func HumanDuration(i int) string {
} }
return fmt.Sprintf("%d %s", i, w) return fmt.Sprintf("%d %s", i, w)
} }
func ReturnError(logger *slog.Logger, message string, err error) int {
if err != nil {
logger.Error(message, slog.Any("error", err))
} else {
logger.Error(message)
}
return RC_ERROR
}