summaryrefslogtreecommitdiff
path: root/gemini/response.go
diff options
context:
space:
mode:
authortjpcc <tjp@ctrl-c.club>2023-01-09 16:40:24 -0700
committertjpcc <tjp@ctrl-c.club>2023-01-09 16:40:24 -0700
commitff05d62013906f3086b452bfeda3e0d5b9b7a541 (patch)
tree3be29de0b1bc7c273041c6d89b71ca447c940556 /gemini/response.go
Initial commit.
some basics: - minimal README - some TODOs - server and request handler framework - contribs: file serving, request logging - server examples - CI setup
Diffstat (limited to 'gemini/response.go')
-rw-r--r--gemini/response.go308
1 files changed, 308 insertions, 0 deletions
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
+}