This repository has been archived on 2023-05-01. You can view files and clone it, but cannot push or open issues or pull requests.
gus/gemini/response.go

332 lines
9.1 KiB
Go

package gemini
import (
"bufio"
"bytes"
"errors"
"io"
"strconv"
"sync"
"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
}
func NewResponseReader(response *gus.Response) gus.ResponseReader {
return &responseReader{
Response: response,
once: &sync.Once{},
}
}
type responseReader struct {
*gus.Response
reader io.Reader
once *sync.Once
}
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() {
rdr.once.Do(func() {
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
}