-
Notifications
You must be signed in to change notification settings - Fork 15
/
responses.odin
196 lines (155 loc) · 5.69 KB
/
responses.odin
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
package http
import "core:bytes"
import "core:encoding/json"
import "core:io"
import "core:log"
import "core:os"
import "core:path/filepath"
import "core:strings"
import "nbio"
// Sets the response to one that sends the given HTML.
respond_html :: proc(r: ^Response, html: string, status: Status = .OK, loc := #caller_location) {
r.status = status
headers_set_content_type(&r.headers, mime_to_content_type(Mime_Type.Html))
body_set(r, html, loc)
respond(r, loc)
}
// Sets the response to one that sends the given plain text.
respond_plain :: proc(r: ^Response, text: string, status: Status = .OK, loc := #caller_location) {
r.status = status
headers_set_content_type(&r.headers, mime_to_content_type(Mime_Type.Plain))
body_set(r, text, loc)
respond(r, loc)
}
@(private)
ENOENT :: os.ERROR_FILE_NOT_FOUND when ODIN_OS == .Windows else os.ENOENT
/*
Sends the content of the file at the given path as the response.
This procedure uses non blocking IO and only allocates the size of the file in the body's buffer,
no other allocations or temporary buffers, this is to make it as fast as possible.
The content type is taken from the path, optionally overwritten using the parameter.
If the file doesn't exist, a 404 response is sent.
If any other error occurs, a 500 is sent and the error is logged.
*/
respond_file :: proc(r: ^Response, path: string, content_type: Maybe(Mime_Type) = nil, loc := #caller_location) {
// PERF: we are still putting the content into the body buffer, we could stream it.
assert_has_td(loc)
assert(!r.sent, "response has already been sent", loc)
io := &td.io
handle, errno := nbio.open(io, path)
if errno != os.ERROR_NONE {
if errno == ENOENT {
log.debugf("respond_file, open %q, no such file or directory", path)
} else {
log.warnf("respond_file, open %q error: %i", path, errno)
}
respond(r, Status.Not_Found)
return
}
size, err := nbio.seek(io, handle, 0, .End)
if err != os.ERROR_NONE {
log.errorf("Could not seek the file size of file at %q, error number: %i", path, err)
respond(r, Status.Internal_Server_Error)
nbio.close(io, handle)
return
}
mime := mime_from_extension(path)
content_type := mime_to_content_type(mime)
headers_set_content_type(&r.headers, content_type)
_response_write_heading(r, size)
bytes.buffer_grow(&r._buf, size)
buf := _dynamic_unwritten(r._buf.buf)[:size]
on_read :: proc(user: rawptr, read: int, err: os.Errno) {
r := cast(^Response)user
handle := os.Handle(uintptr(context.user_ptr))
_dynamic_add_len(&r._buf.buf, read)
if err != os.ERROR_NONE {
log.errorf("Reading file from respond_file failed, error number: %i", err)
respond(r, Status.Internal_Server_Error)
nbio.close(&td.io, handle)
return
}
respond(r, Status.OK)
nbio.close(&td.io, handle)
}
// Using the context.user_ptr to point to the file handle.
context.user_ptr = rawptr(uintptr(handle))
nbio.read_at_all(io, handle, 0, buf, r, on_read)
}
/*
Responds with the given content, determining content type from the given path.
This is very useful when you want to `#load(path)` at compile time and respond with that.
*/
respond_file_content :: proc(r: ^Response, path: string, content: []byte, status: Status = .OK, loc := #caller_location) {
mime := mime_from_extension(path)
content_type := mime_to_content_type(mime)
r.status = status
headers_set_content_type(&r.headers, content_type)
body_set(r, content, loc)
respond(r, loc)
}
/*
Sets the response to one that, based on the request path, returns a file.
base: The base of the request path that should be removed when retrieving the file.
target: The path to the directory to serve.
request: The request path.
Path traversal is detected and cleaned up.
The Content-Type is set based on the file extension, see the MimeType enum for known file extensions.
*/
respond_dir :: proc(r: ^Response, base, target, request: string, loc := #caller_location) {
if !strings.has_prefix(request, base) {
respond(r, Status.Not_Found)
return
}
// Detect path traversal attacks.
req_clean := filepath.clean(request, context.temp_allocator)
base_clean := filepath.clean(base, context.temp_allocator)
if !strings.has_prefix(req_clean, base_clean) {
respond(r, Status.Not_Found)
return
}
file_path := filepath.join([]string{"./", target, strings.trim_prefix(req_clean, base_clean)}, context.temp_allocator)
respond_file(r, file_path, loc = loc)
}
// Sets the response to one that returns the JSON representation of the given value.
respond_json :: proc(r: ^Response, v: any, status: Status = .OK, opt: json.Marshal_Options = {}, loc := #caller_location) -> (err: json.Marshal_Error) {
opt := opt
r.status = status
headers_set_content_type(&r.headers, mime_to_content_type(Mime_Type.Json))
// Going to write a MINIMUM of 128 bytes at a time.
rw: Response_Writer
buf: [128]byte
response_writer_init(&rw, r, buf[:])
// Ends the body and sends the response.
defer io.close(rw.w)
if err = json.marshal_to_writer(rw.w, v, &opt); err != nil {
headers_set_close(&r.headers)
response_status(r, .Internal_Server_Error)
}
return
}
/*
Prefer the procedure group `respond`.
*/
respond_with_none :: proc(r: ^Response, loc := #caller_location) {
assert_has_td(loc)
conn := r._conn
req := conn.loop.req
// Respond as head request if we set it to get.
if rline, ok := req.line.(Requestline); ok && req.is_head && conn.server.opts.redirect_head_to_get {
rline.method = .Head
}
response_send(r, conn, loc)
}
/*
Prefer the procedure group `respond`.
*/
respond_with_status :: proc(r: ^Response, status: Status, loc := #caller_location) {
response_status(r, status)
respond(r, loc)
}
// Sends the response back to the client, handlers should call this.
respond :: proc {
respond_with_none,
respond_with_status,
}