package gopher import ( "bytes" "errors" "io" "net/url" "path" "strings" "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) if err != nil { return nil, err } if !strings.HasPrefix(selector, "/") { selector = "/" + selector } return &types.Request{ URL: &url.URL{ Scheme: "gopher", Path: path.Clean(selector), OmitHost: true, //nolint:typecheck // (for some reason typecheck on drone-ci doesn't realize OmitHost is a field in url.URL) RawQuery: url.QueryEscape(search), }, }, nil } func readFullRequest(rdr io.Reader) (string, string, error) { buf := make([]byte, maxSelectorLen+len("\r\n")) n, err := rdr.Read(buf) if err != nil && !errors.Is(err, io.EOF) { return "", "", err } buf = buf[: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 { 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 } 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 } } } else if len(buf) > maxSelectorLen { return "", "", errors.New("gopher selector too long") } selector, search, _ := bytes.Cut(buf, []byte{'\t'}) 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 }