├── .gitignore
├── LICENSE
├── README.md
├── context.go
├── default.go
├── examples
├── markdown-render
│ ├── books
│ │ ├── one-hundred-years-of-solitude.md
│ │ ├── the-great-gatsby.md
│ │ ├── the-house-of-spirits.md
│ │ ├── the-myth-of-sisyphus.md
│ │ └── to-the-lighthouse.md
│ └── main.go
├── static-site
│ ├── main.go
│ └── www
│ │ ├── alice
│ │ └── index.html
│ │ ├── bob
│ │ └── index.html
│ │ ├── e404.html
│ │ ├── index.html
│ │ ├── main.js
│ │ └── style.css
└── todos
│ └── main.go
├── go.mod
├── go.sum
├── middlewares.go
├── mux.go
├── response.go
├── serve.go
├── session.go
├── session
├── pool.go
├── pool_memory.go
├── session.go
└── sid.go
└── writer.go
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | .vscode
3 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2018-present Je Xia
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy of
6 | this software and associated documentation files (the "Software"), to deal in
7 | the Software without restriction, including without limitation the rights to
8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
9 | the Software, and to permit persons to whom the Software is furnished to do so,
10 | subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
21 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # REX
2 |
3 | [](https://godoc.org/github.com/ije/rex)
4 | [](https://goreportcard.com/report/github.com/ije/rex)
5 | [](./LICENSE)
6 |
7 | Yet another web framework in Go.
8 |
9 | ## Installation
10 |
11 | ```bash
12 | go get -u github.com/ije/rex
13 | ```
14 |
15 | ## Usage
16 |
17 | ```go
18 | package main
19 |
20 | import (
21 | "log"
22 | "github.com/ije/rex"
23 | )
24 |
25 | func main() {
26 | // use middlewares
27 | rex.Use(
28 | rex.Logger(log.Default()),
29 | rex.Cors(rex.CorsAll()),
30 | rex.Compress(),
31 | )
32 |
33 | // match "GET /" route
34 | rex.GET("/{$}", func(ctx *rex.Context) any {
35 | return rex.Render(
36 | rex.Tpl(" By {{.Author}}My Blog
{{range .}}
"),
37 | posts.List(),
38 | )
39 | })
40 |
41 | // match "GET /posts/:id" route
42 | rex.GET("/posts/{id}", func(ctx *rex.Context) any {
43 | post, ok := posts.Get(ctx.PathValue("id"))
44 | if !ok {
45 | return rex.Err(404, "post not found")
46 | }
47 | return post
48 | })
49 |
50 | // match "POST /posts" route
51 | rex.POST("/posts", func(ctx *rex.Context) any {
52 | return posts.Add(ctx.FormValue("title"), ctx.FormValue("author"), ctx.FormValue("content"))
53 | })
54 |
55 | // match "DELETE /posts/:id" route
56 | rex.DELETE("/posts/{id}", func(ctx *rex.Context) any {
57 | ok := posts.Delete(ctx.PathValue("id"))
58 | return ok
59 | })
60 |
61 | // Starts the server
62 | <-rex.Start(80)
63 |
64 | // Starts the server with autoTLS
65 | <-rex.StartWithAutoTLS(443)
66 | }
67 | ```
68 |
69 | More usages please check [examples/](./examples).
70 |
71 | ## Middleware
72 |
73 | In **REX**, a middleware is a function that receives a `*rex.Context` and returns a `any`. If the returned value is not `rex.Next()`, the middleware will return the value to the client, or continue to execute the next middleware.
74 |
75 | ```go
76 | rex.Use(func(ctx *rex.Context) any {
77 | if ctx.Pathname() == "/hello" {
78 | // return a html response
79 | return rex.HTML("hello world
")
80 | }
81 |
82 | // use next handler
83 | return ctx.Next()
84 | })
85 | ```
86 |
87 | ## Routing
88 |
89 | **REX** uses [ServeMux Patterns](https://pkg.go.dev/net/http#ServeMux) (requires Go 1.22+) to define routes.
90 |
91 | Patterns can match the method, host and path of a request. Some examples:
92 |
93 | - `/index.html` matches the path `/index.html` for any host and method.
94 | - `GET /static/` matches a GET request whose path begins with `/static/`.
95 | - `example.com/` matches any request to the host `example.com`.
96 | - `example.com/{$}` matches requests with host `example.com` and path `/`.
97 | - `/b/{bucket}/o/{objectname...}` matches paths whose first segment is `b` and whose third segment is `o`. The name `bucket` denotes the second segment and `objectname` denotes the remainder of the path.
98 |
99 | In general, a pattern looks like:
100 | ```
101 | [METHOD ][HOST]/[PATH]
102 | ```
103 |
104 | You can access the path params via calling `ctx.PathValue(paramName)`:
105 |
106 | ```go
107 | rex.GET("/posts/{id}", func(ctx *rex.Context) any {
108 | return fmt.Sprintf("ID is %s", ctx.PathValue("id"))
109 | })
110 | ```
111 |
--------------------------------------------------------------------------------
/context.go:
--------------------------------------------------------------------------------
1 | package rex
2 |
3 | import (
4 | "bytes"
5 | "compress/gzip"
6 | "encoding/json"
7 | "fmt"
8 | "io"
9 | "mime"
10 | "mime/multipart"
11 | "net/http"
12 | "net/url"
13 | "os"
14 | "path"
15 | "strconv"
16 | "strings"
17 | "time"
18 | "unicode/utf8"
19 |
20 | "github.com/andybalholm/brotli"
21 | "github.com/ije/gox/utils"
22 | "github.com/ije/rex/session"
23 | )
24 |
25 | // A AclUser interface contains the Permissions method that returns the permission IDs
26 | type AclUser interface {
27 | Perms() []string
28 | }
29 |
30 | // A ILogger interface contains the Printf method.
31 | type ILogger interface {
32 | Printf(format string, v ...any)
33 | }
34 |
35 | // A Context to handle http requests.
36 | type Context struct {
37 | R *http.Request
38 | W http.ResponseWriter
39 | queryRaw string
40 | query url.Values
41 | header http.Header
42 | basicAuthUser string
43 | aclUser AclUser
44 | session *SessionStub
45 | sessionPool session.Pool
46 | sessionIdHandler session.SidHandler
47 | logger ILogger
48 | accessLogger ILogger
49 | compress bool
50 | }
51 |
52 | // Next executes the next middleware in the chain.
53 | func (ctx *Context) Next() any {
54 | return next
55 | }
56 |
57 | // Method returns the request method.
58 | func (ctx *Context) Method() string {
59 | return ctx.R.Method
60 | }
61 |
62 | // Pathname returns the request pathname.
63 | func (ctx *Context) Pathname() string {
64 | return ctx.R.URL.Path
65 | }
66 |
67 | // PathValue returns the value for the named path wildcard in the [ServeMux] pattern
68 | // that matched the request.
69 | // It returns the empty string if the request was not matched against a pattern
70 | // or there is no such wildcard in the pattern.
71 | func (ctx *Context) PathValue(key string) string {
72 | return ctx.R.PathValue(key)
73 | }
74 |
75 | // RawQuery returns the request raw query string.
76 | func (ctx *Context) RawQuery() string {
77 | return ctx.R.URL.RawQuery
78 | }
79 |
80 | // Query parses RawQuery and returns the corresponding values. It silently discards malformed value pairs.
81 | func (ctx *Context) Query() url.Values {
82 | if url := ctx.R.URL; ctx.query == nil || ctx.queryRaw != url.RawQuery {
83 | ctx.queryRaw = url.RawQuery
84 | ctx.query = url.Query()
85 | }
86 | return ctx.query
87 | }
88 |
89 | // BasicAuthUser returns the BasicAuth username
90 | func (ctx *Context) BasicAuthUser() string {
91 | return ctx.basicAuthUser
92 | }
93 |
94 | // AclUser returns the ACL user
95 | func (ctx *Context) AclUser() AclUser {
96 | return ctx.aclUser
97 | }
98 |
99 | // Session returns the session if it is undefined then create a new one.
100 | func (ctx *Context) Session() *SessionStub {
101 | if ctx.sessionPool == nil {
102 | panic(&invalid{500, "session pool is not set"})
103 | }
104 |
105 | if ctx.session == nil {
106 | sid := ctx.sessionIdHandler.Get(ctx.R)
107 | sess, err := ctx.sessionPool.GetSession(sid)
108 | if err != nil {
109 | panic(&invalid{500, err.Error()})
110 | }
111 |
112 | ctx.session = &SessionStub{sess}
113 |
114 | if sess.SID() != sid {
115 | ctx.sessionIdHandler.Put(ctx.W, sess.SID())
116 | }
117 | }
118 |
119 | return ctx.session
120 | }
121 |
122 | // UserAgent returns the request User-Agent.
123 | func (ctx *Context) UserAgent() string {
124 | return ctx.R.Header.Get("User-Agent")
125 | }
126 |
127 | // RemoteIP returns the remote client IP.
128 | func (ctx *Context) RemoteIP() string {
129 | ip := ctx.R.Header.Get("X-Real-IP")
130 | if ip == "" {
131 | ip = ctx.R.Header.Get("X-Forwarded-For")
132 | if ip != "" {
133 | ip, _ = utils.SplitByFirstByte(ip, ',')
134 | } else {
135 | ip = ctx.R.RemoteAddr
136 | }
137 | }
138 | ip, _ = utils.SplitByLastByte(ip, ':')
139 | return ip
140 | }
141 |
142 | // Set sets the header entries associated with key to the
143 | // single element value. It replaces any existing values
144 | // associated with key. The key is case insensitive; it is
145 | // canonicalized by [textproto.CanonicalMIMEHeaderKey].
146 | // To use non-canonical keys, assign to the map directly.
147 | func (ctx *Context) SetHeader(key, value string) {
148 | ctx.header.Set(key, value)
149 | }
150 |
151 | // Cookie returns the named cookie provided in the request or
152 | // [ErrNoCookie] if not found.
153 | // If multiple cookies match the given name, only one cookie will
154 | // be returned.
155 | func (ctx *Context) Cookie(name string) (cookie *http.Cookie) {
156 | cookie, _ = ctx.R.Cookie(name)
157 | return
158 | }
159 |
160 | // SetCookie sets a cookie to the response.
161 | func (ctx *Context) SetCookie(cookie http.Cookie) {
162 | if cookie.Name != "" {
163 | ctx.header.Add("Set-Cookie", cookie.String())
164 | }
165 | }
166 |
167 | // DeleteCookie sets a cookie to the response with an expiration time in the past.
168 | func (ctx *Context) DeleteCookie(cookie http.Cookie) {
169 | cookie.Value = "-"
170 | cookie.Expires = time.Unix(0, 0)
171 | ctx.SetCookie(cookie)
172 | }
173 |
174 | // DeleteCookieByName sets a cookie to the response with an expiration time in the past.
175 | func (ctx *Context) DeleteCookieByName(name string) {
176 | ctx.SetCookie(http.Cookie{
177 | Name: name,
178 | Value: "-",
179 | Expires: time.Unix(0, 0),
180 | })
181 | }
182 |
183 | // FormValue returns the first value for the named component of the query.
184 | // The precedence order:
185 | // 1. application/x-www-form-urlencoded form body (POST, PUT, PATCH only)
186 | // 2. query parameters (always)
187 | // 3. multipart/form-data form body (always)
188 | //
189 | // FormValue calls [Request.ParseMultipartForm] and [Request.ParseForm]
190 | // if necessary and ignores any errors returned by these functions.
191 | // If key is not present, FormValue returns the empty string.
192 | // To access multiple values of the same key, call ParseForm and
193 | // then inspect [Request.Form] directly.
194 | func (ctx *Context) FormValue(key string) string {
195 | return ctx.R.FormValue(key)
196 | }
197 |
198 | // PostFormValue returns the first value for the named component of the POST,
199 | // PUT, or PATCH request body. URL query parameters are ignored.
200 | // PostFormValue calls [Request.ParseMultipartForm] and [Request.ParseForm] if necessary and ignores
201 | // any errors returned by these functions.
202 | // If key is not present, PostFormValue returns the empty string.
203 | func (ctx *Context) PostFormValue(key string) string {
204 | return ctx.R.PostFormValue(key)
205 | }
206 |
207 | // FormFile returns the first file for the provided form key.
208 | // FormFile calls [Request.ParseMultipartForm] and [Request.ParseForm] if necessary.
209 | func (ctx *Context) FormFile(key string) (multipart.File, *multipart.FileHeader, error) {
210 | return ctx.R.FormFile(key)
211 | }
212 |
213 | func (ctx *Context) enableCompression() bool {
214 | var encoding string
215 | accectEncoding := ctx.R.Header.Get("Accept-Encoding")
216 | if accectEncoding != "" && strings.Contains(accectEncoding, "br") {
217 | encoding = "br"
218 | } else if accectEncoding != "" && strings.Contains(accectEncoding, "gzip") {
219 | encoding = "gzip"
220 | }
221 | if encoding != "" {
222 | w, ok := ctx.W.(*rexWriter)
223 | if ok {
224 | h := w.Header()
225 | if v := h.Get("Vary"); v == "" {
226 | h.Set("Vary", "Accept-Encoding")
227 | } else if !strings.Contains(v, "Accept-Encoding") && !strings.Contains(v, "accept-encoding") {
228 | h.Set("Vary", v+", Accept-Encoding")
229 | }
230 | h.Set("Content-Encoding", encoding)
231 | h.Del("Content-Length")
232 | if encoding == "br" {
233 | w.zWriter = brotli.NewWriterLevel(w.rawWriter, brotli.BestSpeed)
234 | } else if encoding == "gzip" {
235 | w.zWriter, _ = gzip.NewWriterLevel(w.rawWriter, gzip.BestSpeed)
236 | }
237 | return true
238 | }
239 | }
240 | return false
241 | }
242 |
243 | func (ctx *Context) respondWith(v any) {
244 | w := ctx.W
245 | h := w.Header()
246 | code := 200
247 |
248 | SWITCH:
249 | switch r := v.(type) {
250 | case http.Handler:
251 | r.ServeHTTP(w, ctx.R)
252 |
253 | case *http.Response:
254 | for k, v := range r.Header {
255 | h[k] = v
256 | }
257 | w.WriteHeader(r.StatusCode)
258 | io.Copy(w, r.Body)
259 | r.Body.Close()
260 |
261 | case *redirect:
262 | h.Set("Location", hexEscapeNonASCII(r.url))
263 | w.WriteHeader(r.status)
264 |
265 | case string:
266 | data := []byte(r)
267 | if h.Get("Content-Type") == "" {
268 | h.Set("Content-Type", "text/plain; charset=utf-8")
269 | }
270 | if ctx.compress {
271 | if len(data) < compressMinSize || !ctx.enableCompression() {
272 | h.Set("Content-Length", strconv.Itoa(len(data)))
273 | }
274 | } else {
275 | h.Set("Content-Length", strconv.Itoa(len(data)))
276 | }
277 | w.WriteHeader(code)
278 | w.Write(data)
279 |
280 | case bool, int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64, float32, float64:
281 | if h.Get("Content-Type") == "" {
282 | h.Set("Content-Type", "text/plain")
283 | }
284 | w.WriteHeader(code)
285 | fmt.Fprintf(w, "%v", r)
286 |
287 | case []byte:
288 | cType := h.Get("Content-Type")
289 | if ctx.compress && isTextContent(cType) {
290 | if len(r) < compressMinSize || !ctx.enableCompression() {
291 | h.Set("Content-Length", strconv.Itoa(len(r)))
292 | }
293 | } else {
294 | h.Set("Content-Length", strconv.Itoa(len(r)))
295 | }
296 | if cType == "" {
297 | h.Set("Content-Type", "binary/octet-stream")
298 | }
299 | w.WriteHeader(code)
300 | w.Write(r)
301 |
302 | case io.Reader:
303 | defer func() {
304 | if c, ok := r.(io.Closer); ok {
305 | c.Close()
306 | }
307 | }()
308 | size := -1
309 | if s, ok := r.(io.Seeker); ok {
310 | n, err := s.Seek(0, io.SeekEnd)
311 | if err == nil {
312 | _, err = s.Seek(0, io.SeekStart)
313 | if err != nil {
314 | ctx.respondWithError(err)
315 | return
316 | }
317 | size = int(n)
318 | }
319 | }
320 | cType := h.Get("Content-Type")
321 | if cType == "" {
322 | h.Set("Content-Type", "binary/octet-stream")
323 | }
324 | if ctx.compress && isTextContent(cType) {
325 | if size >= 0 {
326 | if size < compressMinSize || !ctx.enableCompression() {
327 | h.Set("Content-Length", strconv.Itoa(size))
328 | }
329 | } else {
330 | // unable to seek, compress the content anyway
331 | ctx.enableCompression()
332 | }
333 | } else if size >= 0 {
334 | h.Set("Content-Length", strconv.Itoa(size))
335 | }
336 | w.WriteHeader(code)
337 | io.Copy(w, r)
338 |
339 | case *content:
340 | if c, ok := r.content.(io.Closer); ok {
341 | defer c.Close()
342 | }
343 | size := -1
344 | if s, ok := r.content.(io.Seeker); ok {
345 | n, err := s.Seek(0, io.SeekEnd)
346 | if err == nil {
347 | _, err = s.Seek(0, io.SeekStart)
348 | if err != nil {
349 | ctx.respondWithError(err)
350 | return
351 | }
352 | size = int(n)
353 | }
354 | }
355 | if ctx.compress && isTextFile(r.name) {
356 | if size >= 0 {
357 | if size < compressMinSize || !ctx.enableCompression() {
358 | h.Set("Content-Length", strconv.Itoa(int(size)))
359 | }
360 | } else {
361 | // unable to seek, compress the content anyway
362 | ctx.enableCompression()
363 | }
364 | } else if size >= 0 {
365 | h.Set("Content-Length", strconv.Itoa(size))
366 | }
367 | etag := h.Get("ETag")
368 | if etag != "" && etag == ctx.R.Header.Get("If-None-Match") {
369 | w.WriteHeader(304)
370 | return
371 | }
372 | if r.mtime.IsZero() {
373 | if h.Get("Cache-Control") == "" {
374 | h.Set("Cache-Control", "public, max-age=0, must-revalidate")
375 | }
376 | } else {
377 | if checkIfModifiedSince(ctx.R, r.mtime) {
378 | w.WriteHeader(304)
379 | return
380 | }
381 | h.Set("Last-Modified", r.mtime.UTC().Format(http.TimeFormat))
382 | }
383 | ctype := h.Get("Content-Type")
384 | if ctype == "" {
385 | ctype = mime.TypeByExtension(path.Ext(r.name))
386 | if ctype != "" {
387 | h.Set("Content-Type", ctype)
388 | }
389 | }
390 | w.WriteHeader(code)
391 | if ctx.R.Method != "HEAD" {
392 | io.Copy(w, r.content)
393 | }
394 |
395 | case *noContent:
396 | w.WriteHeader(http.StatusNoContent)
397 |
398 | case *status:
399 | code = r.code
400 | v = r.content
401 | if v == nil {
402 | w.WriteHeader(code)
403 | } else {
404 | goto SWITCH
405 | }
406 |
407 | case *fs:
408 | filepath := path.Join(r.root, ctx.R.URL.Path)
409 | fi, err := os.Stat(filepath)
410 | if err == nil && fi.IsDir() {
411 | filepath = path.Join(filepath, "index.html")
412 | _, err = os.Stat(filepath)
413 | }
414 | if err != nil && os.IsNotExist(err) && r.fallback != "" {
415 | filepath = path.Join(r.root, r.fallback)
416 | _, err = os.Stat(filepath)
417 | }
418 | if err != nil {
419 | if os.IsNotExist(err) {
420 | w.WriteHeader(404)
421 | w.Write([]byte("Not Found"))
422 | } else {
423 | ctx.respondWithError(err)
424 | }
425 | return
426 | }
427 | v = File(filepath)
428 | goto SWITCH
429 |
430 | case *invalid:
431 | h.Set("Content-Type", "text/plain; charset=utf-8")
432 | w.WriteHeader(r.code)
433 | w.Write([]byte(r.message))
434 |
435 | case Error:
436 | h.Set("Content-Type", "application/json; charset=utf-8")
437 | w.WriteHeader(r.Code)
438 | json.NewEncoder(w).Encode(r)
439 |
440 | case *Error:
441 | h.Set("Content-Type", "application/json; charset=utf-8")
442 | w.WriteHeader(r.Code)
443 | json.NewEncoder(w).Encode(r)
444 |
445 | case error:
446 | ctx.respondWithError(r)
447 |
448 | default:
449 | buf := bytes.NewBuffer(nil)
450 | err := json.NewEncoder(buf).Encode(v)
451 | h.Set("Content-Type", "application/json; charset=utf-8")
452 | if err != nil {
453 | w.WriteHeader(500)
454 | w.Write([]byte(`{"error": {"status": 500, "message": "bad json"}}`))
455 | return
456 | }
457 | if !ctx.compress || buf.Len() < compressMinSize || !ctx.enableCompression() {
458 | h.Set("Content-Length", strconv.Itoa(buf.Len()))
459 | }
460 | w.WriteHeader(code)
461 | io.Copy(w, buf)
462 | }
463 | }
464 |
465 | func (ctx *Context) respondWithError(err error) {
466 | w := ctx.W
467 | message := err.Error()
468 | if ctx.logger != nil {
469 | ctx.logger.Printf("[error] %s", message)
470 | }
471 | w.Header().Set("Content-Type", "text/plain; charset=utf-8")
472 | w.WriteHeader(500)
473 | w.Write([]byte(message))
474 | }
475 |
476 | func isTextFile(filename string) bool {
477 | switch strings.TrimPrefix(path.Ext(filename), ".") {
478 | case "html", "htm", "xml", "svg", "css", "less", "sass", "scss", "json", "json5", "map", "js", "jsx", "mjs", "cjs", "ts", "mts", "tsx", "md", "mdx", "yaml", "txt", "wasm":
479 | return true
480 | default:
481 | return false
482 | }
483 | }
484 |
485 | func isTextContent(contentType string) bool {
486 | return contentType != "" && (strings.HasPrefix(contentType, "text/") ||
487 | strings.HasPrefix(contentType, "application/javascript") ||
488 | strings.HasPrefix(contentType, "application/json") ||
489 | strings.HasPrefix(contentType, "application/xml") ||
490 | strings.HasPrefix(contentType, "application/wasm"))
491 | }
492 |
493 | func hexEscapeNonASCII(s string) string {
494 | newLen := 0
495 | for i := 0; i < len(s); i++ {
496 | if s[i] >= utf8.RuneSelf {
497 | newLen += 3
498 | } else {
499 | newLen++
500 | }
501 | }
502 | if newLen == len(s) {
503 | return s
504 | }
505 | b := make([]byte, 0, newLen)
506 | var pos int
507 | for i := 0; i < len(s); i++ {
508 | if s[i] >= utf8.RuneSelf {
509 | if pos < i {
510 | b = append(b, s[pos:i]...)
511 | }
512 | b = append(b, '%')
513 | b = strconv.AppendInt(b, int64(s[i]), 16)
514 | pos = i + 1
515 | }
516 | }
517 | if pos < len(s) {
518 | b = append(b, s[pos:]...)
519 | }
520 | return string(b)
521 | }
522 |
523 | func checkIfModifiedSince(r *http.Request, modtime time.Time) bool {
524 | if r.Method != "GET" && r.Method != "HEAD" {
525 | return false
526 | }
527 | ims := r.Header.Get("If-Modified-Since")
528 | if ims == "" || modtime.IsZero() || modtime.Equal(time.Unix(0, 0)) {
529 | return false
530 | }
531 | t, err := http.ParseTime(ims)
532 | if err != nil {
533 | return false
534 | }
535 | // The Last-Modified header truncates sub-second precision so
536 | // the modtime needs to be truncated too.
537 | modtime = modtime.Truncate(time.Second)
538 | return modtime.Compare(t) > 0
539 | }
540 |
--------------------------------------------------------------------------------
/default.go:
--------------------------------------------------------------------------------
1 | package rex
2 |
3 | import (
4 | "log"
5 | "time"
6 |
7 | "github.com/ije/rex/session"
8 | )
9 |
10 | const compressMinSize = 1024
11 |
12 | var defaultMux = New()
13 | var defaultSessionPool = session.NewMemorySessionPool(time.Hour / 2)
14 | var defaultSessionIdHandler = session.NewCookieSidHandler("SID")
15 | var defaultLogger = &log.Logger{}
16 |
17 | // Use appends middlewares to current APIS middleware stack.
18 | func Use(middlewares ...Handle) {
19 | defaultMux.Use(middlewares...)
20 | }
21 |
22 | // AddRoute adds a route.
23 | func AddRoute(pattern string, handle Handle) {
24 | defaultMux.AddRoute(pattern, handle)
25 | }
26 |
27 | // HEAD returns a Handle to handle HEAD requests
28 | func HEAD(pattern string, handles ...Handle) {
29 | AddRoute("HEAD "+pattern, Chain(handles...))
30 | }
31 |
32 | // GET returns a Handle to handle GET requests
33 | func GET(pattern string, handles ...Handle) {
34 | AddRoute("GET "+pattern, Chain(handles...))
35 | }
36 |
37 | // POST returns a Handle to handle POST requests
38 | func POST(pattern string, handles ...Handle) {
39 | AddRoute("POST "+pattern, Chain(handles...))
40 | }
41 |
42 | // PUT returns a Handle to handle PUT requests
43 | func PUT(pattern string, handles ...Handle) {
44 | AddRoute("PUT "+pattern, Chain(handles...))
45 | }
46 |
47 | // PATCH returns a Handle to handle PATCH requests
48 | func PATCH(pattern string, handles ...Handle) {
49 | AddRoute("PATCH "+pattern, Chain(handles...))
50 | }
51 |
52 | // DELETE returns a Handle to handle DELETE requests
53 | func DELETE(pattern string, handles ...Handle) {
54 | AddRoute("DELETE "+pattern, Chain(handles...))
55 | }
56 |
--------------------------------------------------------------------------------
/examples/markdown-render/books/one-hundred-years-of-solitude.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: One Hundred Years of Solitude
3 | author: Gabriel Garcia Marquez
4 | published: 1967-05-30
5 | ---
6 |
7 | “One Hundred Years of Solitude” by Gabriel García Márquez is a landmark novel in the genre of magical realism. The novel tells the story of the Buendia family over the course of seven generations, tracing their lives and fortunes as they build the town of Macondo and confront the forces of modernity.
8 |
9 | At its core, “One Hundred Years of Solitude” is a meditation on the cyclical nature of history and the recurring patterns that shape human experience. The novel is marked by a sense of repetition and circularity, as characters are born, live, and die, only to be replaced by new generations who seem to follow the same patterns of behavior and thought.
10 |
11 | But the novel is also a celebration of the power of imagination and the ways in which stories can shape our understanding of the world. Márquez uses magical realism to blur the line between reality and fantasy, infusing the novel with a sense of wonder and possibility. The novel is full of fantastical events and characters, from the rain that lasts for four years to the woman who ascends to heaven on a carpet of butterflies.
12 |
13 | At the same time, “One Hundred Years of Solitude” is a searing critique of the forces of imperialism and capitalism that threaten to destroy traditional ways of life. The Buendia family is repeatedly challenged by outside forces, from the arrival of the railroad to the influence of foreign companies, and Márquez portrays their struggles with a sense of urgency and compassion.
14 |
15 | Márquez’s prose is lyrical and poetic, capturing the rhythms and cadences of everyday speech. He weaves together multiple strands of narrative, creating a complex web of events and characters that span generations. The result is a novel that is both deeply personal and universally resonant, a testament to the power of storytelling to capture the complexities of the human experience.
16 |
17 | “One Hundred Years of Solitude” is a masterpiece of world literature, a novel that continues to captivate and inspire readers around the globe. Its themes of love, loss, and the passage of time are timeless, and its characters are unforgettable, serving as a powerful reminder of the enduring power of the novel as an art form.
18 |
--------------------------------------------------------------------------------
/examples/markdown-render/books/the-great-gatsby.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: The Great Gatsby
3 | author: F. Scott Fitzgerald
4 | published: 1925-04-10
5 | ---
6 |
7 | “The Great Gatsby” by F. Scott Fitzgerald is a masterpiece of American literature that explores the decadence and excess of the Jazz Age. The novel tells the story of Jay Gatsby, a mysterious millionaire who throws lavish parties in the hopes of winning back his former love, Daisy Buchanan.
8 |
9 | At its core, “The Great Gatsby” is a meditation on the American Dream and the ways in which it can be corrupted by wealth and power. Gatsby’s pursuit of wealth and status is driven by his desire to win back Daisy, but he is ultimately consumed by his own illusions and the empty promises of the American Dream.
10 |
11 | Fitzgerald’s prose is both elegant and cutting, capturing the glittering surface of Gatsby’s world while also exposing its darker underbelly. The novel is full of memorable characters, from the cynical narrator, Nick Carraway, to the tragic figure of Myrtle Wilson, whose affair with Tom Buchanan sets in motion the events that lead to Gatsby’s downfall.
12 |
13 | But at its heart, “The Great Gatsby” is a love story, a tale of two people separated by time, class, and circumstance. Gatsby and Daisy’s relationship is marked by a sense of longing and unfulfilled desire, and their ultimate reunion is both cathartic and tragic.
14 |
15 | “The Great Gatsby” remains a seminal work of American literature, a novel that captures the spirit of an era and the enduring themes of love, wealth, and power. Its insights into the human condition are as relevant today as they were nearly a century ago, and its legacy continues to inspire generations of readers and writers alike.
16 |
--------------------------------------------------------------------------------
/examples/markdown-render/books/the-house-of-spirits.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: The House of Spirits
3 | author: Isabel Allende
4 | published: 1982-01-01
5 | ---
6 |
7 | “The House of Spirits” by Isabel Allende is a [sprawling family saga that spans multiple generations](https://bibliophile.mystagingwebsite.com/about/) and offers a rich exploration of the social and political turmoil of 20th century Chile. The novel is narrated by Alba, the youngest member of the Trueba family, and follows the lives of her ancestors, including her grandfather Esteban Trueba, who builds the family fortune, and her mother Blanca, who falls in love with a revolutionary and challenges her family’s conservative values.
8 |
9 | ## The story
10 |
11 | At its core, “The House of Spirits” is a story about power and its abuses. The Trueba family is an emblem of the patriarchal system that dominated Chilean society for decades, and Esteban is a quintessential authoritarian figure who uses violence and coercion to maintain his grip on his family and his country. Allende portrays his character with complexity, highlighting his flaws and his humanity even as he commits unspeakable atrocities.
12 |
13 | But the novel is not just a condemnation of the status quo. It also offers a vision of a better world, one in which women, indigenous people, and the poor are granted the dignity and respect they deserve. Blanca and her lover Pedro embody this vision, fighting for social justice and a more equitable society. Allende’s prose is poetic and evocative, imbuing even the darkest moments with a sense of hope and possibility.
14 |
15 | ## Past shaping present
16 |
17 | At the heart of the novel is the theme of memory, and the way in which the past shapes the present. Alba is a living link to her family’s history, and as she narrates their stories, she becomes both a witness to their struggles and a participant in their legacy. Allende masterfully weaves together different strands of narrative, creating a tapestry of events and emotions that reverberate through the ages.
18 |
19 | “The House of Spirits” is a stunning achievement, a novel that manages to be both epic in scope and intimate in detail. It offers a vivid portrait of a society in flux, and a powerful meditation on the forces that shape our lives. Allende’s prose is vibrant and lush, and her characters are unforgettable, breathing life into a story that feels both timeless and urgent.
20 |
--------------------------------------------------------------------------------
/examples/markdown-render/books/the-myth-of-sisyphus.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: The Myth of Sisyphus
3 | author: Albert Camus
4 | published: 1942-10-16
5 | ---
6 |
7 | “The Myth of Sisyphus” by Albert Camus is a philosophical essay that explores the nature of human existence and the search for meaning in a world that appears to be indifferent to our struggles. The essay takes its title from the Greek myth of Sisyphus, a king who was condemned by the gods to roll a boulder up a hill for all eternity, only to see it roll back down again each time he reached the top.
8 |
9 | Camus uses the myth of Sisyphus as a metaphor for the human condition, arguing that our lives are similarly marked by a sense of futility and absurdity. We are all Sisyphus, condemned to repeat the same actions over and over again, without ever achieving any real sense of purpose or fulfillment.
10 |
11 | But Camus also suggests that there is a kind of beauty in this struggle, a defiance and resilience that allows us to find meaning in the face of the absurd. He writes, “The struggle itself […] is enough to fill a man’s heart. One must imagine Sisyphus happy.”
12 |
13 | For Camus, the search for meaning is not something that can be found in external sources, such as religion or philosophy, but is instead a deeply personal and subjective experience. He argues that we must confront the absurdity of existence head-on, accepting it for what it is and finding ways to create meaning in our own lives.
14 |
15 | Camus’ prose is both elegant and accessible, and his ideas have had a profound impact on modern philosophy and literature. “The Myth of Sisyphus” is a powerful meditation on the human condition, challenging us to confront the fundamental questions of existence and to find our own way in a world that often seems indifferent to our struggles.
16 |
--------------------------------------------------------------------------------
/examples/markdown-render/books/to-the-lighthouse.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: To The Lighthouse
3 | author: Virginia Woolf
4 | published: 1927-05-05
5 | ---
6 |
7 | “To the Lighthouse” by Virginia Woolf is a modernist masterpiece that explores the complexities of human relationships and the nature of time. The novel is divided into three sections: “The Window,” “Time Passes,” and “The Lighthouse,” each of which offers a unique perspective on the lives of the Ramsay family and their guests.
8 |
9 | ## The story
10 |
11 | At the heart of the novel is the tension between Mrs. Ramsay, the matriarch of the family, and her husband, Mr. Ramsay, a philosopher and academic. Woolf portrays their relationship with subtlety and nuance, showing how their differences in temperament and worldview create both intimacy and conflict. Mrs. Ramsay is a warm and nurturing figure, while Mr. Ramsay is distant and intellectual, and their interactions are marked by a sense of longing and unspoken understanding.
12 |
13 | But “To the Lighthouse” is not just a story about one family. Woolf uses the novel to explore the broader themes of mortality, the passing of time, and the fleeting nature of human experience. In “Time Passes,” Woolf depicts the empty house as it falls into disrepair over the years, creating a haunting sense of loss and decay. The novel suggests that while individual lives may be fleeting, the human experience is part of a larger, ongoing narrative that stretches beyond our own existence.
14 |
15 | ## Woolf’s Prose
16 |
17 | Woolf’s prose is lyrical and experimental, using stream of consciousness techniques to convey the inner thoughts and emotions of her characters. She captures the fleeting nature of human experience with a sense of immediacy and intimacy, creating a vivid portrait of the world as it exists in the minds of her characters.
18 |
19 | “To the Lighthouse” is a remarkable achievement, a novel that manages to be both deeply personal and universally resonant. Woolf’s exploration of the human condition is profound and moving, and her prose is a testament to the power of language to capture the fleeting nature of our lives. It is a work of art that continues to captivate and inspire readers nearly a century after its publication.
20 |
--------------------------------------------------------------------------------
/examples/markdown-render/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "bufio"
5 | "bytes"
6 | "fmt"
7 | "os"
8 | "regexp"
9 | "strings"
10 |
11 | "github.com/ije/rex"
12 | )
13 |
14 | type Book struct {
15 | Title string
16 | Author string
17 | Published string
18 | Intro string
19 | Slug string
20 | }
21 |
22 | const listTplRaw = `
23 |
24 |
25 |
26 |
27 | Books
31 |
32 | {{range .}}
33 |
39 |
40 |
41 | `
42 |
43 | const pageTplRaw = `
44 |
45 |
46 |
47 |
48 | {{.Title}}
35 | {{.Title}}
52 |
By {{.Author}}
53 |"+resolveAnchorLinks(line)+"
") 158 | } 159 | } 160 | if err := scanner.Err(); err != nil { 161 | return "", err 162 | } 163 | return strings.Join(html, "\n"), nil 164 | } 165 | 166 | var mdAnchorRegexp = regexp.MustCompile(`\[(.*?)\]\((.*?)\)`) 167 | 168 | func resolveAnchorLinks(raw string) string { 169 | return mdAnchorRegexp.ReplaceAllString(raw, `$1`) 170 | } 171 | -------------------------------------------------------------------------------- /examples/static-site/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/ije/rex" 7 | ) 8 | 9 | func main() { 10 | rex.Use( 11 | rex.Compress(), 12 | rex.Static("./www", "e404.html"), 13 | ) 14 | 15 | fmt.Println("Server running on http://localhost:8080") 16 | <-rex.Start(8080) 17 | } 18 | -------------------------------------------------------------------------------- /examples/static-site/www/alice/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |Try to login with admin or guest.
41 | {{end}} 42 | 43 | 44 | ` 45 | 46 | var indexTpl = rex.Tpl(indexHTML) 47 | 48 | type user struct { 49 | name string 50 | perms []string 51 | } 52 | 53 | func (u *user) Perms() []string { 54 | return u.perms 55 | } 56 | 57 | func main() { 58 | todos := []string{} 59 | 60 | // override http methods middleware 61 | rex.Use(func(ctx *rex.Context) any { 62 | if ctx.R.Method == "POST" && ctx.FormValue("_method") == "DELETE" { 63 | ctx.R.Method = "DELETE" 64 | } 65 | return ctx.Next() 66 | }) 67 | 68 | // auth middleware 69 | rex.Use(rex.AclAuth(func(ctx *rex.Context) rex.AclUser { 70 | value := ctx.Session().Get("USER") 71 | if value == nil { 72 | return nil 73 | } 74 | name := string(value) 75 | if name == "admin" { 76 | return &user{ 77 | name: "admin", 78 | perms: []string{"create", "delete"}, 79 | } 80 | } else if name == "guest" { 81 | return &user{ 82 | name: name, 83 | } 84 | } 85 | return nil 86 | })) 87 | 88 | rex.GET("/{$}", func(ctx *rex.Context) any { 89 | data := map[string]any{} 90 | aclUser := ctx.AclUser() 91 | if aclUser != nil { 92 | data["user"] = aclUser.(*user).name 93 | data["todos"] = todos 94 | } 95 | return rex.Render(indexTpl, data) 96 | }) 97 | 98 | rex.POST("/add-todo", rex.Perm("create"), func(ctx *rex.Context) any { 99 | todo := ctx.FormValue("todo") 100 | todos = append(todos, todo) 101 | return rex.Redirect("/", 302) 102 | }) 103 | 104 | rex.DELETE("/delete-todo", rex.Perm("delete"), func(ctx *rex.Context) any { 105 | index, err := strconv.ParseInt(ctx.FormValue("index"), 10, 64) 106 | if err != nil { 107 | return err 108 | } 109 | var newTodos []string 110 | for i, todo := range todos { 111 | if i != int(index) { 112 | newTodos = append(newTodos, todo) 113 | } 114 | } 115 | todos = newTodos 116 | return rex.Redirect("/", 302) 117 | }) 118 | 119 | rex.POST("/login", func(ctx *rex.Context) any { 120 | user := ctx.FormValue("user") 121 | if user != "admin" && user != "guest" { 122 | return rex.Status(403, rex.HTML("Oops, you are not allowed to login. Go back
")) 123 | } 124 | ctx.Session().Set("USER", []byte(user)) 125 | return rex.Redirect("/", 302) 126 | }) 127 | 128 | rex.POST("/logout", func(ctx *rex.Context) any { 129 | ctx.Session().Delete("USER") 130 | return rex.Redirect("/", 302) 131 | }) 132 | 133 | fmt.Println("Server running on http://localhost:8080") 134 | <-rex.Start(8080) 135 | } 136 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/ije/rex 2 | 3 | go 1.23.0 4 | 5 | require ( 6 | github.com/andybalholm/brotli v1.1.1 7 | github.com/ije/gox v0.9.8 8 | github.com/rs/cors v1.11.1 9 | golang.org/x/crypto v0.38.0 10 | ) 11 | 12 | require ( 13 | golang.org/x/net v0.40.0 // indirect 14 | golang.org/x/text v0.25.0 // indirect 15 | ) 16 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/andybalholm/brotli v1.1.1 h1:PR2pgnyFznKEugtsUo0xLdDop5SKXd5Qf5ysW+7XdTA= 2 | github.com/andybalholm/brotli v1.1.1/go.mod h1:05ib4cKhjx3OQYUY22hTVd34Bc8upXjOLL2rKwwZBoA= 3 | github.com/ije/gox v0.9.8 h1:vlkCIy8NxmZAVfZ6eah0/H/RNqMKHXC9FS5kiYdAwbE= 4 | github.com/ije/gox v0.9.8/go.mod h1:3GTaK8WXf6oxRbrViLqKNLTNcMR871Dz0zoujFNmG48= 5 | github.com/rs/cors v1.11.1 h1:eU3gRzXLRK57F5rKMGMZURNdIG4EoAmX8k94r9wXWHA= 6 | github.com/rs/cors v1.11.1/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU= 7 | github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU= 8 | github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E= 9 | golang.org/x/crypto v0.38.0 h1:jt+WWG8IZlBnVbomuhg2Mdq0+BBQaHbtqHEFEigjUV8= 10 | golang.org/x/crypto v0.38.0/go.mod h1:MvrbAqul58NNYPKnOra203SB9vpuZW0e+RRZV+Ggqjw= 11 | golang.org/x/net v0.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY= 12 | golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds= 13 | golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4= 14 | golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA= 15 | -------------------------------------------------------------------------------- /middlewares.go: -------------------------------------------------------------------------------- 1 | package rex 2 | 3 | import ( 4 | "encoding/base64" 5 | "fmt" 6 | "net/http" 7 | "strings" 8 | 9 | "github.com/ije/gox/utils" 10 | "github.com/ije/rex/session" 11 | "github.com/rs/cors" 12 | ) 13 | 14 | var next = &struct{}{} 15 | 16 | // Next executes the next middleware in the chain. 17 | func Next() any { 18 | return next 19 | } 20 | 21 | // Header is rex middleware to set http header for the current request. 22 | func Header(key string, value string) Handle { 23 | return func(ctx *Context) any { 24 | if key != "" && value != "" { 25 | ctx.header.Set(key, value) 26 | } 27 | return next 28 | } 29 | } 30 | 31 | // Logger returns a logger middleware to sets the error logger for the context. 32 | func Logger(logger ILogger) Handle { 33 | return func(ctx *Context) any { 34 | ctx.logger = logger 35 | return next 36 | } 37 | } 38 | 39 | // AccessLogger returns a logger middleware to sets the access logger. 40 | func AccessLogger(logger ILogger) Handle { 41 | return func(ctx *Context) any { 42 | ctx.accessLogger = logger 43 | return next 44 | } 45 | } 46 | 47 | // SessionOptions contains the options for the session manager. 48 | type SessionOptions struct { 49 | IdHandler session.SidHandler 50 | Pool session.Pool 51 | } 52 | 53 | // Session returns a session middleware to configure the session manager. 54 | func Session(opts SessionOptions) Handle { 55 | return func(ctx *Context) any { 56 | if opts.IdHandler != nil { 57 | ctx.sessionIdHandler = opts.IdHandler 58 | } 59 | if opts.Pool != nil { 60 | ctx.sessionPool = opts.Pool 61 | } 62 | return next 63 | } 64 | } 65 | 66 | // CorsOptions is a configuration container to setup the CorsOptions middleware. 67 | type CorsOptions struct { 68 | // AllowedOrigins is a list of origins a cross-domain request can be executed from. 69 | // If the special "*" value is present in the list, all origins will be allowed. 70 | // An origin may contain a wildcard (*) to replace 0 or more characters 71 | // (i.e.: http://*.domain.com). Usage of wildcards implies a small performance penalty. 72 | // Only one wildcard can be used per origin. 73 | // Default value is ["*"] 74 | AllowedOrigins []string 75 | // AllowOriginFunc is a custom function to validate the origin. It take the 76 | // origin as argument and returns true if allowed or false otherwise. If 77 | // this option is set, the content of `AllowedOrigins` is ignored. 78 | AllowOriginFunc func(origin string) bool 79 | // AllowOriginRequestFunc is a custom function to validate the origin. It 80 | // takes the HTTP Request object and the origin as argument and returns true 81 | // if allowed or false otherwise. If headers are used take the decision, 82 | // consider using AllowOriginVaryRequestFunc instead. If this option is set, 83 | // the content of `AllowedOrigins`, `AllowOriginFunc` are ignored. 84 | AllowOriginRequestFunc func(r *http.Request, origin string) bool 85 | // AllowOriginVaryRequestFunc is a custom function to validate the origin. 86 | // It takes the HTTP Request object and the origin as argument and returns 87 | // true if allowed or false otherwise with a list of headers used to take 88 | // that decision if any so they can be added to the Vary header. If this 89 | // option is set, the content of `AllowedOrigins`, `AllowOriginFunc` and 90 | // `AllowOriginRequestFunc` are ignored. 91 | AllowOriginVaryRequestFunc func(r *http.Request, origin string) (bool, []string) 92 | // AllowedMethods is a list of methods the client is allowed to use with 93 | // cross-domain requests. Default value is simple methods (HEAD, GET and POST). 94 | AllowedMethods []string 95 | // AllowedHeaders is list of non simple headers the client is allowed to use with 96 | // cross-domain requests. 97 | // If the special "*" value is present in the list, all headers will be allowed. 98 | // Default value is []. 99 | AllowedHeaders []string 100 | // ExposedHeaders indicates which headers are safe to expose to the API of a CORS 101 | // API specification 102 | ExposedHeaders []string 103 | // MaxAge indicates how long (in seconds) the results of a preflight request 104 | // can be cached. Default value is 0, which stands for no 105 | // Access-Control-Max-Age header to be sent back, resulting in browsers 106 | // using their default value (5s by spec). If you need to force a 0 max-age, 107 | // set `MaxAge` to a negative value (ie: -1). 108 | MaxAge int 109 | // AllowCredentials indicates whether the request can include user credentials like 110 | // cookies, HTTP authentication or client side SSL certificates. 111 | AllowCredentials bool 112 | // AllowPrivateNetwork indicates whether to accept cross-origin requests over a 113 | // private network. 114 | AllowPrivateNetwork bool 115 | // OptionsPassthrough instructs preflight to let other potential next handlers to 116 | // process the OPTIONS method. Turn this on if your application handles OPTIONS. 117 | OptionsPassthrough bool 118 | // Provides a status code to use for successful OPTIONS requests. 119 | // Default value is http.StatusNoContent (204). 120 | OptionsSuccessStatus int 121 | // Debugging flag adds additional output to debug server side CORS issues 122 | Debug bool 123 | // Adds a custom logger, implies Debug is true 124 | Logger ILogger 125 | } 126 | 127 | // CorsAll create a new Cors handler with permissive configuration allowing all 128 | // origins with all standard methods with any header and credentials. 129 | func CorsAll() CorsOptions { 130 | return CorsOptions{ 131 | AllowedOrigins: []string{"*"}, 132 | AllowedMethods: []string{ 133 | http.MethodHead, 134 | http.MethodGet, 135 | http.MethodPost, 136 | http.MethodPut, 137 | http.MethodPatch, 138 | http.MethodDelete, 139 | }, 140 | AllowedHeaders: []string{"*"}, 141 | AllowCredentials: false, 142 | } 143 | } 144 | 145 | // Cors returns a CORS middleware to handle CORS. 146 | func Cors(c CorsOptions) Handle { 147 | cors := cors.New(cors.Options{ 148 | AllowedOrigins: c.AllowedOrigins, 149 | AllowOriginFunc: c.AllowOriginFunc, 150 | AllowOriginRequestFunc: c.AllowOriginRequestFunc, 151 | AllowOriginVaryRequestFunc: c.AllowOriginVaryRequestFunc, 152 | AllowedMethods: c.AllowedMethods, 153 | AllowedHeaders: c.AllowedHeaders, 154 | ExposedHeaders: c.ExposedHeaders, 155 | MaxAge: c.MaxAge, 156 | AllowCredentials: c.AllowCredentials, 157 | AllowPrivateNetwork: c.AllowPrivateNetwork, 158 | OptionsPassthrough: c.OptionsPassthrough, 159 | OptionsSuccessStatus: c.OptionsSuccessStatus, 160 | Debug: c.Debug, 161 | Logger: c.Logger, 162 | }) 163 | return func(ctx *Context) any { 164 | optionPassthrough := false 165 | h := func(w http.ResponseWriter, r *http.Request) { 166 | optionPassthrough = true 167 | } 168 | cors.ServeHTTP(ctx.W, ctx.R, h) 169 | if optionPassthrough { 170 | return next 171 | } 172 | return h // end 173 | } 174 | } 175 | 176 | // Perm returns a ACL middleware that sets the permission for the current request. 177 | func Perm(perms ...string) Handle { 178 | permSet := make(map[string]struct{}, len(perms)) 179 | for _, p := range perms { 180 | permSet[p] = struct{}{} 181 | } 182 | return func(ctx *Context) any { 183 | if ctx.aclUser != nil { 184 | permissions := ctx.aclUser.Perms() 185 | for _, p := range permissions { 186 | if _, ok := permSet[p]; ok { 187 | return next 188 | } 189 | } 190 | } 191 | return &invalid{403, "Forbidden"} 192 | } 193 | } 194 | 195 | // BasicAuth returns a basic HTTP authorization middleware. 196 | func BasicAuth(auth func(name string, secret string) (ok bool, err error)) Handle { 197 | return BasicAuthWithRealm("", auth) 198 | } 199 | 200 | // BasicAuthWithRealm returns a basic HTTP authorization middleware with realm. 201 | func BasicAuthWithRealm(realm string, auth func(name string, secret string) (ok bool, err error)) Handle { 202 | return func(ctx *Context) any { 203 | value := ctx.R.Header.Get("Authorization") 204 | if strings.HasPrefix(value, "Basic ") { 205 | authInfo, err := base64.StdEncoding.DecodeString(value[6:]) 206 | if err == nil { 207 | name, secret := utils.SplitByFirstByte(string(authInfo), ':') 208 | ok, err := auth(name, secret) 209 | if err != nil { 210 | return err 211 | } 212 | if ok { 213 | ctx.basicAuthUser = name 214 | return next 215 | } 216 | } 217 | } 218 | 219 | if realm == "" { 220 | realm = "Authorization Required" 221 | } 222 | ctx.header.Set("WWW-Authenticate", fmt.Sprintf(`Basic realm="%s"`, realm)) 223 | return Status(401, "") 224 | } 225 | } 226 | 227 | // AclAuth returns a ACL authorization middleware. 228 | func AclAuth(auth func(ctx *Context) AclUser) Handle { 229 | return func(ctx *Context) any { 230 | ctx.aclUser = auth(ctx) 231 | return next 232 | } 233 | } 234 | 235 | // Compress returns a rex middleware to enable http compression. 236 | func Compress() Handle { 237 | return func(ctx *Context) any { 238 | ctx.compress = true 239 | return next 240 | } 241 | } 242 | 243 | // Static returns a static file server middleware. 244 | func Static(root, fallback string) Handle { 245 | return func(ctx *Context) any { 246 | return FS(root, fallback) 247 | } 248 | } 249 | 250 | // Optional returns a middleware handler that executes the given handler only if the condition is true. 251 | func Optional(handle Handle, condition bool) Handle { 252 | if condition { 253 | return handle 254 | } 255 | return func(ctx *Context) any { 256 | // dummy handler 257 | return next 258 | } 259 | } 260 | 261 | // Chain returns a middleware handler that executes handlers in a chain. 262 | func Chain(middlewares ...Handle) Handle { 263 | if len(middlewares) == 0 { 264 | panic("no middlewares in the chain") 265 | } 266 | return func(ctx *Context) any { 267 | for _, mw := range middlewares { 268 | v := mw(ctx) 269 | if v != next { 270 | return v 271 | } 272 | } 273 | return next 274 | } 275 | } 276 | -------------------------------------------------------------------------------- /mux.go: -------------------------------------------------------------------------------- 1 | package rex 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "net/http" 7 | "runtime" 8 | "strings" 9 | "sync" 10 | "time" 11 | ) 12 | 13 | // Handle defines the API handle 14 | type Handle func(ctx *Context) any 15 | 16 | // Mux is a http.Handler with middlewares and routes. 17 | type Mux struct { 18 | contextPool sync.Pool 19 | writerPool sync.Pool 20 | middlewares []Handle 21 | router *http.ServeMux 22 | } 23 | 24 | // New returns a new Mux. 25 | func New() *Mux { 26 | return &Mux{ 27 | contextPool: sync.Pool{ 28 | New: func() any { 29 | return &Context{} 30 | }, 31 | }, 32 | writerPool: sync.Pool{ 33 | New: func() any { 34 | return &rexWriter{} 35 | }, 36 | }, 37 | } 38 | } 39 | 40 | // Use appends middlewares to current APIS middleware stack. 41 | func (a *Mux) Use(middlewares ...Handle) { 42 | for _, handle := range middlewares { 43 | if handle != nil { 44 | a.middlewares = append(a.middlewares, handle) 45 | } 46 | } 47 | } 48 | 49 | // AddRoute adds a route. 50 | func (a *Mux) AddRoute(pattern string, handle Handle) { 51 | // create the router on demand 52 | if a.router == nil { 53 | a.router = http.NewServeMux() 54 | } 55 | a.router.HandleFunc(pattern, func(w http.ResponseWriter, r *http.Request) { 56 | wr, ok := w.(*rexWriter) 57 | if ok { 58 | v := handle(wr.ctx) 59 | if v != nil { 60 | wr.ctx.respondWith(v) 61 | } 62 | } 63 | }) 64 | } 65 | 66 | // ServeHTTP implements the http Handler. 67 | func (a *Mux) ServeHTTP(w http.ResponseWriter, r *http.Request) { 68 | ctx := a.newContext(r) 69 | defer a.recycleContext(ctx) 70 | 71 | wr := a.newWriter(ctx, w) 72 | defer a.recycleWriter(wr) 73 | defer wr.Close() 74 | 75 | ctx.W = wr 76 | ctx.header = w.Header() 77 | ctx.header.Set("Connection", "keep-alive") 78 | 79 | if r.Method != "OPTIONS" { 80 | startTime := time.Now() 81 | defer func() { 82 | if ctx.accessLogger != nil { 83 | ref := r.Referer() 84 | if ref == "" { 85 | ref = "-" 86 | } 87 | ctx.accessLogger.Printf( 88 | `%s %s %s %s %s %d %s "%s" %d %d %dms`, 89 | ctx.RemoteIP(), 90 | r.Host, 91 | r.Proto, 92 | r.Method, 93 | r.RequestURI, 94 | r.ContentLength, 95 | ref, 96 | strings.ReplaceAll(r.UserAgent(), `"`, `\"`), 97 | wr.code, 98 | wr.writeN, 99 | time.Since(startTime)/time.Millisecond, 100 | ) 101 | } 102 | }() 103 | } 104 | 105 | defer func() { 106 | if v := recover(); v != nil { 107 | if err, ok := v.(*invalid); ok { 108 | ctx.W.WriteHeader(err.code) 109 | ctx.W.Write([]byte(err.message)) 110 | return 111 | } 112 | 113 | buf := bytes.NewBuffer(nil) 114 | for i := 3; ; i++ { 115 | pc, file, line, ok := runtime.Caller(i) 116 | if !ok { 117 | break 118 | } 119 | fmt.Fprint(buf, "\t", strings.TrimSpace(runtime.FuncForPC(pc).Name()), " ", file, ":", line, "\n") 120 | } 121 | 122 | if ctx.logger != nil { 123 | ctx.logger.Printf("[panic] %v\n%s", v, buf.String()) 124 | } 125 | ctx.W.WriteHeader(500) 126 | ctx.W.Write([]byte("Internal Server Error")) 127 | } 128 | }() 129 | 130 | for _, handle := range a.middlewares { 131 | v := handle(ctx) 132 | if v != next { 133 | ctx.respondWith(v) 134 | return 135 | } 136 | } 137 | 138 | if a.router != nil { 139 | a.router.ServeHTTP(wr, r) 140 | return 141 | } 142 | 143 | if r.Method == "GET" { 144 | ctx.respondWith(&status{404, "Not Found"}) 145 | return 146 | } 147 | 148 | ctx.respondWith(&status{405, "Method Not Allowed"}) 149 | } 150 | 151 | // newContext returns a new Context from the pool. 152 | func (a *Mux) newContext(r *http.Request) (ctx *Context) { 153 | ctx = a.contextPool.Get().(*Context) 154 | ctx.R = r 155 | ctx.sessionPool = defaultSessionPool 156 | ctx.sessionIdHandler = defaultSessionIdHandler 157 | ctx.logger = defaultLogger 158 | return 159 | } 160 | 161 | // recycleContext puts a Context back to the pool. 162 | func (a *Mux) recycleContext(ctx *Context) { 163 | ctx.R = nil 164 | ctx.W = nil 165 | ctx.header = nil 166 | ctx.basicAuthUser = "" 167 | ctx.aclUser = nil 168 | ctx.session = nil 169 | ctx.sessionPool = nil 170 | ctx.sessionIdHandler = nil 171 | ctx.logger = nil 172 | ctx.accessLogger = nil 173 | ctx.compress = false 174 | a.contextPool.Put(ctx) 175 | } 176 | 177 | // newWriter returns a new Writer from the pool. 178 | func (a *Mux) newWriter(ctx *Context, w http.ResponseWriter) (wr *rexWriter) { 179 | wr = a.writerPool.Get().(*rexWriter) 180 | wr.ctx = ctx 181 | wr.rawWriter = w 182 | wr.code = 200 183 | return 184 | } 185 | 186 | // recycleWriter puts a Writer back to the pool. 187 | func (a *Mux) recycleWriter(wr *rexWriter) { 188 | wr.ctx = nil 189 | wr.code = 0 190 | wr.isHeaderSent = false 191 | wr.writeN = 0 192 | wr.rawWriter = nil 193 | wr.zWriter = nil 194 | a.writerPool.Put(wr) 195 | } 196 | -------------------------------------------------------------------------------- /response.go: -------------------------------------------------------------------------------- 1 | package rex 2 | 3 | import ( 4 | "bytes" 5 | "io" 6 | "net/http" 7 | "os" 8 | "path" 9 | "strings" 10 | "text/template" 11 | "time" 12 | ) 13 | 14 | type invalid struct { 15 | code int 16 | message string 17 | } 18 | 19 | // Invalid returns an invalid error with code and message. 20 | func Invalid(code int, v ...string) any { 21 | var messsage string 22 | if len(v) > 0 { 23 | messsage = strings.Join(v, " ") 24 | } else { 25 | messsage = http.StatusText(code) 26 | } 27 | return &invalid{code, messsage} 28 | } 29 | 30 | // Error defines an error with code and message. 31 | type Error struct { 32 | Code int `json:"code"` 33 | Message string `json:"message"` 34 | } 35 | 36 | func (e *Error) Error() string { 37 | return e.Message 38 | } 39 | 40 | // Err returns an error with code and message. 41 | func Err(code int, v ...string) any { 42 | if code < 400 || code >= 600 { 43 | panic("invalid status code") 44 | } 45 | var messsage string 46 | if len(v) > 0 { 47 | messsage = strings.Join(v, " ") 48 | } else { 49 | messsage = http.StatusText(code) 50 | } 51 | return &Error{ 52 | Code: code, 53 | Message: messsage, 54 | } 55 | } 56 | 57 | type redirect struct { 58 | status int 59 | url string 60 | } 61 | 62 | // Redirect replies to the request with a redirect to url, 63 | // which may be a path relative to the request path. 64 | func Redirect(url string, status int) any { 65 | if url == "" { 66 | url = "/" 67 | } 68 | if status < 300 || status >= 400 { 69 | status = 302 70 | } 71 | return &redirect{status, url} 72 | } 73 | 74 | type status struct { 75 | code int 76 | content any 77 | } 78 | 79 | // Status replies to the request using the payload in the status. 80 | func Status(code int, content any) any { 81 | return &status{code, content} 82 | } 83 | 84 | // Template is an interface for template. 85 | type Template interface { 86 | Name() string 87 | Execute(wr io.Writer, data any) error 88 | } 89 | 90 | func Tpl(text string) Template { 91 | return template.Must(template.New("index.html").Parse(text)) 92 | } 93 | 94 | // Render renders the template with the given data. 95 | func Render(t Template, data any) any { 96 | buf := bytes.NewBuffer(nil) 97 | if err := t.Execute(buf, data); err != nil { 98 | panic(&invalid{500, err.Error()}) 99 | } 100 | 101 | return &content{ 102 | name: t.Name(), 103 | content: bytes.NewReader(buf.Bytes()), 104 | } 105 | } 106 | 107 | type noContent struct{} 108 | 109 | // NoContent replies to the request with no content. 110 | func NoContent() any { 111 | return &noContent{} 112 | } 113 | 114 | type content struct { 115 | name string 116 | mtime time.Time 117 | content io.Reader 118 | } 119 | 120 | // Content replies to the request using the content in the provided Reader. 121 | func Content(name string, mtime time.Time, r io.Reader) any { 122 | return &content{name, mtime, r} 123 | } 124 | 125 | // HTML replies to the request with a html content. 126 | func HTML(html string) any { 127 | return &content{ 128 | name: "index.html", 129 | content: bytes.NewReader([]byte(html)), 130 | } 131 | } 132 | 133 | // File replies to the request using the file content. 134 | func File(name string) any { 135 | fi, err := os.Stat(name) 136 | if err != nil { 137 | if os.IsNotExist(err) { 138 | panic(&invalid{404, "file not found"}) 139 | } 140 | panic(&invalid{500, err.Error()}) 141 | } 142 | if fi.IsDir() { 143 | panic(&invalid{400, "is a directory"}) 144 | } 145 | 146 | file, err := os.Open(name) 147 | if err != nil { 148 | panic(&invalid{500, err.Error()}) 149 | } 150 | 151 | return &content{path.Base(name), fi.ModTime(), file} 152 | } 153 | 154 | type fs struct { 155 | root string 156 | fallback string 157 | } 158 | 159 | // FS replies to the request with the contents of the file system rooted at root. 160 | func FS(root string, fallback string) any { 161 | fi, err := os.Lstat(root) 162 | if err != nil { 163 | panic(&invalid{500, err.Error()}) 164 | } 165 | if !fi.IsDir() { 166 | panic(&invalid{500, "FS root is not a directory"}) 167 | } 168 | return &fs{root, fallback} 169 | } 170 | -------------------------------------------------------------------------------- /serve.go: -------------------------------------------------------------------------------- 1 | // Package rex provides a simple & light-weight REST server in golang 2 | package rex 3 | 4 | import ( 5 | "context" 6 | "fmt" 7 | "net/http" 8 | "os" 9 | "time" 10 | 11 | "golang.org/x/crypto/acme/autocert" 12 | ) 13 | 14 | // ServerConfig contains options to run the REX server. 15 | type ServerConfig struct { 16 | Host string 17 | Port uint16 18 | TLS TLSConfig 19 | ReadTimeout uint32 20 | WriteTimeout uint32 21 | MaxHeaderBytes uint32 22 | } 23 | 24 | // TLSConfig contains options to support https. 25 | type TLSConfig struct { 26 | Port uint16 27 | CertFile string 28 | KeyFile string 29 | AutoTLS AutoTLSConfig 30 | AutoRedirect bool 31 | } 32 | 33 | // AutoTLSConfig contains options to support autocert by Let's Encrypto SSL. 34 | type AutoTLSConfig struct { 35 | AcceptTOS bool 36 | Hosts []string 37 | CacheDir string 38 | Cache autocert.Cache 39 | } 40 | 41 | // serve starts a REX server. 42 | func serve(ctx context.Context, config *ServerConfig, c chan error) { 43 | port := config.Port 44 | if port == 0 { 45 | port = 80 46 | } 47 | serv := &http.Server{ 48 | Addr: fmt.Sprintf(("%s:%d"), config.Host, port), 49 | Handler: defaultMux, 50 | ReadTimeout: time.Duration(config.ReadTimeout) * time.Second, 51 | WriteTimeout: time.Duration(config.WriteTimeout) * time.Second, 52 | MaxHeaderBytes: int(config.MaxHeaderBytes), 53 | } 54 | if ctx != nil { 55 | go func() { 56 | <-ctx.Done() 57 | serv.Close() 58 | }() 59 | } 60 | c <- serv.ListenAndServe() 61 | } 62 | 63 | // serveTLS starts a REX server with TLS. 64 | func serveTLS(ctx context.Context, config *ServerConfig, c chan error) { 65 | tls := config.TLS 66 | port := tls.Port 67 | if port == 0 { 68 | port = 443 69 | } 70 | serv := &http.Server{ 71 | Addr: fmt.Sprintf(("%s:%d"), config.Host, port), 72 | Handler: defaultMux, 73 | ReadTimeout: time.Duration(config.ReadTimeout) * time.Second, 74 | WriteTimeout: time.Duration(config.WriteTimeout) * time.Second, 75 | MaxHeaderBytes: int(config.MaxHeaderBytes), 76 | } 77 | if tls.AutoTLS.AcceptTOS { 78 | m := &autocert.Manager{ 79 | Prompt: autocert.AcceptTOS, 80 | } 81 | if tls.AutoTLS.Cache != nil { 82 | m.Cache = tls.AutoTLS.Cache 83 | } else if cacheDir := tls.AutoTLS.CacheDir; cacheDir != "" { 84 | fi, err := os.Stat(cacheDir) 85 | if err == nil && !fi.IsDir() { 86 | c <- fmt.Errorf("AutoTLS: invalid cache dir '%s'", cacheDir) 87 | return 88 | } 89 | if err != nil && os.IsNotExist(err) { 90 | err = os.MkdirAll(cacheDir, 0755) 91 | if err != nil { 92 | c <- fmt.Errorf("AutoTLS: can't create the cache dir '%s'", cacheDir) 93 | return 94 | } 95 | } 96 | m.Cache = autocert.DirCache(cacheDir) 97 | } 98 | if len(tls.AutoTLS.Hosts) > 0 { 99 | m.HostPolicy = autocert.HostWhitelist(tls.AutoTLS.Hosts...) 100 | } 101 | serv.TLSConfig = m.TLSConfig() 102 | } 103 | if ctx != nil { 104 | go func() { 105 | <-ctx.Done() 106 | serv.Close() 107 | }() 108 | } 109 | c <- serv.ListenAndServeTLS(tls.CertFile, tls.KeyFile) 110 | } 111 | 112 | // Serve serves a REX server. 113 | func Serve(config ServerConfig) chan error { 114 | c := make(chan error, 1) 115 | 116 | if tls := config.TLS; tls.AutoTLS.AcceptTOS || (tls.CertFile != "" && tls.KeyFile != "") { 117 | c2 := make(chan error, 1) 118 | ctx, cancel := context.WithCancel(context.Background()) 119 | go serve(ctx, &config, c2) 120 | go serveTLS(ctx, &config, c2) 121 | err := <-c2 122 | cancel() 123 | c <- err 124 | } else { 125 | go serve(context.Background(), &config, c) 126 | } 127 | 128 | return c 129 | } 130 | 131 | // Start starts a REX server. 132 | func Start(port uint16) chan error { 133 | c := make(chan error, 1) 134 | go serve(context.Background(), &ServerConfig{ 135 | Port: port, 136 | }, c) 137 | return c 138 | } 139 | 140 | // StartWithTLS starts a REX server with TLS. 141 | func StartWithTLS(port uint16, certFile string, keyFile string) chan error { 142 | c := make(chan error, 1) 143 | go serveTLS(context.Background(), &ServerConfig{ 144 | TLS: TLSConfig{ 145 | Port: port, 146 | CertFile: certFile, 147 | KeyFile: keyFile, 148 | }, 149 | }, c) 150 | return c 151 | } 152 | 153 | // StartWithAutoTLS starts a REX server with autocert powered by Let's Encrypto SSL 154 | func StartWithAutoTLS(port uint16, hosts ...string) chan error { 155 | c := make(chan error, 1) 156 | go serveTLS(context.Background(), &ServerConfig{ 157 | TLS: TLSConfig{ 158 | Port: port, 159 | AutoTLS: AutoTLSConfig{ 160 | AcceptTOS: true, 161 | Hosts: hosts, 162 | CacheDir: "/var/rex/autotls", 163 | }, 164 | }, 165 | }, c) 166 | return c 167 | } 168 | -------------------------------------------------------------------------------- /session.go: -------------------------------------------------------------------------------- 1 | package rex 2 | 3 | import ( 4 | "github.com/ije/rex/session" 5 | ) 6 | 7 | // SessionStub is a stub for a session 8 | type SessionStub struct { 9 | session.Session 10 | } 11 | 12 | // SID returns the sid 13 | func (s *SessionStub) SID() string { 14 | return s.Session.SID() 15 | } 16 | 17 | // Has checks a value exists 18 | func (s *SessionStub) Has(key string) bool { 19 | ok, err := s.Session.Has(key) 20 | if err != nil { 21 | panic(&invalid{500, err.Error()}) 22 | } 23 | return ok 24 | } 25 | 26 | // Get returns a session value 27 | func (s *SessionStub) Get(key string) []byte { 28 | value, err := s.Session.Get(key) 29 | if err != nil { 30 | panic(&invalid{500, err.Error()}) 31 | } 32 | return value 33 | } 34 | 35 | // Set sets a session value 36 | func (s *SessionStub) Set(key string, value []byte) { 37 | err := s.Session.Set(key, value) 38 | if err != nil { 39 | panic(&invalid{500, err.Error()}) 40 | } 41 | } 42 | 43 | // Delete removes a session value 44 | func (s *SessionStub) Delete(key string) { 45 | err := s.Session.Delete(key) 46 | if err != nil { 47 | panic(&invalid{500, err.Error()}) 48 | } 49 | } 50 | 51 | // Flush flushes all session values 52 | func (s *SessionStub) Flush() { 53 | err := s.Session.Flush() 54 | if err != nil { 55 | panic(&invalid{500, err.Error()}) 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /session/pool.go: -------------------------------------------------------------------------------- 1 | package session 2 | 3 | // Pool interface represents a session pool. 4 | type Pool interface { 5 | GetSession(sid string) (Session, error) 6 | Destroy(sid string) error 7 | } 8 | -------------------------------------------------------------------------------- /session/pool_memory.go: -------------------------------------------------------------------------------- 1 | package session 2 | 3 | import ( 4 | "sync" 5 | "time" 6 | 7 | "github.com/ije/gox/crypto/rand" 8 | ) 9 | 10 | type MemorySession struct { 11 | lock sync.RWMutex 12 | store map[string][]byte 13 | sid string 14 | expires time.Time 15 | } 16 | 17 | // SID returns the sid 18 | func (ms *MemorySession) SID() string { 19 | return ms.sid 20 | } 21 | 22 | // Has checks a value exists 23 | func (ms *MemorySession) Has(key string) (ok bool, err error) { 24 | ms.lock.RLock() 25 | _, ok = ms.store[key] 26 | ms.lock.RUnlock() 27 | 28 | return 29 | } 30 | 31 | // Get returns a session value 32 | func (ms *MemorySession) Get(key string) (value []byte, err error) { 33 | ms.lock.RLock() 34 | value = ms.store[key] 35 | ms.lock.RUnlock() 36 | 37 | return 38 | } 39 | 40 | // Set sets a session value 41 | func (ms *MemorySession) Set(key string, value []byte) error { 42 | ms.lock.Lock() 43 | ms.store[key] = value 44 | ms.lock.Unlock() 45 | 46 | return nil 47 | } 48 | 49 | // Delete removes a session value 50 | func (ms *MemorySession) Delete(key string) error { 51 | ms.lock.Lock() 52 | delete(ms.store, key) 53 | ms.lock.Unlock() 54 | 55 | return nil 56 | } 57 | 58 | // Flush flushes all session values 59 | func (ms *MemorySession) Flush() error { 60 | ms.lock.Lock() 61 | ms.store = map[string][]byte{} 62 | ms.lock.Unlock() 63 | 64 | return nil 65 | } 66 | 67 | type MemorySessionPool struct { 68 | lock sync.RWMutex 69 | sessions map[string]*MemorySession 70 | ttl time.Duration 71 | } 72 | 73 | // NewMemorySessionPool returns a new MemorySessionPool 74 | func NewMemorySessionPool(lifetime time.Duration) *MemorySessionPool { 75 | pool := &MemorySessionPool{ 76 | sessions: map[string]*MemorySession{}, 77 | ttl: lifetime, 78 | } 79 | if lifetime > time.Second { 80 | go pool.gcLoop() 81 | } 82 | return pool 83 | } 84 | 85 | // GetSession returns a session by sid 86 | func (pool *MemorySessionPool) GetSession(sid string) (session Session, err error) { 87 | pool.lock.RLock() 88 | ms, ok := pool.sessions[sid] 89 | pool.lock.RUnlock() 90 | 91 | now := time.Now() 92 | if ok && ms.expires.Before(now) { 93 | pool.lock.Lock() 94 | delete(pool.sessions, sid) 95 | pool.lock.Unlock() 96 | ok = false 97 | } 98 | 99 | if !ok { 100 | RE: 101 | sid = rand.Base64.String(64) 102 | pool.lock.RLock() 103 | _, ok := pool.sessions[sid] 104 | pool.lock.RUnlock() 105 | if ok { 106 | goto RE 107 | } 108 | 109 | ms = &MemorySession{ 110 | sid: sid, 111 | expires: now.Add(pool.ttl), 112 | store: map[string][]byte{}, 113 | } 114 | pool.lock.Lock() 115 | pool.sessions[sid] = ms 116 | pool.lock.Unlock() 117 | } else { 118 | ms.expires = now.Add(pool.ttl) 119 | } 120 | 121 | session = ms 122 | return 123 | } 124 | 125 | // Destroy destroys a session by sid 126 | func (pool *MemorySessionPool) Destroy(sid string) error { 127 | pool.lock.Lock() 128 | delete(pool.sessions, sid) 129 | pool.lock.Unlock() 130 | 131 | return nil 132 | } 133 | 134 | func (pool *MemorySessionPool) gcLoop() { 135 | t := time.NewTicker(pool.ttl) 136 | for { 137 | <-t.C 138 | pool.gc() 139 | } 140 | } 141 | 142 | func (pool *MemorySessionPool) gc() error { 143 | now := time.Now() 144 | 145 | pool.lock.RLock() 146 | defer pool.lock.RUnlock() 147 | 148 | for sid, session := range pool.sessions { 149 | if session.expires.Before(now) { 150 | pool.lock.RUnlock() 151 | pool.lock.Lock() 152 | delete(pool.sessions, sid) 153 | pool.lock.Unlock() 154 | pool.lock.RLock() 155 | } 156 | } 157 | 158 | return nil 159 | } 160 | 161 | func init() { 162 | var _ Pool = (*MemorySessionPool)(nil) 163 | } 164 | -------------------------------------------------------------------------------- /session/session.go: -------------------------------------------------------------------------------- 1 | package session 2 | 3 | // Session interface represents a http session. 4 | type Session interface { 5 | // SID returns the sid. 6 | SID() string 7 | // Has checks a value exists. 8 | Has(key string) (ok bool, err error) 9 | // Get returns a session valuen. 10 | Get(key string) (value []byte, err error) 11 | // Set sets a session value. 12 | Set(key string, value []byte) error 13 | // Delete removes a session value. 14 | Delete(key string) error 15 | // Flush flushes all session values. 16 | Flush() error 17 | } 18 | -------------------------------------------------------------------------------- /session/sid.go: -------------------------------------------------------------------------------- 1 | package session 2 | 3 | import ( 4 | "net/http" 5 | "strings" 6 | ) 7 | 8 | // A SidHandler to handle session id 9 | type SidHandler interface { 10 | Get(r *http.Request) string 11 | Put(w http.ResponseWriter, id string) 12 | } 13 | 14 | // A CookieSidHandler to handle session id by http cookie header 15 | type CookieSidHandler struct { 16 | cookieName string 17 | } 18 | 19 | // NewCookieSidHandler returns a new CookieIdHandler 20 | func NewCookieSidHandler(cookieName string) *CookieSidHandler { 21 | return &CookieSidHandler{cookieName: strings.TrimSpace(cookieName)} 22 | } 23 | 24 | // CookieName returns cookie name 25 | func (s *CookieSidHandler) CookieName() string { 26 | name := strings.TrimSpace(s.cookieName) 27 | if name == "" { 28 | name = "SID" 29 | } 30 | return name 31 | } 32 | 33 | // Get return seesion id by http cookie 34 | func (s *CookieSidHandler) Get(r *http.Request) string { 35 | cookie, err := r.Cookie(s.CookieName()) 36 | if err != nil { 37 | return "" 38 | } 39 | return cookie.Value 40 | } 41 | 42 | // Put sets seesion id by http cookie 43 | func (s *CookieSidHandler) Put(w http.ResponseWriter, id string) { 44 | cookie := &http.Cookie{ 45 | Name: s.CookieName(), 46 | Value: id, 47 | HttpOnly: true, 48 | } 49 | w.Header().Add("Set-Cookie", cookie.String()) 50 | } 51 | -------------------------------------------------------------------------------- /writer.go: -------------------------------------------------------------------------------- 1 | package rex 2 | 3 | import ( 4 | "bufio" 5 | "errors" 6 | "io" 7 | "net" 8 | "net/http" 9 | ) 10 | 11 | // A Writer implements the http.ResponseWriter interface. 12 | type rexWriter struct { 13 | ctx *Context 14 | code int 15 | isHeaderSent bool 16 | writeN int 17 | rawWriter http.ResponseWriter 18 | zWriter io.WriteCloser 19 | } 20 | 21 | // Hijack lets the caller take over the connection. 22 | func (w *rexWriter) Hijack() (net.Conn, *bufio.ReadWriter, error) { 23 | h, ok := w.rawWriter.(http.Hijacker) 24 | if ok { 25 | return h.Hijack() 26 | } 27 | 28 | return nil, nil, errors.New("the raw response writer does not implement the http.Hijacker") 29 | } 30 | 31 | // Flush sends any buffered data to the client. 32 | func (w *rexWriter) Flush() { 33 | f, ok := w.rawWriter.(http.Flusher) 34 | if ok { 35 | f.Flush() 36 | } 37 | } 38 | 39 | // Header returns the header map that will be sent by WriteHeader. 40 | func (w *rexWriter) Header() http.Header { 41 | return w.rawWriter.Header() 42 | } 43 | 44 | // WriteHeader sends a HTTP response header with the provided status code. 45 | func (w *rexWriter) WriteHeader(code int) { 46 | if !w.isHeaderSent { 47 | w.rawWriter.WriteHeader(code) 48 | w.code = code 49 | w.isHeaderSent = true 50 | } 51 | } 52 | 53 | // Write writes the data to the connection as part of an HTTP reply. 54 | func (w *rexWriter) Write(p []byte) (n int, err error) { 55 | if !w.isHeaderSent { 56 | w.isHeaderSent = true 57 | } 58 | var wr io.Writer = w.rawWriter 59 | if w.zWriter != nil { 60 | wr = w.zWriter 61 | } 62 | n, err = wr.Write(p) 63 | if n > 0 { 64 | w.writeN += n 65 | } 66 | return 67 | } 68 | 69 | // Close closes the underlying connection. 70 | func (w *rexWriter) Close() error { 71 | if w.zWriter != nil { 72 | return w.zWriter.Close() 73 | } 74 | return nil 75 | } 76 | --------------------------------------------------------------------------------