diff options
| author | T <t@tjp.lol> | 2025-04-03 10:00:40 -0600 |
|---|---|---|
| committer | t <t@tjp.lol> | 2026-07-01 10:47:36 -0600 |
| commit | 748a806adc5a325e63ca6883b2aa9bf15da90513 (patch) | |
| tree | a60cf2e32370f9e2cf194e8b52bcb2af3ed6a120 /gopher/request.go | |
| parent | 74bbf67ebbd1325ff699a052933ad94bc0401c58 (diff) | |
Reject oversized or malformed Gopher selectors as parse errors instead of panicking. Keep search requests bounded, and wrap shared server goroutines with panic logging so one bad connection cannot take down the process.
Diffstat (limited to 'gopher/request.go')
| -rw-r--r-- | gopher/request.go | 50 |
1 files changed, 35 insertions, 15 deletions
diff --git a/gopher/request.go b/gopher/request.go index fdd7480..3aeaf74 100644 --- a/gopher/request.go +++ b/gopher/request.go @@ -11,6 +11,11 @@ import ( "tildegit.org/tjp/sliderule/internal/types" ) +const ( + maxSelectorLen = 255 + maxRequestLen = 4096 +) + // ParseRequest parses a gopher protocol request into a sliderule.Request object. func ParseRequest(rdr io.Reader) (*types.Request, error) { selector, search, err := readFullRequest(rdr) @@ -34,9 +39,7 @@ func ParseRequest(rdr io.Reader) (*types.Request, error) { } func readFullRequest(rdr io.Reader) (string, string, error) { - // The vast majority of requests will fit in this size: - // the specified 255 byte max for selector, then CRLF. - buf := make([]byte, 257) + buf := make([]byte, maxSelectorLen+len("\r\n")) n, err := rdr.Read(buf) if err != nil && !errors.Is(err, io.EOF) { @@ -44,29 +47,46 @@ func readFullRequest(rdr io.Reader) (string, string, error) { } buf = buf[:n] - // Full-text search transactions are the exception, they - // may be longer because there is an additional search string - if n == 257 && buf[256] != '\n' { - intake := buf[n:cap(buf)] - total := n + if lineEnd := bytes.IndexByte(buf, '\n'); lineEnd >= 0 { + buf = buf[:lineEnd+1] + } else if tab := bytes.IndexByte(buf, '\t'); tab >= 0 { + // Full-text search transactions append TAB and a search string. + // RFC 1436 still caps the selector at 255 bytes, but gives no + // search-string limit, so keep a small implementation cap. for { - intake = append(intake, 0) - intake = intake[:cap(intake)] + if len(buf) >= maxRequestLen { + return "", "", errors.New("gopher request too long") + } + intake := make([]byte, maxRequestLen-len(buf)) n, err = rdr.Read(intake) if err != nil && err != io.EOF { return "", "", err } - total += n - if n < cap(intake) || intake[cap(intake)-1] == '\n' { + buf = append(buf, intake[:n]...) + if lineEnd := bytes.IndexByte(intake[:n], '\n'); lineEnd >= 0 { + buf = buf[:len(buf)-n+lineEnd+1] + break + } + if err == io.EOF { break } - intake = intake[n:] } - buf = buf[:total] + } else if len(buf) > maxSelectorLen { + return "", "", errors.New("gopher selector too long") } selector, search, _ := bytes.Cut(buf, []byte{'\t'}) - return strings.TrimRight(string(selector), "\r\n"), strings.TrimRight(string(search), "\r\n"), nil + selector = bytes.TrimRight(selector, "\r\n") + search = bytes.TrimRight(search, "\r\n") + + if len(selector) > maxSelectorLen { + return "", "", errors.New("gopher selector too long") + } + if bytes.ContainsAny(selector, "\x00\r\n") || bytes.ContainsAny(search, "\x00\t\r\n") { + return "", "", errors.New("malformed gopher request") + } + + return string(selector), string(search), nil } |
