aboutsummaryrefslogtreecommitdiff
path: root/gemini.go
blob: c39474e44b0dc56333453cdc31d8ea8fa9e290e4 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
// Package gemini implements the Gemini protocol.
package gemini

import (
	"crypto/tls"
	"errors"
	"io"
	"net/url"
	"strconv"
)

// Status codes.
const (
	StatusInput                     = 10
	StatusSensitiveInput            = 11
	StatusSuccess                   = 20
	StatusRedirectTemporary         = 30
	StatusRedirectPermanent         = 31
	StatusTemporaryFailure          = 40
	StatusServerUnavailable         = 41
	StatusCGIError                  = 42
	StatusProxyError                = 43
	StatusSlowDown                  = 44
	StatusPermanentFailure          = 50
	StatusNotFound                  = 51
	StatusGone                      = 52
	StatusProxyRequestRefused       = 53
	StatusBadRequest                = 59
	StatusClientCertificateRequired = 60
	StatusCertificateNotAuthorised  = 61
	StatusCertificateNotValid       = 62
)

// Status code categories.
const (
	StatusClassInput                     = 1
	StatusClassSuccess                   = 2
	StatusClassRedirect                  = 3
	StatusClassTemporaryFailure          = 4
	StatusClassPermanentFailure          = 5
	StatusClassClientCertificateRequired = 6
)

// Errors.
var (
	ErrProtocol   = errors.New("Protocol error")
	ErrInvalidURL = errors.New("Invalid URL")
)

// Request is a Gemini request.
//
// A Request can optionally be configured with a client certificate. Example:
//
//     req := NewRequest(url)
//     cert, err := tls.LoadX509KeyPair("client.crt", "client.key")
//     if err != nil {
//         panic(err)
//     }
//     req.Certificates = append(req.Certificates, cert)
//
type Request struct {
	Host         string            // host or host:port
	URL          *url.URL          // the requested URL
	Certificates []tls.Certificate // client certificates
}

// NewRequest returns a new request. The host is inferred from the provided url.
func NewRequest(rawurl string) (*Request, error) {
	u, err := url.Parse(rawurl)
	if err != nil {
		return nil, err
	}

	// UserInfo is invalid
	if u.User != nil {
		return nil, ErrInvalidURL
	}

	return &Request{
		Host: u.Host,
		URL:  u,
	}, nil
}

// NewProxyRequest returns a new request using the provided host.
func NewProxyRequest(host, rawurl string) (*Request, error) {
	u, err := url.Parse(rawurl)
	if err != nil {
		return nil, err
	}

	// UserInfo is invalid
	if u.User != nil {
		return nil, ErrInvalidURL
	}

	return &Request{
		Host: host,
		URL:  u,
	}, nil
}

// Write writes the Gemini request to the provided io.Writer.
func (r *Request) Write(w io.Writer) error {
	request := r.URL.String() + "\r\n"
	_, err := w.Write([]byte(request))
	return err
}

// Response is a Gemini response.
type Response struct {
	Status int
	Meta   string
	Body   []byte
}

// Write writes the Gemini response header and body to the provided io.Writer.
func (r *Response) Write(w io.Writer) error {
	header := strconv.Itoa(r.Status) + " " + r.Meta + "\r\n"
	if _, err := w.Write([]byte(header)); err != nil {
		return err
	}

	// Only write the response body on success
	if r.Status/10 == StatusClassSuccess {
		if _, err := w.Write(r.Body); err != nil {
			return err
		}
	}

	return nil
}