From ff05d62013906f3086b452bfeda3e0d5b9b7a541 Mon Sep 17 00:00:00 2001 From: tjpcc Date: Mon, 9 Jan 2023 16:40:24 -0700 Subject: Initial commit. some basics: - minimal README - some TODOs - server and request handler framework - contribs: file serving, request logging - server examples - CI setup --- gemini/response.go | 308 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 308 insertions(+) create mode 100644 gemini/response.go (limited to 'gemini/response.go') diff --git a/gemini/response.go b/gemini/response.go new file mode 100644 index 0000000..90340a5 --- /dev/null +++ b/gemini/response.go @@ -0,0 +1,308 @@ +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 +} -- cgit v1.2.3