package gemini import ( "bufio" "bytes" "errors" "io" "strconv" "tildegit.org/tjp/gus" ) // ResponseCategory represents the various types of gemini responses. type ResponseCategory int const ( // ResponseCategoryInput is for responses which request additional input. // // The META line will be the prompt to display to the user. ResponseCategoryInput ResponseCategory = iota*10 + 10 // ResponseCategorySuccess is for successful responses. // // The META line will be the resource's mime type. // This is the only response status which indicates the presence of a response body, // and it will contain the resource itself. ResponseCategorySuccess // ResponseCategoryRedirect is for responses which direct the client to an alternative URL. // // The META line will contain the new URL the client should try. ResponseCategoryRedirect // ResponseCategoryTemporaryFailure is for responses which indicate a transient server-side failure. // // The META line may contain a line with more information about the error. ResponseCategoryTemporaryFailure // ResponseCategoryPermanentFailure is for permanent failure responses. // // The META line may contain a line with more information about the error. ResponseCategoryPermanentFailure // ResponseCategoryCertificateRequired indicates client certificate related issues. // // The META line may contain a line with more information about the error. ResponseCategoryCertificateRequired ) func ResponseCategoryForStatus(status gus.Status) ResponseCategory { return ResponseCategory(status / 10) } const ( // StatusInput indicates a required query parameter at the requested URL. StatusInput gus.Status = gus.Status(ResponseCategoryInput) + iota // StatusSensitiveInput indicates a sensitive query parameter is required. StatusSensitiveInput ) const ( // StatusSuccess is a successful response. StatusSuccess = gus.Status(ResponseCategorySuccess) + iota ) const ( // StatusTemporaryRedirect indicates a temporary redirect to another URL. StatusTemporaryRedirect = gus.Status(ResponseCategoryRedirect) + iota // StatusPermanentRedirect indicates that the resource should always be requested at the new URL. StatusPermanentRedirect ) const ( // StatusTemporaryFailure indicates that the request failed and there is no response body. StatusTemporaryFailure = gus.Status(ResponseCategoryTemporaryFailure) + iota // StatusServerUnavailable occurs when the server is unavailable due to overload or maintenance. StatusServerUnavailable // StatusCGIError is the result of a failure of a CGI script. StatusCGIError // StatusProxyError indicates that the server is acting as a proxy and the outbound request failed. StatusProxyError // StatusSlowDown tells the client that rate limiting is in effect. // // Unlike other statuses in this category, the META line is an integer indicating how // many more seconds the client must wait before sending another request. StatusSlowDown ) const ( // StatusPermanentFailure is a server failure which should be expected to continue indefinitely. StatusPermanentFailure = gus.Status(ResponseCategoryPermanentFailure) + iota // StatusNotFound means the resource doesn't exist but it may in the future. StatusNotFound // StatusGone occurs when a resource will not be available any longer. StatusGone // StatusProxyRequestRefused means the server is unwilling to act as a proxy for the resource. StatusProxyRequestRefused // StatusBadRequest indicates that the request was malformed somehow. StatusBadRequest = gus.Status(ResponseCategoryPermanentFailure) + 9 ) const ( // StatusClientCertificateRequired is returned when a certificate was required but not provided. StatusClientCertificateRequired = gus.Status(ResponseCategoryCertificateRequired) + iota // StatusCertificateNotAuthorized means the certificate doesn't grant access to the requested resource. StatusCertificateNotAuthorized // StatusCertificateNotValid means the provided client certificate is invalid. StatusCertificateNotValid ) // Input builds an input-prompting response. func Input(prompt string) *gus.Response { return &gus.Response{ Status: StatusInput, Meta: prompt, } } // SensitiveInput builds a password-prompting response. func SensitiveInput(prompt string) *gus.Response { return &gus.Response{ Status: StatusSensitiveInput, Meta: prompt, } } // Success builds a success response with resource body. func Success(mediatype string, body io.Reader) *gus.Response { return &gus.Response{ Status: StatusSuccess, Meta: mediatype, Body: body, } } // Redirect builds a redirect response. func Redirect(url string) *gus.Response { return &gus.Response{ Status: StatusTemporaryRedirect, Meta: url, } } // PermanentRedirect builds a response with a permanent redirect. func PermanentRedirect(url string) *gus.Response { return &gus.Response{ Status: StatusPermanentRedirect, Meta: url, } } // Failure builds a temporary failure response from an error. func Failure(err error) *gus.Response { return &gus.Response{ Status: StatusTemporaryFailure, Meta: err.Error(), } } // Unavailable build a "server unavailable" response. func Unavailable(msg string) *gus.Response { return &gus.Response{ Status: StatusServerUnavailable, Meta: msg, } } // CGIError builds a "cgi error" response. func CGIError(err string) *gus.Response { return &gus.Response{ Status: StatusCGIError, Meta: err, } } // ProxyError builds a proxy error response. func ProxyError(msg string) *gus.Response { return &gus.Response{ Status: StatusProxyError, Meta: msg, } } // SlowDown builds a "slow down" response with the number of seconds until the resource is available. func SlowDown(seconds int) *gus.Response { return &gus.Response{ Status: StatusSlowDown, Meta: strconv.Itoa(seconds), } } // PermanentFailure builds a "permanent failure" from an error. func PermanentFailure(err error) *gus.Response { return &gus.Response{ Status: StatusPermanentFailure, Meta: err.Error(), } } // NotFound builds a "resource not found" response. func NotFound(msg string) *gus.Response { return &gus.Response{ Status: StatusNotFound, Meta: msg, } } // Gone builds a "resource gone" response. func Gone(msg string) *gus.Response { return &gus.Response{ Status: StatusGone, Meta: msg, } } // RefuseProxy builds a "proxy request refused" response. func RefuseProxy(msg string) *gus.Response { return &gus.Response{ Status: StatusProxyRequestRefused, Meta: msg, } } // BadRequest builds a "bad request" response. func BadRequest(msg string) *gus.Response { return &gus.Response{ Status: StatusBadRequest, Meta: msg, } } // RequireCert builds a "client certificate required" response. func RequireCert(msg string) *gus.Response { return &gus.Response{ Status: StatusClientCertificateRequired, Meta: msg, } } // CertAuthFailure builds a "certificate not authorized" response. func CertAuthFailure(msg string) *gus.Response { return &gus.Response{ Status: StatusCertificateNotAuthorized, Meta: msg, } } // CertInvalid builds a "client certificate not valid" response. func CertInvalid(msg string) *gus.Response { return &gus.Response{ Status: StatusCertificateNotValid, Meta: msg, } } // InvalidResponseLineEnding indicates that a gemini response header didn't end with "\r\n". var InvalidResponseLineEnding = errors.New("Invalid response line ending.") // InvalidResponseHeaderLine indicates a malformed gemini response header line. var InvalidResponseHeaderLine = errors.New("Invalid response header line.") // ParseResponse parses a complete gemini response from a reader. // // The reader must contain only one gemini response. func ParseResponse(rdr io.Reader) (*gus.Response, error) { bufrdr := bufio.NewReader(rdr) hdrLine, err := bufrdr.ReadBytes('\n') if err != nil { return nil, InvalidResponseLineEnding } if hdrLine[len(hdrLine)-2] != '\r' { return nil, InvalidResponseLineEnding } if hdrLine[2] != ' ' { return nil, InvalidResponseHeaderLine } hdrLine = hdrLine[:len(hdrLine)-2] status, err := strconv.Atoi(string(hdrLine[:2])) if err != nil { return nil, InvalidResponseHeaderLine } return &gus.Response{ Status: gus.Status(status), Meta: string(hdrLine[3:]), Body: bufrdr, }, nil } type ResponseReader interface { io.Reader io.WriterTo io.Closer } func NewResponseReader(response *gus.Response) ResponseReader { return &responseReader{ Response: response } } type responseReader struct { *gus.Response reader io.Reader } func (rdr *responseReader) Read(b []byte) (int, error) { rdr.ensureReader() return rdr.reader.Read(b) } func (rdr *responseReader) WriteTo(dst io.Writer) (int64, error) { rdr.ensureReader() return rdr.reader.(io.WriterTo).WriteTo(dst) } func (rdr *responseReader) ensureReader() { if rdr.reader != nil { return } hdr := bytes.NewBuffer(rdr.headerLine()) if rdr.Body != nil { rdr.reader = io.MultiReader(hdr, rdr.Body) } else { rdr.reader = hdr } } func (rdr responseReader) headerLine() []byte { meta := rdr.Meta.(string) buf := make([]byte, len(meta)+5) _ = strconv.AppendInt(buf[:0], int64(rdr.Status), 10) buf[2] = ' ' copy(buf[3:], meta) buf[len(buf)-2] = '\r' buf[len(buf)-1] = '\n' return buf }