-
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathhttp_easy.go
259 lines (229 loc) · 8.27 KB
/
http_easy.go
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
package web
import (
"fmt"
"io"
"net/http"
"runtime/debug"
"strconv"
"strings"
"time"
"github.com/ecnepsnai/web/router"
)
// HTTPEasy describes a simple to use HTTP router. HTTPEasy handles are expected to return a reader and specify the
// content type and length themselves.
//
// HTTP abstracts many features away from the caller, providing a simpler experience when a only a simple HTTP server
// is needed. If you require more control, use the HTTP router.
//
// The HTTPEasy server supports HTTP range requests, should the client request it and the application provide a
// supported Reader [io.ReadSeekCloser].
type HTTPEasy struct {
server *Server
}
// Static registers a GET and HEAD handle for all requests under path to serve any files matching the directory.
//
// For example:
//
// directory = /usr/share/www/
// path = /static/
//
// Request for '/static/image.jpg' would read file '/usr/share/www/image.jpg'
//
// Will panic if any handle is registered under path. Attempting to register a new handle under path after calling
// Static will panic.
//
// Caching will be enabled by default for all files served by this router. The mtime of the file will be used for the
// Last-Modified date.
//
// By default, the server will use the file extension (if any) to determine the MIME type for the response.
func (h HTTPEasy) Static(path string, directory string) {
log.PDebug("Serving files from directory", map[string]interface{}{
"directory": directory,
"path": path,
})
h.server.router.ServeFiles(directory, path)
}
// GET register a new HTTP GET request handle
func (h HTTPEasy) GET(path string, handle HTTPEasyHandle, options HandleOptions) {
h.registerHTTPEasyEndpoint("GET", path, handle, options)
}
// HEAD register a new HTTP HEAD request handle
func (h HTTPEasy) HEAD(path string, handle HTTPEasyHandle, options HandleOptions) {
h.registerHTTPEasyEndpoint("HEAD", path, handle, options)
}
// GETHEAD registers both a HTTP GET and HTTP HEAD request handle. Equal to calling HTTPEasy.GET and HTTPEasy.HEAD.
//
// Handle responses can always return a reader, it will automatically be ignored for HEAD requests.
func (h HTTPEasy) GETHEAD(path string, handle HTTPEasyHandle, options HandleOptions) {
h.registerHTTPEasyEndpoint("GET", path, handle, options)
h.registerHTTPEasyEndpoint("HEAD", path, handle, options)
}
// OPTIONS register a new HTTP OPTIONS request handle
func (h HTTPEasy) OPTIONS(path string, handle HTTPEasyHandle, options HandleOptions) {
h.registerHTTPEasyEndpoint("OPTIONS", path, handle, options)
}
// POST register a new HTTP POST request handle
func (h HTTPEasy) POST(path string, handle HTTPEasyHandle, options HandleOptions) {
h.registerHTTPEasyEndpoint("POST", path, handle, options)
}
// PUT register a new HTTP PUT request handle
func (h HTTPEasy) PUT(path string, handle HTTPEasyHandle, options HandleOptions) {
h.registerHTTPEasyEndpoint("PUT", path, handle, options)
}
// PATCH register a new HTTP PATCH request handle
func (h HTTPEasy) PATCH(path string, handle HTTPEasyHandle, options HandleOptions) {
h.registerHTTPEasyEndpoint("PATCH", path, handle, options)
}
// DELETE register a new HTTP DELETE request handle
func (h HTTPEasy) DELETE(path string, handle HTTPEasyHandle, options HandleOptions) {
h.registerHTTPEasyEndpoint("DELETE", path, handle, options)
}
func (h HTTPEasy) registerHTTPEasyEndpoint(method string, path string, handle HTTPEasyHandle, options HandleOptions) {
log.PDebug("Register HTTP endpoint", map[string]interface{}{
"method": method,
"path": path,
})
h.server.router.Handle(method, path, h.httpPreHandle(handle, options))
}
func (h HTTPEasy) httpPreHandle(endpointHandle HTTPEasyHandle, options HandleOptions) router.Handle {
return func(w http.ResponseWriter, request router.Request) {
if options.PreHandle != nil {
if err := options.PreHandle(w, request.HTTP); err != nil {
return
}
}
if h.server.isRateLimited(w, request.HTTP) {
return
}
if options.MaxBodyLength > 0 {
// We don't need to worry about this not being a number. Go's own HTTP server
// won't respond to requests like these
length, _ := strconv.ParseUint(request.HTTP.Header.Get("Content-Length"), 10, 64)
if length > options.MaxBodyLength {
log.PError("Rejecting HTTP request with oversized body", map[string]interface{}{
"body_length": length,
"max_length": options.MaxBodyLength,
})
w.WriteHeader(413)
return
}
}
if options.AuthenticateMethod != nil {
userData := options.AuthenticateMethod(request.HTTP)
if isUserdataNil(userData) {
if options.UnauthorizedMethod == nil {
log.PWarn("Rejected request to authenticated HTTP endpoint", map[string]interface{}{
"url": request.HTTP.URL,
"method": request.HTTP.Method,
"remote_addr": RealRemoteAddr(request.HTTP),
})
w.Header().Set("Content-Type", "text/html")
w.WriteHeader(http.StatusUnauthorized)
w.Write([]byte("<html><head><title>Unauthorized</title></head><body><h1>Unauthorized</h1></body></html>"))
return
}
options.UnauthorizedMethod(w, request.HTTP)
} else {
h.httpPostHandle(endpointHandle, userData, options)(w, request)
}
return
}
h.httpPostHandle(endpointHandle, nil, options)(w, request)
}
}
func (h HTTPEasy) httpPostHandle(endpointHandle HTTPEasyHandle, userData interface{}, options HandleOptions) router.Handle {
return func(w http.ResponseWriter, r router.Request) {
request := Request{
HTTP: r.HTTP,
Parameters: r.Parameters,
UserData: userData,
}
start := time.Now()
defer func() {
if p := recover(); p != nil {
log.PError("Recovered from panic during HTTPEasy handle", map[string]interface{}{
"error": fmt.Sprintf("%v", p),
"route": request.HTTP.URL.Path,
"method": request.HTTP.Method,
"stack": string(debug.Stack()),
})
w.WriteHeader(500)
}
}()
response := endpointHandle(request)
elapsed := time.Since(start)
if response.Reader != nil {
defer response.Reader.Close()
}
// Return a HTTP range response only if:
// 1. A range was actually requested by the client
// 2. The reader implemented Seek
// 3. The response was either default or 200
ranges := router.ParseRangeHeader(r.HTTP.Header.Get("range"))
_, canSeek := response.Reader.(io.ReadSeekCloser)
if len(ranges) > 0 && (response.Status == 0 || response.Status == 200) && !h.server.Options.IgnoreHTTPRangeRequests && canSeek {
router.ServeHTTPRange(router.ServeHTTPRangeOptions{
Headers: response.Headers,
Ranges: ranges,
Reader: response.Reader.(io.ReadSeekCloser),
TotalLength: response.ContentLength,
MIMEType: response.ContentType,
Writer: w,
})
log.PWrite(h.server.Options.RequestLogLevel, "HTTP Request", map[string]interface{}{
"remote_addr": RealRemoteAddr(r.HTTP),
"method": r.HTTP.Method,
"url": r.HTTP.URL,
"elapsed": elapsed.String(),
"status": response.Status,
"range": true,
})
return
}
if canSeek && !h.server.Options.IgnoreHTTPRangeRequests {
w.Header().Set("Accept-Ranges", "bytes")
}
if len(response.ContentType) == 0 {
w.Header().Set("Content-Type", "text/html; charset=utf-8")
} else {
w.Header().Set("Content-Type", response.ContentType)
}
if response.ContentLength > 0 {
w.Header().Set("Content-Length", fmt.Sprintf("%d", response.ContentLength))
}
for k, v := range response.Headers {
w.Header().Set(k, v)
}
for _, cookie := range response.Cookies {
http.SetCookie(w, &cookie)
}
code := 200
if response.Status != 0 {
code = response.Status
}
if !options.DontLogRequests {
log.PWrite(h.server.Options.RequestLogLevel, "HTTP Request", map[string]interface{}{
"remote_addr": RealRemoteAddr(r.HTTP),
"method": r.HTTP.Method,
"url": r.HTTP.URL,
"elapsed": elapsed.String(),
"status": code,
})
}
w.WriteHeader(code)
if r.HTTP.Method != "HEAD" && response.Reader != nil {
if copied, err := io.Copy(w, response.Reader); err != nil {
if strings.Contains(err.Error(), "write: broken pipe") {
return
}
log.PError("Error writing response data", map[string]interface{}{
"method": r.HTTP.Method,
"url": r.HTTP.URL,
"wrote": copied,
"error": err.Error(),
})
return
}
}
}
}