1
0
Fork 0
forked from jriou/coller

feat: Return top-level errors to clients

In order to reduce security risk, the server now returns only functional error
messages to the clients and log low-level error messages.

Fixes #35.

Signed-off-by: Julien Riou <julien@riou.xyz>
This commit is contained in:
Julien Riou 2025-10-01 18:11:30 +02:00
commit 55de3afc71
Signed by: jriou
GPG key ID: 9A099EDA51316854
4 changed files with 170 additions and 115 deletions

View file

@ -12,6 +12,43 @@ import (
"git.riou.xyz/jriou/coller/internal"
)
type APIErrorResponse struct {
Message string `json:"message"`
}
func (e APIErrorResponse) ToJSON() string {
b, err := json.Marshal(e)
if err == nil {
return string(b)
}
return fmt.Sprintf("{\"message\":\"could not serialize response to JSON\", \"error\":\"%v\"}", err)
}
func apiError(level int, w http.ResponseWriter, logger *slog.Logger, msg string, err error) {
if err != nil {
err = fmt.Errorf("%s: %w", msg, err)
}
logger.Error(fmt.Sprintf("%v", err))
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(level)
fmt.Fprint(w, APIErrorResponse{
Message: msg,
}.ToJSON())
}
func APIError(w http.ResponseWriter, logger *slog.Logger, msg string, err error) {
apiError(http.StatusInternalServerError, w, logger, msg, err)
}
func APIErrorNotFound(w http.ResponseWriter, logger *slog.Logger, msg string, err error) {
apiError(http.StatusNotFound, w, logger, msg, err)
}
func APIErrorBadRequest(w http.ResponseWriter, logger *slog.Logger, msg string, err error) {
apiError(http.StatusBadRequest, w, logger, msg, err)
}
func HealthHandler(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "OK")
}
@ -41,36 +78,38 @@ type CreateNoteResponse struct {
func (h *CreateNoteHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
logger := h.logger.With("handler", "CreateNoteHandler")
bodyReader := http.MaxBytesReader(w, r.Body, h.maxUploadSize)
defer r.Body.Close()
var body CreateNotePayload
err := json.NewDecoder(bodyReader).Decode(&body)
if err != nil {
WriteError(w, "could not decode payload to create note", err)
APIError(w, logger, "could not decode payload", err)
return
}
if !h.allowNoEncryption && !body.Encrypted {
WriteError(w, "could not create note", fmt.Errorf("encryption is mandatory"))
APIError(w, logger, "encryption is mandatory", nil)
return
}
if !h.allowClientEncryptionKey && body.EncryptionKey != "" {
WriteError(w, "could not create note", fmt.Errorf("client encryption key is not allowed"))
APIError(w, logger, "client encryption key is not allowed", nil)
return
}
content, err := internal.Decode(body.Content)
if err != nil {
WriteError(w, "could not decode content", err)
APIError(w, logger, "could not decode content", err)
return
}
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)
APIError(w, logger, "could not create note", err)
return
}
@ -90,14 +129,14 @@ func (h *GetNoteHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
note, err := h.db.Get(id)
logger := h.logger.With("handler", "CreateNoteHandler", "note_id", id)
if err != nil {
WriteError(w, "could not get note", err)
APIError(w, logger, "could not find note", err)
} else if note == nil {
w.WriteHeader(http.StatusNotFound)
h.logger.Error("note does not exists", slog.Any("note_id", id))
APIErrorNotFound(w, logger, "note does not exist", err)
} else if note.PasswordHash != nil {
w.WriteHeader(http.StatusBadRequest)
h.logger.Error("note is password protected", slog.Any("note_id", note.ID))
APIErrorBadRequest(w, logger, "note is password protected", err)
} else {
if note.Encrypted {
w.Header().Set("Content-Type", "application/octet-stream")
@ -124,20 +163,22 @@ func (h *GetProtectedNoteHandler) ServeHTTP(w http.ResponseWriter, r *http.Reque
vars := mux.Vars(r)
id := vars["id"]
logger := h.logger.With("handler", "GetProtectedNoteHandler", "note_id", id)
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)
APIError(w, logger, "could not decode payload", err)
return
}
note, err := h.db.Get(id)
if err != nil {
WriteError(w, "could not get note", err)
APIError(w, logger, "could not find note", err)
return
} else if note == nil {
w.WriteHeader(http.StatusNotFound)
@ -147,15 +188,15 @@ func (h *GetProtectedNoteHandler) ServeHTTP(w http.ResponseWriter, r *http.Reque
if body.EncryptionKey != "" && note.Encrypted {
note.Content, err = internal.Decrypt(note.Content, body.EncryptionKey)
if err != nil {
WriteError(w, "could not decrypt note", err)
APIError(w, logger, "could not decrypt note", err)
return
}
}
if body.Password != "" && (note.PasswordHash != nil || len(note.PasswordHash) > 0) {
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)
APIError(w, logger, "could not validate password", err)
return
}
}