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
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
|
package gemini
import (
"bufio"
"bytes"
"errors"
"fmt"
"io"
"strconv"
"sync"
"tildegit.org/tjp/sliderule/internal/types"
)
// 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 types.Status) ResponseCategory {
return ResponseCategory((status / 10) * 10)
}
const (
// StatusInput indicates a required query parameter at the requested URL.
StatusInput types.Status = types.Status(ResponseCategoryInput) + iota
// StatusSensitiveInput indicates a sensitive query parameter is required.
StatusSensitiveInput
)
const (
// StatusSuccess is a successful response.
StatusSuccess = types.Status(ResponseCategorySuccess) + iota
)
const (
// StatusTemporaryRedirect indicates a temporary redirect to another URL.
StatusTemporaryRedirect = types.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 = types.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 = types.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 = types.Status(ResponseCategoryPermanentFailure) + 9
)
const (
// StatusClientCertificateRequired is returned when a certificate was required but not provided.
StatusClientCertificateRequired = types.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) *types.Response {
return &types.Response{
Status: StatusInput,
Meta: prompt,
}
}
// SensitiveInput builds a password-prompting response.
func SensitiveInput(prompt string) *types.Response {
return &types.Response{
Status: StatusSensitiveInput,
Meta: prompt,
}
}
// Success builds a success response with resource body.
func Success(mediatype string, body io.Reader) *types.Response {
return &types.Response{
Status: StatusSuccess,
Meta: mediatype,
Body: body,
}
}
// Redirect builds a redirect response.
func Redirect(url string) *types.Response {
return &types.Response{
Status: StatusTemporaryRedirect,
Meta: url,
}
}
// PermanentRedirect builds a response with a permanent redirect.
func PermanentRedirect(url string) *types.Response {
return &types.Response{
Status: StatusPermanentRedirect,
Meta: url,
}
}
// Failure builds a temporary failure response from an error.
func Failure(err error) *types.Response {
return &types.Response{
Status: StatusTemporaryFailure,
Meta: err.Error(),
}
}
// Unavailable build a "server unavailable" response.
func Unavailable(msg string) *types.Response {
return &types.Response{
Status: StatusServerUnavailable,
Meta: msg,
}
}
// CGIError builds a "cgi error" response.
func CGIError(err string) *types.Response {
return &types.Response{
Status: StatusCGIError,
Meta: err,
}
}
// ProxyError builds a proxy error response.
func ProxyError(msg string) *types.Response {
return &types.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) *types.Response {
return &types.Response{
Status: StatusSlowDown,
Meta: strconv.Itoa(seconds),
}
}
// PermanentFailure builds a "permanent failure" from an error.
func PermanentFailure(err error) *types.Response {
return &types.Response{
Status: StatusPermanentFailure,
Meta: err.Error(),
}
}
// NotFound builds a "resource not found" response.
func NotFound(msg string) *types.Response {
return &types.Response{
Status: StatusNotFound,
Meta: msg,
}
}
// Gone builds a "resource gone" response.
func Gone(msg string) *types.Response {
return &types.Response{
Status: StatusGone,
Meta: msg,
}
}
// RefuseProxy builds a "proxy request refused" response.
func RefuseProxy(msg string) *types.Response {
return &types.Response{
Status: StatusProxyRequestRefused,
Meta: msg,
}
}
// BadRequest builds a "bad request" response.
func BadRequest(msg string) *types.Response {
return &types.Response{
Status: StatusBadRequest,
Meta: msg,
}
}
// RequireCert builds a "client certificate required" response.
func RequireCert(msg string) *types.Response {
return &types.Response{
Status: StatusClientCertificateRequired,
Meta: msg,
}
}
// CertAuthFailure builds a "certificate not authorized" response.
func CertAuthFailure(msg string) *types.Response {
return &types.Response{
Status: StatusCertificateNotAuthorized,
Meta: msg,
}
}
// CertInvalid builds a "client certificate not valid" response.
func CertInvalid(msg string) *types.Response {
return &types.Response{
Status: StatusCertificateNotValid,
Meta: msg,
}
}
func StatusName(status types.Status) string {
switch status {
case StatusInput:
return "StatusInput"
case StatusSensitiveInput:
return "StatusSensitiveInput"
case StatusSuccess:
return "StatusSuccess"
case StatusTemporaryRedirect:
return "StatusTemporaryRedirect"
case StatusPermanentRedirect:
return "StatusPermanentRedirect"
case StatusTemporaryFailure:
return "StatusTemporaryFailure"
case StatusServerUnavailable:
return "StatusServerUnavailable"
case StatusCGIError:
return "StatusCGIError"
case StatusProxyError:
return "StatusProxyError"
case StatusSlowDown:
return "StatusSlowDown"
case StatusPermanentFailure:
return "StatusPermanentFailure"
case StatusNotFound:
return "StatusNotFound"
case StatusGone:
return "StatusGone"
case StatusProxyRequestRefused:
return "StatusProxyRequestRefused"
case StatusBadRequest:
return "StatusBadRequest"
case StatusClientCertificateRequired:
return "StatusClientCertificateRequired"
case StatusCertificateNotAuthorized:
return "StatusCertificateNotAuthorized"
case StatusCertificateNotValid:
return "StatusCertificateNotValid"
default:
return fmt.Sprintf("Unknown (%d)", status)
}
}
// ErrInvalidResponseLineEnding indicates that a gemini response header didn't end with "\r\n".
var ErrInvalidResponseLineEnding = errors.New("invalid response line ending")
// ErrInvalidResponseHeaderLine indicates a malformed gemini response header line.
var ErrInvalidResponseHeaderLine = 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) (*types.Response, error) {
bufrdr := bufio.NewReader(rdr)
hdrLine, err := bufrdr.ReadBytes('\n')
if err != nil {
return nil, ErrInvalidResponseLineEnding
}
if hdrLine[len(hdrLine)-2] != '\r' {
return nil, ErrInvalidResponseLineEnding
}
if hdrLine[2] != ' ' {
return nil, ErrInvalidResponseHeaderLine
}
hdrLine = hdrLine[:len(hdrLine)-2]
status, err := strconv.Atoi(string(hdrLine[:2]))
if err != nil {
return nil, ErrInvalidResponseHeaderLine
}
return &types.Response{
Status: types.Status(status),
Meta: string(hdrLine[3:]),
Body: bufrdr,
}, nil
}
func NewResponseReader(response *types.Response) types.ResponseReader {
return &responseReader{
Response: response,
once: &sync.Once{},
}
}
type responseReader struct {
*types.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
}
|