forked from emad-elsaid/xlog
-
Notifications
You must be signed in to change notification settings - Fork 0
/
server.go
222 lines (189 loc) · 6.37 KB
/
server.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
package xlog
import (
"crypto/rand"
"encoding/json"
"flag"
"fmt"
"net/http"
"os"
"time"
"github.com/gorilla/csrf"
)
const xCSRF_COOKIE_NAME = "xlog_csrf"
var (
bindAddress string
serveInsecure bool
router = http.NewServeMux()
// a function that renders CSRF hidden input field
CSRF = csrf.TemplateField
)
type (
// alias http.ResponseWriter for shorter handler declaration
Response = http.ResponseWriter
// alias *http.Request for shorter handler declaration
Request = *http.Request
// alias of http.HandlerFunc as output is expected from defined http handlers
Output = http.HandlerFunc
// map of string to any value used for template rendering
Locals map[string]any // passed to templates
)
func defaultMiddlewares(readonly bool) (middlewares []func(http.Handler) http.Handler) {
if !readonly {
crsfOpts := []csrf.Option{
csrf.Path("/"),
csrf.FieldName("csrf"),
csrf.CookieName(xCSRF_COOKIE_NAME),
csrf.Secure(!serveInsecure),
}
sessionSecret := []byte(os.Getenv("SESSION_SECRET"))
if len(sessionSecret) == 0 {
sessionSecret = make([]byte, 128)
rand.Read(sessionSecret)
}
middlewares = append(middlewares,
methodOverrideHandler,
csrf.Protect(sessionSecret, crsfOpts...))
}
middlewares = append(middlewares, requestLoggerHandler)
return
}
func init() {
flag.StringVar(&bindAddress, "bind", "127.0.0.1:3000", "IP and port to bind the web server to")
flag.BoolVar(&serveInsecure, "serve-insecure", false, "Accept http connections and forward crsf cookie over non secure connections")
}
func server() *http.Server {
compileTemplates()
var handler http.Handler = router
for _, v := range defaultMiddlewares(READONLY) {
handler = v(handler)
}
return &http.Server{
Handler: handler,
Addr: bindAddress,
WriteTimeout: 15 * time.Second,
ReadTimeout: 15 * time.Second,
}
}
// HandlerFunc is the type of an HTTP handler function + returns output function.
// it makes it easier to return the output directly instead of writing the output to w then return.
type HandlerFunc func(Response, Request) Output
func handlerFuncToHttpHandler(handler HandlerFunc) http.HandlerFunc {
return func(w Response, r Request) {
handler(w, r)(w, r)
}
}
// NotFound returns an output function that writes 404 NotFound to http response
func NotFound(msg string) Output {
return func(w Response, r Request) {
http.Error(w, "", http.StatusNotFound)
}
}
// BadRequest returns an output function that writes BadRequest http response
func BadRequest(msg string) Output {
return func(w Response, r Request) {
http.Error(w, msg, http.StatusBadRequest)
}
}
// Unauthorized returns an output function that writes Unauthorized http response
func Unauthorized(msg string) Output {
return func(w Response, r Request) {
http.Error(w, "", http.StatusUnauthorized)
}
}
// InternalServerError returns an output function that writes InternalServerError http response
func InternalServerError(err error) Output {
return func(w Response, r Request) {
http.Error(w, err.Error(), http.StatusInternalServerError)
}
}
// Redirect returns an output function that writes Found http response to provided URL
func Redirect(url string) Output {
return func(w Response, r Request) {
http.Redirect(w, r, url, http.StatusFound)
}
}
// NoContent returns an output function that writes NoContent http status
func NoContent() Output {
return func(w Response, r Request) {
w.WriteHeader(http.StatusNoContent)
}
}
// Noop is an output that doesn't do anything to the request. can be useful for a websocket upgrader
func Noop(w Response, r Request) {}
// PlainText returns an output function that writes text to response writer
func PlainText(text string) Output {
return func(w Response, r Request) {
w.Write([]byte(text))
}
}
func JsonResponse(a any) Output {
return func(w Response, r Request) {
b, err := json.Marshal(a)
if err != nil {
w.Write([]byte(err.Error()))
return
}
w.Write(b)
}
}
// Get defines a new route that gets executed when the request matches path and
// method is http Get. the list of middlewares are executed in order
func Get(path string, handler HandlerFunc, middlewares ...func(http.HandlerFunc) http.HandlerFunc) {
defer timing("GET", "path", path, "func", FuncName(handler))()
router.HandleFunc("GET "+path,
applyMiddlewares(handlerFuncToHttpHandler(handler), middlewares...),
)
}
// Post defines a new route that gets executed when the request matches path and
// method is http Post. the list of middlewares are executed in order
func Post(path string, handler HandlerFunc, middlewares ...func(http.HandlerFunc) http.HandlerFunc) {
defer timing("POST", "path", path, "func", FuncName(handler))()
router.HandleFunc("POST "+path,
applyMiddlewares(handlerFuncToHttpHandler(handler), middlewares...),
)
}
// Delete defines a new route that gets executed when the request matches path and
// method is http Delete. the list of middlewares are executed in order
func Delete(path string, handler HandlerFunc, middlewares ...func(http.HandlerFunc) http.HandlerFunc) {
defer timing("DELETE", "path", path, "func", FuncName(handler))()
router.HandleFunc("DELETE "+path,
applyMiddlewares(handlerFuncToHttpHandler(handler), middlewares...),
)
}
// Render returns an output function that renders partial with data and writes it as response
func Render(path string, data Locals) Output {
return func(w Response, r Request) {
fmt.Fprint(w, Partial(path, data))
}
}
func applyMiddlewares(handler http.HandlerFunc, middlewares ...func(http.HandlerFunc) http.HandlerFunc) http.HandlerFunc {
for i := len(middlewares) - 1; i >= 0; i-- {
handler = middlewares[i](handler)
}
return handler
}
// Derived from Gorilla middleware https://github.com/gorilla/handlers/blob/v1.5.1/handlers.go#L134
func methodOverrideHandler(h http.Handler) http.Handler {
return http.HandlerFunc(func(w Response, r Request) {
if r.Method == "POST" {
om := r.FormValue("_method")
if om == "PUT" || om == "PATCH" || om == "DELETE" {
r.Method = om
}
}
h.ServeHTTP(w, r)
})
}
func requestLoggerHandler(h http.Handler) http.Handler {
return http.HandlerFunc(func(w Response, r Request) {
defer timing(r.Method + " " + r.URL.Path)()
h.ServeHTTP(w, r)
})
}
// Cache wraps Output and adds header to instruct the browser to cache the output
func Cache(out Output) Output {
return func(w Response, r Request) {
w.Header().Add("Cache-Control", "max-age=604800")
out(w, r)
}
}