-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathresponse.go
More file actions
266 lines (229 loc) · 8.48 KB
/
Copy pathresponse.go
File metadata and controls
266 lines (229 loc) · 8.48 KB
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
package compass
import (
"encoding/json"
"fmt"
"net/http"
"net/url"
"os"
"regexp"
"strings"
)
var asciiFallback = regexp.MustCompile(`[^A-Za-z0-9 ._-]+`)
type Response struct {
internalError bool
cookies []Cookie
ContentType *string
Body []byte
StatusCode int
Headers map[string]string
}
// SetCookie attaches a cookie to the response.
//
// Multiple calls are allowed. Each cookie results in its own Set-Cookie
// header. cookies are written before the response body.
func (r *Response) SetCookie(c Cookie) {
r.cookies = append(r.cookies, c)
}
// RemoveCookie expires a cookie by name, which makes the browser delete it.
//
// It sets Max-Age to -1 and clears the value. Path defaults to "/" as with
// all cookies, so it will correctly expire cookies set with the default path.
func (r *Response) RemoveCookie(name string) {
r.SetCookie(Cookie{Name: name, Value: "", MaxAge: -1})
}
// SetSession attaches a special session tracking cookie to the response.
//
// This ensures that future calls can properly read the session. You must run
// this method if you want to keep the session in future request.
func (r *Response) SetSession(session *Session) {
r.SetCookie(session.cookie())
}
// writeCookies writes a list of cookies as Set-Cookie headers.
//
// Set-Cookie is the one HTTP header that repeats, so each
// cookie gets its own header line rather than being joined with commas.
//
// Logs a warning if a cookie has SameSiteNone but (Secure == false).
//
// This is called by writeResponse before the status code is written.
func (s *Server) writeCookies(w http.ResponseWriter, cookies []Cookie) {
for _, c := range cookies {
if c.SameSite == SameSiteNone && !c.Secure {
s.Logger.Warn(fmt.Sprintf("Writing cookie %q with SameSiteNone, but Secure is false. Client browser will reject this cookie!", c.Name))
}
w.Header().Add("Set-Cookie", c.toHeader())
}
}
// Raw creates a basic Response with full control over its contents.
//
// It sets the body, status code, and optional content type. Headers
// are initialized as an empty map and can be modified after creation.
func Raw(contentType *string, body []byte, code int) Response {
return Response{
internalError: false,
cookies: make([]Cookie, 0),
ContentType: contentType,
Body: body,
StatusCode: code,
Headers: make(map[string]string),
}
}
// InternalError creates a Response that signals an internal server error.
//
// This does not directly write a response to the client. Instead, it marks
// the response as an internal error so the server can handle it separately.
func InternalError(message string) Response {
return Response{
internalError: true,
ContentType: nil,
Body: []byte(message),
StatusCode: 500,
Headers: make(map[string]string),
}
}
// Text creates a plain text response with status code 200.
func Text(text string) Response {
return TextWithCode(text, 200)
}
// TextWithCode creates a plain text response with a custom status code.
func TextWithCode(text string, code int) Response {
return Raw(nil, []byte(text), code)
}
// HTML creates a html text response with status code 200.
func HTML(content string) Response {
return HTMLWithCode(content, 200)
}
// HTMLWithCode creates a html text response with a custom status code.
func HTMLWithCode(content string, code int) Response {
typ := "text/html"
return Raw(&typ, []byte(content), code)
}
// JsonString creates a JSON response from a raw string with status code 200.
//
// The string is assumed to already be valid JSON.
func JsonString(content string) Response {
return JsonStringWithCode(content, 200)
}
// JsonStringWithCode creates a JSON response from a raw string
// with a custom status code.
//
// The string is assumed to already be valid JSON.
func JsonStringWithCode(content string, code int) Response {
typ := "application/json"
return Raw(&typ, []byte(content), code)
}
// JsonMarshal converts an object to JSON and returns it as a response.
//
// If marshalling fails, an InternalError response is returned instead.
func JsonMarshal(obj any) Response {
return JsonMarshalWithCode(obj, 200)
}
// JsonMarshalWithCode converts an object to JSON and returns it
// with a custom status code.
//
// If marshalling fails, an InternalError response is returned instead.
func JsonMarshalWithCode(obj any, code int) Response {
content, err := json.Marshal(obj)
if err != nil {
return InternalError(fmt.Sprintf("failed to marshal json object: %s", err))
}
return JsonStringWithCode(string(content), code)
}
// DownloadBytes creates a response that forces the client to download data
// as a file with status code 200.
//
// The filename is sanitized for ASCII compatibility while preserving the
// original name for UTF-8 capable clients.
func DownloadBytes(filename string, data []byte) Response {
return DownloadBytesWithCode(filename, data, 200)
}
// DownloadBytesWithCode creates a response that forces the client to download data
// as a file with a custom status code.
//
// The filename is sanitized for ASCII compatibility while preserving the
// original name for UTF-8 capable clients.
func DownloadBytesWithCode(filename string, data []byte, code int) Response {
ascii := strings.TrimSpace(filename)
ascii = asciiFallback.ReplaceAllString(ascii, "_")
if ascii == "" {
ascii = "download"
}
resp := Raw(nil, data, code)
resp.Headers["Content-Disposition"] = `attachment; filename="` + ascii + `"; filename*=UTF-8''` + url.PathEscape(filename)
return resp
}
// DownloadFile reads a file from disk and returns it as a download response.
//
// If the file cannot be read, an internal error response is returned.
func DownloadFile(filename string, path string) Response {
return DownloadFileWithCode(filename, path, 200)
}
// DownloadFileWithCode reads a file from disk and returns it as a download
// response with a custom status code.
//
// If the file cannot be read, an internal error response is returned.
func DownloadFileWithCode(filename string, path string, code int) Response {
data, err := os.ReadFile(path)
if err != nil {
return InternalError(fmt.Sprintf("failed to prepare file data for download: %s", err))
}
return DownloadBytesWithCode(filename, data, code)
}
// Redirect creates a redirect response to the given target.
//
// If retainMethod is true, a 307 redirect is used. Otherwise, a 303
// redirect is used, which converts the request to a GET.
func Redirect(target string, retainMethod bool) Response {
typ := "--COMPASS-redirect"
status := http.StatusSeeOther
if retainMethod {
status = http.StatusTemporaryRedirect
}
return Raw(&typ, []byte(target), status)
}
// PermaRedirect creates a permanent redirect (HTTP 308) to the target URL.
// The method which a request uses is retained.
func PermaRedirect(target string) Response {
typ := "--COMPASS-redirect"
return Raw(&typ, []byte(target), http.StatusPermanentRedirect)
}
// ServeBytes creates a response that serves data as a file-like resource
// with status code 200.
//
// Unlike "download" responses, this does not force a download. The client
// (e.g. browser) may choose to display the content inline if it supports it
// (such as images, text, or PDFs).
func ServeBytes(data []byte, name string) Response {
return ServeBytesWithCode(data, name, 200)
}
// ServeBytesWithCode creates a response that serves data as a file-like
// resource with a custom status code.
//
// Unlike "download" responses, this does not force a download. The client
// (e.g. browser) may choose to display the content inline if it supports it
// (such as images, text, or PDFs).
func ServeBytesWithCode(data []byte, name string, code int) Response {
typ := "--COMPASS-serve"
raw := Raw(&typ, data, code)
raw.Headers["-Compass-File-Name"] = name
return raw
}
// ServeFile reads a file from disk and serves it as a file-like response.
//
// This behaves the same as ServeBytes, but reads the file from disk first.
// If the file cannot be read, an internal error response is returned.
func ServeFile(path string, name string) Response {
return ServeFileWithCode(path, name, 200)
}
// ServeFileWithCode reads a file from disk and serves it as a file-like
// response with a custom status code.
//
// This behaves the same as ServeBytes, but reads the file from disk first.
// If the file cannot be read, an internal error response is returned.
func ServeFileWithCode(path string, name string, code int) Response {
data, err := os.ReadFile(path)
if err != nil {
return InternalError(fmt.Sprintf("failed to prepare file data for serve: %s", err))
}
return ServeBytesWithCode(data, name, code)
}