aboutsummaryrefslogtreecommitdiff
path: root/response.go
blob: 3cf2f366e2329235c8ad8a70f84395d4ca7e81ae (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
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
package gemini

import (
	"bufio"
	"crypto/tls"
	"io"
	"strconv"
)

// Response is a Gemini response.
type Response struct {
	// Status contains the response status code.
	Status Status

	// Meta contains more information related to the response status.
	// For successful responses, Meta should contain the media type of the response.
	// For failure responses, Meta should contain a short description of the failure.
	// Meta should not be longer than 1024 bytes.
	Meta string

	// Body represents the response body.
	// Body is guaranteed to always be non-nil.
	//
	// The response body is streamed on demand as the Body field is read.
	Body io.ReadCloser

	// TLS contains information about the TLS connection on which the response
	// was received.
	TLS tls.ConnectionState
}

// ReadResponse reads a Gemini response from the provided io.ReadCloser.
func ReadResponse(rc io.ReadCloser) (*Response, error) {
	resp := &Response{}
	br := bufio.NewReader(rc)

	// Read the status
	statusB := make([]byte, 2)
	if _, err := br.Read(statusB); err != nil {
		return nil, err
	}
	status, err := strconv.Atoi(string(statusB))
	if err != nil {
		return nil, err
	}
	resp.Status = Status(status)

	// Disregard invalid status codes
	const minStatus, maxStatus = 1, 6
	statusClass := resp.Status.Class()
	if statusClass < minStatus || statusClass > maxStatus {
		return nil, ErrInvalidResponse
	}

	// Read one space
	if b, err := br.ReadByte(); err != nil {
		return nil, err
	} else if b != ' ' {
		return nil, ErrInvalidResponse
	}

	// Read the meta
	meta, err := br.ReadString('\r')
	if err != nil {
		return nil, err
	}
	// Trim carriage return
	meta = meta[:len(meta)-1]
	// Ensure meta is less than or equal to 1024 bytes
	if len(meta) > 1024 {
		return nil, ErrInvalidResponse
	}
	// Default mime type of text/gemini; charset=utf-8
	if statusClass == StatusClassSuccess && meta == "" {
		meta = "text/gemini; charset=utf-8"
	}
	resp.Meta = meta

	// Read terminating newline
	if b, err := br.ReadByte(); err != nil {
		return nil, err
	} else if b != '\n' {
		return nil, ErrInvalidResponse
	}

	if resp.Status.Class() == StatusClassSuccess {
		resp.Body = newReadCloserBody(br, rc)
	} else {
		resp.Body = nopReadCloser{}
		rc.Close()
	}
	return resp, nil
}

type nopReadCloser struct{}

func (nopReadCloser) Read(p []byte) (int, error) {
	return 0, io.EOF
}

func (nopReadCloser) Close() error {
	return nil
}

type readCloserBody struct {
	br *bufio.Reader // used until empty
	io.ReadCloser
}

func newReadCloserBody(br *bufio.Reader, rc io.ReadCloser) io.ReadCloser {
	body := &readCloserBody{ReadCloser: rc}
	if br.Buffered() != 0 {
		body.br = br
	}
	return body
}

func (b *readCloserBody) Read(p []byte) (n int, err error) {
	if b.br != nil {
		if n := b.br.Buffered(); len(p) > n {
			p = p[:n]
		}
		n, err = b.br.Read(p)
		if b.br.Buffered() == 0 {
			b.br = nil
		}
		return n, err
	}
	return b.ReadCloser.Read(p)
}

// ResponseWriter is used to construct a Gemini response.
type ResponseWriter struct {
	b           *bufio.Writer
	status      Status
	meta        string
	wroteHeader bool
	bodyAllowed bool
}

// NewResponseWriter returns a ResponseWriter that uses the provided io.Writer.
func NewResponseWriter(w io.Writer) *ResponseWriter {
	return &ResponseWriter{
		b: bufio.NewWriter(w),
	}
}

// Header sets the response header.
func (w *ResponseWriter) Header(status Status, meta string) {
	w.status = status
	w.meta = meta
}

// Status sets the response status code.
// It also sets the response meta to status.Meta().
func (w *ResponseWriter) Status(status Status) {
	w.status = status
	w.meta = status.Meta()
}

// Meta sets the response meta.
//
// For successful responses, meta should contain the media type of the response.
// For failure responses, meta should contain a short description of the failure.
// The response meta should not be greater than 1024 bytes.
func (w *ResponseWriter) Meta(meta string) {
	w.meta = meta
}

// Write writes data to the connection as part of the response body.
// If the response status does not allow for a response body, Write returns
// ErrBodyNotAllowed.
//
// Write writes the response header if it has not already been written.
// It writes a successful status code if one is not set.
func (w *ResponseWriter) Write(b []byte) (int, error) {
	if !w.wroteHeader {
		w.writeHeader(StatusSuccess)
	}
	if !w.bodyAllowed {
		return 0, ErrBodyNotAllowed
	}
	return w.b.Write(b)
}

func (w *ResponseWriter) writeHeader(defaultStatus Status) {
	status := w.status
	if status == 0 {
		status = defaultStatus
	}

	meta := w.meta
	if status.Class() == StatusClassSuccess {
		w.bodyAllowed = true

		if meta == "" {
			meta = "text/gemini"
		}
	}

	w.b.WriteString(strconv.Itoa(int(status)))
	w.b.WriteByte(' ')
	w.b.WriteString(meta)
	w.b.Write(crlf)
	w.wroteHeader = true
}

// Flush writes any buffered data to the underlying io.Writer.
//
// Flush writes the response header if it has not already been written.
// It writes a failure status code if one is not set.
func (w *ResponseWriter) Flush() error {
	if !w.wroteHeader {
		w.writeHeader(StatusTemporaryFailure)
	}
	// Write errors from writeHeader will be returned here.
	return w.b.Flush()
}