From df86f772b40b386bff03d05e89a9db1351abaf6b Mon Sep 17 00:00:00 2001 From: oppiliappan Date: Mon, 27 Apr 2026 09:07:58 +0100 Subject: [PATCH] knotserver: limit request size Signed-off-by: oppiliappan --- knotserver/config/config.go | 1 + knotserver/xrpc/create_repo.go | 4 +-- knotserver/xrpc/list_keys.go | 2 +- knotserver/xrpc/owner.go | 2 +- knotserver/xrpc/repo_blob.go | 4 +-- knotserver/xrpc/repo_branch.go | 2 +- knotserver/xrpc/repo_branches.go | 2 +- knotserver/xrpc/repo_compare.go | 2 +- knotserver/xrpc/repo_diff.go | 2 +- knotserver/xrpc/repo_get_default_branch.go | 2 +- knotserver/xrpc/repo_languages.go | 2 +- knotserver/xrpc/repo_log.go | 2 +- knotserver/xrpc/repo_tag.go | 2 +- knotserver/xrpc/repo_tags.go | 2 +- knotserver/xrpc/repo_tree.go | 2 +- knotserver/xrpc/version.go | 2 +- knotserver/xrpc/xrpc.go | 33 +++++++++++++++++++--- nix/modules/knot.nix | 7 +++++ xrpc/errors/errors.go | 5 ++++ 19 files changed, 59 insertions(+), 21 deletions(-) diff --git a/knotserver/config/config.go b/knotserver/config/config.go index efa2c166..a276f714 100644 --- a/knotserver/config/config.go +++ b/knotserver/config/config.go @@ -23,6 +23,7 @@ type Server struct { JetstreamEndpoint string `env:"JETSTREAM_ENDPOINT, default=wss://jetstream1.us-west.bsky.network/subscribe"` Owner string `env:"OWNER, required"` LogDids bool `env:"LOG_DIDS, default=true"` + MaxResponseKB int `env:"MAX_RESPONSE_KB, default=5120"` // This disables signature verification so use with caution. Dev bool `env:"DEV, default=false"` diff --git a/knotserver/xrpc/create_repo.go b/knotserver/xrpc/create_repo.go index 813d7de2..f742b8fe 100644 --- a/knotserver/xrpc/create_repo.go +++ b/knotserver/xrpc/create_repo.go @@ -109,7 +109,7 @@ func (h *Xrpc) CreateRepo(w http.ResponseWriter, r *http.Request) { if _, statErr := os.Stat(didRepoPath); statErr == nil { l.Info("repo already exists from previous attempt", "repoDid", existingDid) output := tangled.RepoCreate_Output{RepoDid: &existingDid} - writeJson(w, &output) + h.writeJson(w, &output) return } l.Warn("stale repo key found without directory, cleaning up", "repoDid", existingDid) @@ -250,7 +250,7 @@ func (h *Xrpc) CreateRepo(w http.ResponseWriter, r *http.Request) { } }() - writeJson(w, &tangled.RepoCreate_Output{RepoDid: &repoDid}) + h.writeJson(w, &tangled.RepoCreate_Output{RepoDid: &repoDid}) } func (h *Xrpc) requestCrawl(ctx context.Context, input *tangled.SyncRequestCrawl_Input) error { diff --git a/knotserver/xrpc/list_keys.go b/knotserver/xrpc/list_keys.go index d3353943..5bc8e205 100644 --- a/knotserver/xrpc/list_keys.go +++ b/knotserver/xrpc/list_keys.go @@ -45,5 +45,5 @@ func (x *Xrpc) ListKeys(w http.ResponseWriter, r *http.Request) { response.Cursor = &nextCursor } - writeJson(w, response) + x.writeJson(w, response) } diff --git a/knotserver/xrpc/owner.go b/knotserver/xrpc/owner.go index b4bbf99c..81e0a758 100644 --- a/knotserver/xrpc/owner.go +++ b/knotserver/xrpc/owner.go @@ -18,5 +18,5 @@ func (x *Xrpc) Owner(w http.ResponseWriter, r *http.Request) { Owner: owner, } - writeJson(w, response) + x.writeJson(w, response) } diff --git a/knotserver/xrpc/repo_blob.go b/knotserver/xrpc/repo_blob.go index 91570348..dcbe8442 100644 --- a/knotserver/xrpc/repo_blob.go +++ b/knotserver/xrpc/repo_blob.go @@ -58,7 +58,7 @@ func (x *Xrpc) RepoBlob(w http.ResponseWriter, r *http.Request) { Branch: &submodule.Branch, }, } - writeJson(w, response) + x.writeJson(w, response) return } @@ -173,7 +173,7 @@ func (x *Xrpc) RepoBlob(w http.ResponseWriter, r *http.Request) { } } - writeJson(w, response) + x.writeJson(w, response) } // isTextualMimeType returns true if the MIME type represents textual content diff --git a/knotserver/xrpc/repo_branch.go b/knotserver/xrpc/repo_branch.go index 7a5273fc..2dda2a17 100644 --- a/knotserver/xrpc/repo_branch.go +++ b/knotserver/xrpc/repo_branch.go @@ -81,5 +81,5 @@ func (x *Xrpc) RepoBranch(w http.ResponseWriter, r *http.Request) { When: commit.Author.When.Format(time.RFC3339), } - writeJson(w, response) + x.writeJson(w, response) } diff --git a/knotserver/xrpc/repo_branches.go b/knotserver/xrpc/repo_branches.go index 973d1880..7acdea67 100644 --- a/knotserver/xrpc/repo_branches.go +++ b/knotserver/xrpc/repo_branches.go @@ -45,5 +45,5 @@ func (x *Xrpc) RepoBranches(w http.ResponseWriter, r *http.Request) { Branches: branches, } - writeJson(w, response) + x.writeJson(w, response) } diff --git a/knotserver/xrpc/repo_compare.go b/knotserver/xrpc/repo_compare.go index 2d4bde31..3b03417f 100644 --- a/knotserver/xrpc/repo_compare.go +++ b/knotserver/xrpc/repo_compare.go @@ -99,5 +99,5 @@ func (x *Xrpc) RepoCompare(w http.ResponseWriter, r *http.Request) { CombinedPatchRaw: combinedPatchRaw, } - writeJson(w, response) + x.writeJson(w, response) } diff --git a/knotserver/xrpc/repo_diff.go b/knotserver/xrpc/repo_diff.go index 84fed659..37768839 100644 --- a/knotserver/xrpc/repo_diff.go +++ b/knotserver/xrpc/repo_diff.go @@ -37,5 +37,5 @@ func (x *Xrpc) RepoDiff(w http.ResponseWriter, r *http.Request) { Diff: diff, } - writeJson(w, response) + x.writeJson(w, response) } diff --git a/knotserver/xrpc/repo_get_default_branch.go b/knotserver/xrpc/repo_get_default_branch.go index 74e6f2cc..c16206d2 100644 --- a/knotserver/xrpc/repo_get_default_branch.go +++ b/knotserver/xrpc/repo_get_default_branch.go @@ -35,5 +35,5 @@ func (x *Xrpc) RepoGetDefaultBranch(w http.ResponseWriter, r *http.Request) { When: time.UnixMicro(0).Format(time.RFC3339), } - writeJson(w, response) + x.writeJson(w, response) } diff --git a/knotserver/xrpc/repo_languages.go b/knotserver/xrpc/repo_languages.go index ffc8681c..86b3396e 100644 --- a/knotserver/xrpc/repo_languages.go +++ b/knotserver/xrpc/repo_languages.go @@ -72,5 +72,5 @@ func (x *Xrpc) RepoLanguages(w http.ResponseWriter, r *http.Request) { response.TotalFiles = &totalFiles } - writeJson(w, response) + x.writeJson(w, response) } diff --git a/knotserver/xrpc/repo_log.go b/knotserver/xrpc/repo_log.go index 30220e34..675140d9 100644 --- a/knotserver/xrpc/repo_log.go +++ b/knotserver/xrpc/repo_log.go @@ -82,5 +82,5 @@ func (x *Xrpc) RepoLog(w http.ResponseWriter, r *http.Request) { response.Log = true - writeJson(w, response) + x.writeJson(w, response) } diff --git a/knotserver/xrpc/repo_tag.go b/knotserver/xrpc/repo_tag.go index 3d7c2e34..1ace1d41 100644 --- a/knotserver/xrpc/repo_tag.go +++ b/knotserver/xrpc/repo_tag.go @@ -81,5 +81,5 @@ func (x *Xrpc) RepoTag(w http.ResponseWriter, r *http.Request) { Tag: &tr, } - writeJson(w, response) + x.writeJson(w, response) } diff --git a/knotserver/xrpc/repo_tags.go b/knotserver/xrpc/repo_tags.go index 96c6a621..a3d2aef2 100644 --- a/knotserver/xrpc/repo_tags.go +++ b/knotserver/xrpc/repo_tags.go @@ -75,5 +75,5 @@ func (x *Xrpc) RepoTags(w http.ResponseWriter, r *http.Request) { Tags: rtags, } - writeJson(w, response) + x.writeJson(w, response) } diff --git a/knotserver/xrpc/repo_tree.go b/knotserver/xrpc/repo_tree.go index cb2cb929..296b5a62 100644 --- a/knotserver/xrpc/repo_tree.go +++ b/knotserver/xrpc/repo_tree.go @@ -142,5 +142,5 @@ func (x *Xrpc) RepoTree(w http.ResponseWriter, r *http.Request) { } } - writeJson(w, response) + x.writeJson(w, response) } diff --git a/knotserver/xrpc/version.go b/knotserver/xrpc/version.go index cbdcafef..89fa9623 100644 --- a/knotserver/xrpc/version.go +++ b/knotserver/xrpc/version.go @@ -56,5 +56,5 @@ func (x *Xrpc) Version(w http.ResponseWriter, r *http.Request) { Version: version, } - writeJson(w, response) + x.writeJson(w, response) } diff --git a/knotserver/xrpc/xrpc.go b/knotserver/xrpc/xrpc.go index b96656f4..27921d6d 100644 --- a/knotserver/xrpc/xrpc.go +++ b/knotserver/xrpc/xrpc.go @@ -1,7 +1,9 @@ package xrpc import ( + "bytes" "encoding/json" + "errors" "log/slog" "net/http" "os" @@ -122,10 +124,33 @@ func writeError(w http.ResponseWriter, e xrpcerr.XrpcError, status int) { json.NewEncoder(w).Encode(e) } -func writeJson(w http.ResponseWriter, response any) { - w.Header().Set("Content-Type", "application/json") - if err := json.NewEncoder(w).Encode(response); err != nil { - writeError(w, xrpcerr.GenericError(err), http.StatusInternalServerError) +type limitWriter struct { + buf bytes.Buffer + limit int + written int +} + +var errResponseTooLarge = errors.New("response too large") + +func (lw *limitWriter) Write(p []byte) (int, error) { + if lw.written+len(p) > lw.limit { + return 0, errResponseTooLarge + } + n, err := lw.buf.Write(p) + lw.written += n + return n, err +} + +func (x *Xrpc) writeJson(w http.ResponseWriter, response any) { + lw := &limitWriter{limit: x.Config.Server.MaxResponseKB * 1024} + if err := json.NewEncoder(lw).Encode(response); err != nil { + if errors.Is(err, errResponseTooLarge) { + writeError(w, xrpcerr.RequestTooLargeError, http.StatusRequestEntityTooLarge) + } else { + writeError(w, xrpcerr.GenericError(err), http.StatusInternalServerError) + } return } + w.Header().Set("Content-Type", "application/json") + w.Write(lw.buf.Bytes()) } diff --git a/nix/modules/knot.nix b/nix/modules/knot.nix index 269448ca..6f7464d1 100644 --- a/nix/modules/knot.nix +++ b/nix/modules/knot.nix @@ -177,6 +177,12 @@ in default = false; description = "Enable development mode (disables signature verification)"; }; + + maxResponseKB = mkOption { + type = types.int; + default = 5120; + description = "Maximum response size in kilobytes"; + }; }; }; }; @@ -282,6 +288,7 @@ in then "true" else "false" }" + "KNOT_SERVER_MAX_RESPONSE_KB=${toString cfg.server.maxResponseKB}" ]; ExecStart = "${cfg.package}/bin/knot server"; Restart = "always"; diff --git a/xrpc/errors/errors.go b/xrpc/errors/errors.go index de508002..9d5ace82 100644 --- a/xrpc/errors/errors.go +++ b/xrpc/errors/errors.go @@ -66,6 +66,11 @@ var RefNotFoundError = NewXrpcError( WithMessage("failed to access ref"), ) +var RequestTooLargeError = NewXrpcError( + WithTag("RequestTooLarge"), + WithMessage("request was too large"), +) + var AuthError = func(err error) XrpcError { return NewXrpcError( WithTag("Auth"), -- 2.51.2