package gemini import ( "bytes" "io" "strconv" ) // StatusCategory represents the various types of responses. type StatusCategory int const ( // StatusCategoryInput is for responses which request additional input. // // The META line will be the prompt to display to the user. StatusCategoryInput StatusCategory = iota*10 + 10 // StatusCategorySuccess 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. StatusCategorySuccess // StatusCategoryRedirect is for responses which direct the client to an alternative URL. // // The META line will contain the new URL the client should try. StatusCategoryRedirect // StatusCategoryTemporaryFailure is for responses which indicate a transient server-side failure. // // The META line may contain a line with more information about the error. StatusCategoryTemporaryFailure // StatusCategoryPermanentFailure is for permanent failure responses. // // The META line may contain a line with more information about the error. StatusCategoryPermanentFailure // StatusCategoryCertificateRequired indicates client certificate related issues. // // The META line may contain a line with more information about the error. StatusCategoryCertificateRequired ) // Status is the integer status code of a gemini response. type Status int const ( // StatusInput indicates a required query parameter at the requested URL. StatusInput Status = Status(StatusCategoryInput) + iota // StatusSensitiveInput indicates a sensitive query parameter is required. StatusSensitiveInput ) const ( // StatusSuccess is a successful response. StatusSuccess = Status(StatusCategorySuccess) + iota ) const ( // StatusTemporaryRedirect indicates a temporary redirect to another URL. StatusTemporaryRedirect = Status(StatusCategoryRedirect) + 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 = Status(StatusCategoryTemporaryFailure) + 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 = Status(StatusCategoryPermanentFailure) + 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 = Status(StatusCategoryPermanentFailure) + 9 ) const ( // StatusClientCertificateRequired is returned when a certificate was required but not provided. StatusClientCertificateRequired = Status(StatusCategoryCertificateRequired) + iota // StatusCertificateNotAuthorized means the certificate doesn't grant access to the requested resource. StatusCertificateNotAuthorized // StatusCertificateNotValid means the provided client certificate is invalid. StatusCertificateNotValid ) // StatusCategory returns the category a specific status belongs to. func (s Status) Category() StatusCategory { return StatusCategory(s / 10) } // Response contains everything in a gemini protocol response. type Response struct { // Status is the status code of the response. Status Status // Meta is the status-specific line of additional information. Meta string // Body is the response body, if any. Body io.Reader reader io.Reader } // Input builds an input-prompting response. func Input(prompt string) *Response { return &Response{ Status: StatusInput, Meta: prompt, } } // SensitiveInput builds a password-prompting response. func SensitiveInput(prompt string) *Response { return &Response{ Status: StatusSensitiveInput, Meta: prompt, } } // Success builds a success response with resource body. func Success(mediatype string, body io.Reader) *Response { return &Response{ Status: StatusSuccess, Meta: mediatype, Body: body, } } // Redirect builds a redirect response. func Redirect(url string) *Response { return &Response{ Status: StatusTemporaryRedirect, Meta: url, } } // PermanentRedirect builds a response with a permanent redirect. func PermanentRedirect(url string) *Response { return &Response{ Status: StatusPermanentRedirect, Meta: url, } } // Failure builds a temporary failure response from an error. func Failure(err error) *Response { return &Response{ Status: StatusTemporaryFailure, Meta: err.Error(), } } // Unavailable build a "server unavailable" response. func Unavailable(msg string) *Response { return &Response{ Status: StatusServerUnavailable, Meta: msg, } } // CGIError builds a "cgi error" response. func CGIError(err string) *Response { return &Response{ Status: StatusCGIError, Meta: err, } } // ProxyError builds a proxy error response. func ProxyError(msg string) *Response { return &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) *Response { return &Response{ Status: StatusSlowDown, Meta: strconv.Itoa(seconds), } } // PermanentFailure builds a "permanent failure" from an error. func PermanentFailure(err error) *Response { return &Response{ Status: StatusPermanentFailure, Meta: err.Error(), } } // NotFound builds a "resource not found" response. func NotFound(msg string) *Response { return &Response{ Status: StatusNotFound, Meta: msg, } } // Gone builds a "resource gone" response. func Gone(msg string) *Response { return &Response{ Status: StatusGone, Meta: msg, } } // RefuseProxy builds a "proxy request refused" response. func RefuseProxy(msg string) *Response { return &Response{ Status: StatusProxyRequestRefused, Meta: msg, } } // BadRequest builds a "bad request" response. func BadRequest(msg string) *Response { return &Response{ Status: StatusBadRequest, Meta: msg, } } // RequireCert builds a "client certificate required" response. func RequireCert(msg string) *Response { return &Response{ Status: StatusClientCertificateRequired, Meta: msg, } } // CertAuthFailure builds a "certificate not authorized" response. func CertAuthFailure(msg string) *Response { return &Response{ Status: StatusCertificateNotAuthorized, Meta: msg, } } // CertInvalid builds a "client certificate not valid" response. func CertInvalid(msg string) *Response { return &Response{ Status: StatusCertificateNotValid, Meta: msg, } } // Read implements io.Reader for Response. func (r *Response) Read(b []byte) (int, error) { r.ensureReader() return r.reader.Read(b) } // WriteTo implements io.WriterTo for Response. func (r *Response) WriteTo(dst io.Writer) (int64, error) { r.ensureReader() return r.reader.(io.WriterTo).WriteTo(dst) } // Close implements io.Closer and ensures the body gets closed. func (r *Response) Close() error { if r != nil { if cl, ok := r.Body.(io.Closer); ok { return cl.Close() } } return nil } func (r *Response) ensureReader() { if r.reader != nil { return } hdr := bytes.NewBuffer(r.headerLine()) if r.Body != nil { r.reader = io.MultiReader(hdr, r.Body) } else { r.reader = hdr } } func (r Response) headerLine() []byte { buf := make([]byte, len(r.Meta)+5) _ = strconv.AppendInt(buf[:0], int64(r.Status), 10) buf[2] = ' ' copy(buf[3:], r.Meta) buf[len(buf)-2] = '\r' buf[len(buf)-1] = '\n' return buf }