summaryrefslogtreecommitdiff
path: root/gemini/response.go
blob: 90340a5cc8680a33de0461c2219a8d83514899d2 (plain)
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
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
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
}