349 lines
9.3 KiB
Go
349 lines
9.3 KiB
Go
package gemini
|
|
|
|
import (
|
|
"bufio"
|
|
"bytes"
|
|
"errors"
|
|
"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.
|
|
//
|
|
// It is not guaranteed to be readable more than once.
|
|
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,
|
|
}
|
|
}
|
|
|
|
// 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) (*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 &Response{
|
|
Status: Status(status),
|
|
Meta: string(hdrLine[3:]),
|
|
Body: bufrdr,
|
|
}, nil
|
|
}
|
|
|
|
// 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
|
|
}
|