├── .github
└── workflows
│ ├── codeql-analysis.yml
│ └── snyk.yml
├── .gitignore
├── CODE_OF_CONDUCT.md
├── LICENSE
├── README.md
├── _config.yml
├── content.go
├── context.go
├── doc.go
├── encoder.go
├── encoder
└── xml
│ ├── README.md
│ ├── xml.go
│ └── xml_test.go
├── errors.go
├── example
├── example_test.go
└── logrus
│ ├── README.md
│ └── logrus.go
├── filter.go
├── filter
├── authbasic
│ ├── authbasic.go
│ └── doc.go
├── cors
│ ├── cors.go
│ └── doc.go
├── etag
│ ├── doc.go
│ └── etag.go
├── gzip
│ ├── doc.go
│ └── gzip.go
├── limits
│ ├── container.go
│ ├── container_redis.go
│ ├── doc.go
│ ├── example_test.go
│ ├── memory.go
│ ├── throttle.go
│ ├── usage.go
│ └── util.go
├── logs
│ ├── doc.go
│ └── logs.go
├── multipart
│ ├── doc.go
│ └── multipart.go
├── override
│ ├── doc.go
│ └── override.go
└── security
│ ├── doc.go
│ └── security.go
├── go.mod
├── go.sum
├── linking.go
├── resource.go
├── response_buffer.go
├── router.go
├── router_test.go
├── service.go
└── util.go
/.github/workflows/codeql-analysis.yml:
--------------------------------------------------------------------------------
1 | # For most projects, this workflow file will not need changing; you simply need
2 | # to commit it to your repository.
3 | #
4 | # You may wish to alter this file to override the set of languages analyzed,
5 | # or to provide custom queries or build logic.
6 | #
7 | # ******** NOTE ********
8 | # We have attempted to detect the languages in your repository. Please check
9 | # the `language` matrix defined below to confirm you have the correct set of
10 | # supported CodeQL languages.
11 | #
12 | name: "CodeQL"
13 |
14 | on:
15 | push:
16 | branches: [ master, develop ]
17 | pull_request:
18 | # The branches below must be a subset of the branches above
19 | branches: [ master ]
20 | schedule:
21 | - cron: '28 8 * * 4'
22 |
23 | jobs:
24 | analyze:
25 | name: Analyze
26 | runs-on: ubuntu-latest
27 | permissions:
28 | actions: read
29 | contents: read
30 | security-events: write
31 |
32 | strategy:
33 | fail-fast: false
34 | matrix:
35 | language: [ 'go' ]
36 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ]
37 | # Learn more:
38 | # https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed
39 |
40 | steps:
41 | - name: Checkout repository
42 | uses: actions/checkout@v2
43 |
44 | # Initializes the CodeQL tools for scanning.
45 | - name: Initialize CodeQL
46 | uses: github/codeql-action/init@v1
47 | with:
48 | languages: ${{ matrix.language }}
49 | # If you wish to specify custom queries, you can do so here or in a config file.
50 | # By default, queries listed here will override any specified in a config file.
51 | # Prefix the list here with "+" to use these queries and those in the config file.
52 | # queries: ./path/to/local/query, your-org/your-repo/queries@main
53 |
54 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
55 | # If this step fails, then you should remove it and run the build manually (see below)
56 | - name: Autobuild
57 | uses: github/codeql-action/autobuild@v1
58 |
59 | # ℹ️ Command-line programs to run using the OS shell.
60 | # 📚 https://git.io/JvXDl
61 |
62 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
63 | # and modify them (or add more) to build your code if your project
64 | # uses a compiled language
65 |
66 | #- run: |
67 | # make bootstrap
68 | # make release
69 |
70 | - name: Perform CodeQL Analysis
71 | uses: github/codeql-action/analyze@v1
72 |
--------------------------------------------------------------------------------
/.github/workflows/snyk.yml:
--------------------------------------------------------------------------------
1 | name: Snyk
2 | on: push
3 | jobs:
4 | security:
5 | runs-on: ubuntu-latest
6 | steps:
7 | - uses: actions/checkout@master
8 | - name: Run Snyk to check for vulnerabilities
9 | uses: snyk/actions/golang@master
10 | env:
11 | SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }}
12 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *~
2 | _*
3 |
4 | # Test binary, build with `go test -c`
5 | *.test
6 |
7 | # Output of the go coverage tool, specifically when used with LiteIDE
8 | *.out
9 |
10 | # Keep vendor iff we are using govendor
11 | vendor/*
12 | !vendor/vendor.json
13 |
14 |
--------------------------------------------------------------------------------
/CODE_OF_CONDUCT.md:
--------------------------------------------------------------------------------
1 | # Contributor Covenant Code of Conduct
2 |
3 | ## Our Pledge
4 |
5 | In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation.
6 |
7 | ## Our Standards
8 |
9 | Examples of behavior that contributes to creating a positive environment include:
10 |
11 | * Using welcoming and inclusive language
12 | * Being respectful of differing viewpoints and experiences
13 | * Gracefully accepting constructive criticism
14 | * Focusing on what is best for the community
15 | * Showing empathy towards other community members
16 |
17 | Examples of unacceptable behavior by participants include:
18 |
19 | * The use of sexualized language or imagery and unwelcome sexual attention or advances
20 | * Trolling, insulting/derogatory comments, and personal or political attacks
21 | * Public or private harassment
22 | * Publishing others' private information, such as a physical or electronic address, without explicit permission
23 | * Other conduct which could reasonably be considered inappropriate in a professional setting
24 |
25 | ## Our Responsibilities
26 |
27 | Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior.
28 |
29 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful.
30 |
31 | ## Scope
32 |
33 | This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers.
34 |
35 | ## Enforcement
36 |
37 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at github@codehack.com. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately.
38 |
39 | Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership.
40 |
41 | ## Attribution
42 |
43 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version]
44 |
45 | [homepage]: http://contributor-covenant.org
46 | [version]: http://contributor-covenant.org/version/1/4/
47 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2014-present, Codehack, LLC. All rights reserved.
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, 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,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Go-Relax [](https://pkg.go.dev/github.com/srfrog/go-relax) [](https://goreportcard.com/report/github.com/srfrog/go-relax)
2 |
3 | *Build fast and complete RESTful APIs in [Go](http://golang.org)*
4 |
5 | *Go-Relax* aims to provide the tools to help developers build RESTful web services, and information needed to abide by [REST](https://en.wikipedia.org/wiki/REST) architectural constraints using correct [HTTP semantics](http://tools.ietf.org/html/rfc7231).
6 |
7 | ## Quick Start
8 |
9 | Install using "go get":
10 |
11 | go get github.com/srfrog/go-relax
12 |
13 | Then import from your source:
14 |
15 | import "github.com/srfrog/go-relax"
16 |
17 | View [example_test.go](https://github.com/srfrog/go-relax/blob/master/example/example_test.go) for an extended example of basic usage and features.
18 |
19 | Also, check the [wiki](https://github.com/srfrog/go-relax/wiki) for HowTo's and recipes.
20 |
21 | ## Features
22 |
23 | - Helps build API's that follow the REST concept using ROA principles.
24 | - Built-in support of HATEOAS constraint with Web Linking header tags.
25 | - Follows REST "best practices", with inspiration from Heroku and GitHub.
26 | - Works fine along with ``http.ServeMux`` or independently as ``http.Handler``
27 | - Supports different media types, and **mixed** for requests and responses.
28 | - It uses **JSON** media type by default, but also includes XML (needs import).
29 | - The default routing engine uses **trie with regexp matching** for speed and flexibility.
30 | - Comes with a complete set of filters to build a working API. _"Batteries included"_
31 | - Uses ``sync.pool`` to efficiently use resources when under heavy load.
32 |
33 | #### Included filters
34 |
35 | - [x] Content - handles mixed request/response encodings, language preference, and versioning.
36 | - [x] Basic authentication - to protect any resource with passwords.
37 | - [x] CORS - Cross-Origin Resource Sharing, for remote client-server setups.
38 | - [x] ETag - entity tagging with conditional requests for efficient caching.
39 | - [x] GZip - Dynamic gzip content data compression, with ETag support.
40 | - [x] Logging - custom logging with pre- and post- request event support.
41 | - [x] Method override - GET/POST method override via HTTP header and query string.
42 | - [x] Security - Various security practices for request handling.
43 | - [x] Limits - request throttler, token-based rate limiter, and memory limits.
44 |
45 | #### Upcoming filters
46 |
47 | - [ ] JSON-API support.
48 | - [ ] JSON-Schema for validating requests and responses.
49 | - [ ] Collection-JSON support.
50 |
51 | ## Documentation
52 |
53 | The full code documentation is located at GoDoc:
54 |
55 | [https://pkg.go.dev/github.com/srfrog/go-relax](https://pkg.go.dev/github.com/srfrog/go-relax)
56 |
57 | The source code is thoroughly commented, have a look.
58 |
59 | ## Hello World
60 |
61 | This minimal example creates a new Relax service that handles a Hello resource.
62 | ```go
63 | package main
64 |
65 | import (
66 | "github.com/srfrog/go-relax"
67 | )
68 |
69 | type Hello string
70 |
71 | func (h *Hello) Index(ctx *relax.Context) {
72 | ctx.Respond(h)
73 | }
74 |
75 | func main() {
76 | h := Hello("hello world!")
77 | svc := relax.NewService("http://api.company.com/")
78 | svc.Resource(&h)
79 | svc.Run()
80 | }
81 | ```
82 |
83 | **$ curl -i -X GET http://api.company.com/hello**
84 |
85 | Response:
86 |
87 | ```
88 | HTTP/1.1 200 OK
89 | Content-Type: application/json;charset=utf-8
90 | Link: ; rel="self"
91 | Link: ; rel="index"
92 | Request-Id: 61d430de-7bb6-4ff8-84da-aff6fe81c0d2
93 | Server: Go-Relax/0.5.0
94 | Date: Thu, 14 Aug 2014 06:20:48 GMT
95 | Content-Length: 14
96 |
97 | "hello world!"
98 | ```
99 |
100 | ## Credits
101 |
102 | **Go-Relax** is Copyright (c) [Codehack](http://codehack.com).
103 | Published under an [MIT License](https://raw.githubusercontent.com/srfrog/go-relax/master/LICENSE)
104 |
--------------------------------------------------------------------------------
/_config.yml:
--------------------------------------------------------------------------------
1 | theme: jekyll-theme-minimal
--------------------------------------------------------------------------------
/content.go:
--------------------------------------------------------------------------------
1 | // Copyright 2014 Codehack http://codehack.com
2 | // Use of this source code is governed by a MIT-style
3 | // license that can be found in the LICENSE file.
4 |
5 | package relax
6 |
7 | import (
8 | "mime"
9 | "net/http"
10 | "strings"
11 | )
12 |
13 | const (
14 | defaultMediatype = "application/vnd.codehack.relax"
15 |
16 | defaultVersion = "current"
17 |
18 | defaultLanguage = "en-US"
19 | )
20 |
21 | /*
22 | Content does content negotiation to select the supported representations
23 | for the request and response. The default representation uses media type
24 | "application/json". If new media types are available to the service, a client
25 | can request it via the Accept header. The format of the Accept header uses
26 | the following vendor extension:
27 |
28 | Accept: application/vnd.relax+{subtype}; version={version}; lang={language}
29 |
30 | The values for {subtype}, {version} and {language} are optional. They correspond
31 | in order; to media subtype, content version and, language. If any value is missing
32 | or unsupported the default values are used. If a request Accept header is not
33 | using the vendor extension, the default values are used:
34 |
35 | Accept: application/vnd.relax+json; version="current"; lang="en"
36 |
37 | By decoupling version and lang from the media type, it allows us to have separate
38 | versions for the same resource and with individual language coverage.
39 |
40 | When Accept indicates all media types "*C;*", the media subtype can be requested
41 | through the URL path's extension. If the service doesn't support the media encoding,
42 | then it will respond with an HTTP error code.
43 |
44 | GET /api/v1/tickets.xml
45 | GET /company/users/123.json
46 |
47 | Note that the extension should be appended to a collection or a resource item.
48 | The extension is removed before the request is dispatched to the routing engine.
49 |
50 | If the request header Accept-Language is found, the value for content language
51 | is automatically set to that. The underlying application should use this to
52 | construct a proper respresentation in that language.
53 |
54 | Content passes down the following info to filters:
55 |
56 | ctx.Get("content.encoding") // media type used for encoding
57 | ctx.Get("content.decoding") // Type used in payload requests POST/PUT/PATCH
58 | ctx.Get("content.version") // requested version, or "current"
59 | ctx.Get("content.language") // requested language, or "en-US"
60 |
61 | Requests and responses can use mixed representations if the service supports the
62 | media types.
63 |
64 | See also, http://tools.ietf.org/html/rfc5646; tags to identify languages.
65 | */
66 | var Content struct {
67 | // MediaType is the vendor extended media type used by this framework.
68 | // Default: application/vnd.codehack.relax
69 | Mediatype string
70 | // Version is the version used when no content version is requested.
71 | // Default: current
72 | Version string
73 | // Language is the language used when no content language is requested.
74 | // Default: en-US
75 | Language string
76 | }
77 |
78 | // content is the function that does the actual content-negotiation described above.
79 | func (svc *Service) content(next HandlerFunc) HandlerFunc {
80 | // JSON is our default representation.
81 | json := svc.encoders["application/json"]
82 |
83 | return func(ctx *Context) {
84 | ctx.Encode = json.Encode
85 | ctx.Decode = json.Decode
86 |
87 | encoder := json
88 |
89 | version := acceptVersion(ctx.Request.Header.Get("Accept-Version"))
90 |
91 | language := acceptLanguage(ctx.Request.Header.Get("Accept-Language"))
92 |
93 | accept := ctx.Request.Header.Get("Accept")
94 | if accept == "*/*" {
95 | // Check if subtype is in the requested URL path's extension.
96 | // Path: /api/v1/users.xml
97 | if ext := PathExt(ctx.Request.URL.Path); ext != "" {
98 | // remove extension from path.
99 | ctx.Request.URL.Path = strings.TrimSuffix(ctx.Request.URL.Path, ext)
100 | // create vendor media type and fallthrough
101 | accept = Content.Mediatype + "+" + ext[1:]
102 | }
103 | }
104 |
105 | // We check our vendor media type for requests of a specific subtype.
106 | // Everything else will default to "application/json" (see above).
107 | if strings.HasPrefix(accept, Content.Mediatype) {
108 | // Accept: application/vnd.relax+{subtype}; version={version}; lang={lang}
109 | mt, op, err := mime.ParseMediaType(accept)
110 | if err != nil {
111 | ctx.Header().Set("Content-Type", json.ContentType())
112 | ctx.Error(http.StatusBadRequest, err.Error())
113 | return
114 | }
115 | // check for media subtype (encoding) request.
116 | if idx := strings.Index(mt, "+"); idx != -1 {
117 | tbe := mime.TypeByExtension("." + mt[idx+1:])
118 | enc, ok := svc.encoders[tbe]
119 | if !ok {
120 | ctx.Header().Set("Content-Type", json.ContentType())
121 | ctx.Error(http.StatusNotAcceptable,
122 | "That media type is not supported for response.",
123 | "You may use type '"+json.Accept()+"'")
124 | return
125 | }
126 | encoder = enc
127 | ctx.Encode = encoder.Encode
128 | }
129 |
130 | // If version or language were specified they are preferred over Accept-* headers.
131 | if v, ok := op["version"]; ok {
132 | version = v
133 | }
134 | if v, ok := op["lang"]; ok {
135 | language = v
136 | }
137 | }
138 |
139 | // At this point we know the response media type.
140 | ctx.Header().Set("Content-Type", encoder.ContentType())
141 |
142 | // Pass the info down to other handlers.
143 | ctx.Set("content.encoding", encoder.Accept())
144 | ctx.Set("content.version", version)
145 | ctx.Set("content.language", language)
146 |
147 | // Now check for payload representation for unsafe methods: POST PUT PATCH.
148 | if ctx.Request.Method[0] == 'P' {
149 | // Content-Type: application/{subtype}
150 | ct, _, err := mime.ParseMediaType(ctx.Request.Header.Get("Content-Type"))
151 | if err != nil {
152 | ctx.Error(http.StatusBadRequest, err.Error())
153 | return
154 | }
155 | decoder, ok := svc.encoders[ct]
156 | if !ok {
157 | ctx.Error(http.StatusUnsupportedMediaType,
158 | "That media type is not supported for transfer.",
159 | "You may use type '"+json.Accept()+"'")
160 | return
161 | }
162 | ctx.Decode = decoder.Decode
163 | ctx.Set("content.decoding", ct)
164 | }
165 |
166 | next(ctx)
167 | }
168 | }
169 |
170 | // acceptVersion checks for specific version in Accept-Version HTTP header.
171 | // returns the version requested or Content.Version if none is set.
172 | //
173 | // Accept-Version: v1
174 | func acceptVersion(version string) string {
175 | if version == "" {
176 | return Content.Version
177 | }
178 | return version
179 | }
180 |
181 | // acceptLanguage checks for language preferences in Accept-Language header.
182 | // It returns the language code with highest quality. If none are set, returns
183 | // Content.Language global default.
184 | //
185 | // Accept-Language: da, jp;q=0.8, en;q=0.9
186 | func acceptLanguage(value string) string {
187 | if value == "" {
188 | return Content.Language
189 | }
190 |
191 | langcode := Content.Language
192 |
193 | prefs, err := ParsePreferences(value)
194 | // If language parsing fails, continue with request.
195 | // See https://tools.ietf.org/html/rfc7231#section-5.3.5
196 | if err == nil {
197 | // If langcode is not listed, give it a competitive value for sanity.
198 | // The value most likely is still "en" (English).
199 | if _, ok := prefs[langcode]; !ok {
200 | prefs[langcode] = 0.85
201 | }
202 | for code, value := range prefs {
203 | if value > prefs[langcode] {
204 | langcode = code
205 | }
206 | }
207 | }
208 | return langcode
209 | }
210 |
211 | func init() {
212 | // Set content defaults
213 | Content.Mediatype = defaultMediatype
214 | Content.Version = defaultVersion
215 | Content.Language = defaultLanguage
216 |
217 | // just in case
218 | _ = mime.AddExtensionType(".json", "application/json")
219 | _ = mime.AddExtensionType(".xml", "application/xml")
220 | }
221 |
--------------------------------------------------------------------------------
/context.go:
--------------------------------------------------------------------------------
1 | // Copyright 2014 Codehack http://codehack.com
2 | // Use of this source code is governed by a MIT-style
3 | // license that can be found in the LICENSE file.
4 |
5 | package relax
6 |
7 | import (
8 | "fmt"
9 | "io"
10 | "net/http"
11 | "net/url"
12 | "strconv"
13 | "strings"
14 | "sync"
15 | "time"
16 |
17 | "context"
18 | )
19 |
20 | // HandlerFunc is simply a version of http.HandlerFunc that uses Context.
21 | // All filters must return and accept this type.
22 | type HandlerFunc func(*Context)
23 |
24 | // Context has information about the request and filters. It implements
25 | // http.ResponseWriter.
26 | type Context struct {
27 | context.Context
28 |
29 | // ResponseWriter is the response object passed from ``net/http``.
30 | http.ResponseWriter
31 | wroteHeader bool
32 | status int
33 | bytes int
34 |
35 | // Request points to the http.Request information for this request.
36 | Request *http.Request
37 |
38 | // PathValues contains the values matched in PSEs by the router. It is a
39 | // name=values map (map[string][]string).
40 | // Examples:
41 | //
42 | // ctx.PathValues.Get("username") // returns the first value for "username"
43 | // ctx.PathValues.Get("_2") // values are also accessible by index
44 | // ctx.PathValues["colors"] // if more than one color value.
45 | //
46 | // See also: Router, url.Values
47 | PathValues url.Values
48 |
49 | // Encode is the media encoding function requested by the client.
50 | // To see the media type use:
51 | //
52 | // ctx.Get("content.encoding")
53 | //
54 | // See also: Encoder.Encode
55 | Encode func(io.Writer, interface{}) error
56 |
57 | // Decode is the decoding function when this request was made. It expects an
58 | // object that implements io.Reader, usually Request.Body. Then it will decode
59 | // the data and try to save it into a variable interface.
60 | // To see the media type use:
61 | //
62 | // ctx.Get("content.decoding")
63 | //
64 | // See also: Encoder.Decode
65 | Decode func(io.Reader, interface{}) error
66 | }
67 |
68 | // contextPool allows us to reuse some Context objects to conserve resources.
69 | var contextPool = sync.Pool{
70 | New: func() interface{} { return new(Context) },
71 | }
72 |
73 | // newContext returns a new Context object.
74 | // This function will alter Request.URL, adding scheme and host:port as provided by the client.
75 | func newContext(parent context.Context, w http.ResponseWriter, r *http.Request) *Context {
76 | ctx := contextPool.Get().(*Context)
77 | ctx.Context = parent
78 | ctx.ResponseWriter = w
79 | ctx.Request = r
80 | return ctx
81 | }
82 |
83 | // free frees a Context object back to the usage pool for later, to conserve
84 | // system resources.
85 | func (ctx *Context) free() {
86 | ctx.ResponseWriter = nil
87 | ctx.wroteHeader = false
88 | ctx.status = 0
89 | ctx.bytes = 0
90 | ctx.PathValues = nil
91 | ctx.Decode = nil
92 | ctx.Encode = nil
93 | contextPool.Put(ctx)
94 | }
95 |
96 | // Clone returns a shallow cloned context using 'w', an http.ResponseWriter object.
97 | // If 'w' is nil, the ResponseWriter value can be assigned after cloning.
98 | func (ctx *Context) Clone(w http.ResponseWriter) *Context {
99 | clone := contextPool.Get().(*Context)
100 | clone.Context = ctx.Context
101 | clone.ResponseWriter = w
102 | clone.Request = ctx.Request
103 | clone.PathValues = ctx.PathValues
104 | clone.bytes = ctx.bytes
105 | clone.Decode = ctx.Decode
106 | clone.Encode = ctx.Encode
107 | return clone
108 | }
109 |
110 | // Set stores the value of key in the Context k/v tree.
111 | func (ctx *Context) Set(key string, value interface{}) {
112 | ctx.Context = context.WithValue(ctx.Context, key, value)
113 | }
114 |
115 | // Get retrieves the value of key from Context storage. The value is returned
116 | // as an interface so it must be converted to an actual type. If the type implements
117 | // fmt.Stringer then it may be used by functions that expect a string.
118 | func (ctx *Context) Get(key string) interface{} {
119 | return ctx.Context.Value(key)
120 | }
121 |
122 | // Header implements ResponseWriter.Header
123 | func (ctx *Context) Header() http.Header {
124 | return ctx.ResponseWriter.Header()
125 | }
126 |
127 | // Write implements ResponseWriter.Write
128 | func (ctx *Context) Write(b []byte) (int, error) {
129 | n, err := ctx.ResponseWriter.Write(b)
130 | ctx.bytes += n
131 | return n, err
132 | }
133 |
134 | // WriteHeader will force a status code header, if one hasn't been set.
135 | // If no call to WriteHeader is done within this context, it defaults to
136 | // http.StatusOK (200), which is sent by net/http.
137 | func (ctx *Context) WriteHeader(code int) {
138 | if ctx.wroteHeader {
139 | return
140 | }
141 | ctx.wroteHeader = true
142 | ctx.status = code
143 | ctx.ResponseWriter.WriteHeader(code)
144 | }
145 |
146 | // Status returns the current known HTTP status code, or http.StatusOK if unknown.
147 | func (ctx *Context) Status() int {
148 | if !ctx.wroteHeader {
149 | return http.StatusOK
150 | }
151 | return ctx.status
152 | }
153 |
154 | // Bytes returns the number of bytes written in the response.
155 | func (ctx *Context) Bytes() int {
156 | return ctx.bytes
157 | }
158 |
159 | /*
160 | Respond writes a response back to the client. A complete RESTful response
161 | should be contained within a structure.
162 |
163 | 'v' is the object value to be encoded. 'code' is an optional HTTP status code.
164 |
165 | If at any point the response fails (due to encoding or system issues), an
166 | error is returned but not written back to the client.
167 |
168 | type Message struct {
169 | Status int `json:"status"`
170 | Text string `json:"text"`
171 | }
172 |
173 | ctx.Respond(&Message{Status: 201, Text: "Ticket created"}, http.StatusCreated)
174 |
175 | See also: Context.Encode, WriteHeader
176 | */
177 | func (ctx *Context) Respond(v interface{}, code ...int) error {
178 | if code != nil {
179 | ctx.WriteHeader(code[0])
180 | }
181 | err := ctx.Encode(ctx.ResponseWriter, v)
182 | if err != nil {
183 | // encoding failed, most likely we tried to encode something that hasn't
184 | // been made marshable yet.
185 | panic(err)
186 | }
187 | return err
188 | }
189 |
190 | /*
191 | Error sends an error response, with appropriate encoding. It basically calls
192 | Respond using a status code and wrapping the message in a StatusError object.
193 |
194 | 'code' is the HTTP status code of the error. 'message' is the actual error message
195 | or reason. 'details' are additional details about this error (optional).
196 |
197 | type RouteDetails struct {
198 | Method string `json:"method"`
199 | Path string `json:"path"`
200 | }
201 | ctx.Error(http.StatusNotImplemented, "That route is not implemented", &RouteDetails{"PATCH", "/v1/tickets/{id}"})
202 |
203 | See also: Respond, StatusError
204 | */
205 | func (ctx *Context) Error(code int, message string, details ...interface{}) {
206 | response := &StatusError{code, message, nil}
207 | if details != nil {
208 | response.Details = details[0]
209 | }
210 | ctx.Respond(response, code)
211 | }
212 |
213 | /*
214 | Format implements the fmt.Formatter interface, based on Apache HTTP's
215 | CustomLog directive. This allows a Context object to have Sprintf verbs for
216 | its values. See: https://httpd.apache.org/docs/2.4/mod/mod_log_config.html#formats
217 |
218 | Verb Description
219 | ---- ---------------------------------------------------
220 |
221 | %% Percent sign
222 | %a Client remote address
223 | %b Size of response in bytes, excluding headers. Or '-' if zero.
224 | %#a Proxy client address, or unknown.
225 | %h Remote hostname. Will perform lookup.
226 | %l Remote ident, will write '-' (only for Apache log support).
227 | %m Request method
228 | %q Request query string.
229 | %r Request line.
230 | %#r Request line without protocol.
231 | %s Response status code.
232 | %#s Response status code and text.
233 | %t Request time, as string.
234 | %u Remote user, if any.
235 | %v Request host name.
236 | %A User agent.
237 | %B Size of response in bytes, excluding headers.
238 | %D Time lapsed to serve request, in seconds.
239 | %H Request protocol.
240 | %I Bytes received.
241 | %L Request ID.
242 | %P Server port used.
243 | %R Referer.
244 | %U Request path.
245 |
246 | Example:
247 |
248 | // Print request line and remote address.
249 | // Index [1] needed to reuse ctx argument.
250 | fmt.Printf("\"%r\" %[1]a", ctx)
251 | // Output:
252 | // "GET /v1/" 192.168.1.10
253 |
254 | */
255 | func (ctx *Context) Format(f fmt.State, c rune) {
256 | var str string
257 |
258 | p, pok := f.Precision()
259 | if !pok {
260 | p = -1
261 | }
262 |
263 | switch c {
264 | case 'a':
265 | if f.Flag('#') {
266 | str = GetRealIP(ctx.Request)
267 | break
268 | }
269 | str = ctx.Request.RemoteAddr
270 | case 'b':
271 | if ctx.Bytes() == 0 {
272 | f.Write([]byte{45})
273 | return
274 | }
275 | fallthrough
276 | case 'B':
277 | str = strconv.Itoa(ctx.Bytes())
278 | case 'h':
279 | t := strings.Split(ctx.Request.RemoteAddr, ":")
280 | str = t[0]
281 | case 'l':
282 | f.Write([]byte{45})
283 | return
284 | case 'm':
285 | str = ctx.Request.Method
286 | case 'q':
287 | str = ctx.Request.URL.RawQuery
288 | case 'r':
289 | str = ctx.Request.Method + " " + ctx.Request.URL.RequestURI()
290 | if f.Flag('#') {
291 | break
292 | }
293 | str += " " + ctx.Request.Proto
294 | case 's':
295 | str = strconv.Itoa(ctx.Status())
296 | if f.Flag('#') {
297 | str += " " + http.StatusText(ctx.Status())
298 | }
299 | case 't':
300 | t := ctx.Get("request.start_time").(time.Time)
301 | str = t.Format("[02/Jan/2006:15:04:05 -0700]")
302 | case 'u':
303 | // XXX: i dont think net/http sets User
304 | if ctx.Request.URL.User == nil {
305 | f.Write([]byte{45})
306 | return
307 | }
308 | str = ctx.Request.URL.User.Username()
309 | case 'v':
310 | str = ctx.Request.Host
311 | case 'A':
312 | str = ctx.Request.UserAgent()
313 | case 'D':
314 | when := ctx.Get("request.start_time").(time.Time)
315 | if when.IsZero() {
316 | f.Write([]byte("%!(BADTIME)"))
317 | return
318 | }
319 | pok = false
320 | str = strconv.FormatFloat(time.Since(when).Seconds(), 'f', p, 32)
321 | case 'H':
322 | str = ctx.Request.Proto
323 | case 'I':
324 | str = fmt.Sprintf("%d", ctx.Request.ContentLength)
325 | case 'L':
326 | str = ctx.Get("request.id").(string)
327 | case 'P':
328 | s := strings.Split(ctx.Request.Host, ":")
329 | if len(s) > 1 {
330 | str = s[1]
331 | break
332 | }
333 | str = "80"
334 | case 'R':
335 | str = ctx.Request.Referer()
336 | case 'U':
337 | str = ctx.Request.URL.Path
338 | }
339 | if pok {
340 | str = str[:p]
341 | }
342 | f.Write([]byte(str))
343 | }
344 |
--------------------------------------------------------------------------------
/doc.go:
--------------------------------------------------------------------------------
1 | // Copyright 2014 Codehack http://codehack.com
2 | // Use of this source code is governed by a MIT-style
3 | // license that can be found in the LICENSE file.
4 |
5 | /*
6 | Package relax is a framework of pluggable components to build RESTful API's. It
7 | provides a thin layer over ``net/http`` to serve resources, without imposing a rigid
8 | structure. It is meant to be used along ``http.ServeMux``, but will work as a replacement
9 | as it implements ``http.Handler``.
10 |
11 | The framework is divided into components: Encoding, Filters, Routing, Hypermedia
12 | and, Resources. These are the parts of a complete REST Service. All the components
13 | are designed to be pluggable (replaced) through interfaces by external packages.
14 | Relax provides enough built-in functionality to assemble a complete REST API.
15 |
16 | The system is based on Resource Oriented Architecture (ROA), and had some inspiration
17 | from Heroku's REST API.
18 | */
19 | package relax
20 |
21 | // Version is the version of this package.
22 | const Version = "1.0.0"
23 |
--------------------------------------------------------------------------------
/encoder.go:
--------------------------------------------------------------------------------
1 | // Copyright 2014 Codehack http://codehack.com
2 | // Use of this source code is governed by a MIT-style
3 | // license that can be found in the LICENSE file.
4 |
5 | package relax
6 |
7 | import (
8 | "encoding/json"
9 | "errors"
10 | "io"
11 | )
12 |
13 | // ErrBodyTooLarge is returned by Encoder.Decode when the read length exceeds the
14 | // maximum size set for payload.
15 | var ErrBodyTooLarge = errors.New("encoder: Body too large")
16 |
17 | /*
18 | Encoder objects provide new data encoding formats.
19 |
20 | Once a request enters service context, all responses are encoded according to the
21 | assigned encoder. Relax includes support for JSON encoding. Other types of encoding
22 | can be added by implementing the Encoder interface.
23 | */
24 | type Encoder interface {
25 | // Accept returns the media type used in HTTP Accept header.
26 | Accept() string
27 |
28 | // ContentType returns the media type, and optionally character set,
29 | // for decoding used in Content-Type header.
30 | ContentType() string
31 |
32 | // Encode function encodes the value of an interface and writes it to an
33 | // io.Writer stream (usually an http.ResponseWriter object).
34 | Encode(io.Writer, interface{}) error
35 |
36 | // Decode function decodes input from an io.Reader (usually Request.Body) and
37 | // tries to save it to an interface variable.
38 | Decode(io.Reader, interface{}) error
39 | }
40 |
41 | // EncoderJSON implements the Encoder interface. It encode/decodes JSON data.
42 | type EncoderJSON struct {
43 | // MaxBodySize is the maximum size (in bytes) of JSON payload to read.
44 | // Defaults to 2097152 (2MB)
45 | MaxBodySize int64
46 |
47 | // Indented indicates whether or not to output indented JSON.
48 | // Note: indented JSON is slower to encode.
49 | // Defaults to false
50 | Indented bool
51 |
52 | // AcceptHeader is the media type used in Accept HTTP header.
53 | // Defaults to "application/json"
54 | AcceptHeader string
55 |
56 | // ContentTypeHeader is the media type used in Content-Type HTTP header
57 | // Defaults to "application/json;charset=utf-8"
58 | ContentTypeHeader string
59 | }
60 |
61 | // NewEncoder returns an EncoderJSON object. This function will initiallize
62 | // the object with sane defaults, for use with Service.encoders.
63 | // Returns the new EncoderJSON object.
64 | func NewEncoder() *EncoderJSON {
65 | return &EncoderJSON{
66 | MaxBodySize: 2097152, // 2MB
67 | Indented: false,
68 | AcceptHeader: "application/json",
69 | ContentTypeHeader: "application/json;charset=utf-8",
70 | }
71 | }
72 |
73 | // Accept returns the media type for JSON content, used in Accept header.
74 | func (e *EncoderJSON) Accept() string {
75 | return e.AcceptHeader
76 | }
77 |
78 | // ContentType returns the media type for JSON content, used in the
79 | // Content-Type header.
80 | func (e *EncoderJSON) ContentType() string {
81 | return e.ContentTypeHeader
82 | }
83 |
84 | // Encode will try to encode the value of v into JSON. If EncoderJSON.Indented
85 | // is true, then the JSON will be indented with tabs.
86 | // Returns nil on success, error on failure.
87 | func (e *EncoderJSON) Encode(writer io.Writer, v interface{}) error {
88 | if e.Indented {
89 | // indented is much slower...
90 | b, err := json.MarshalIndent(v, "", "\t")
91 | if err != nil {
92 | return err
93 | }
94 | _, err = writer.Write(b)
95 | return err
96 | }
97 | return json.NewEncoder(writer).Encode(v)
98 | }
99 |
100 | // Decode reads a JSON payload (usually from Request.Body) and tries to
101 | // save it to a variable v. If the payload is too large, with maximum
102 | // EncoderJSON.MaxBodySize, it will fail with error ErrBodyTooLarge
103 | // Returns nil on success and error on failure.
104 | func (e *EncoderJSON) Decode(reader io.Reader, v interface{}) error {
105 | r := &io.LimitedReader{R: reader, N: e.MaxBodySize}
106 | err := json.NewDecoder(r).Decode(v)
107 | if err != nil && r.N == 0 {
108 | return ErrBodyTooLarge
109 | }
110 | return err
111 | }
112 |
--------------------------------------------------------------------------------
/encoder/xml/README.md:
--------------------------------------------------------------------------------
1 | # xmlenc
2 |
3 | This package provides XML encoding for [Go-Relax](https://github.com/srfrog/go-relax).
4 |
5 | ## Installation
6 |
7 | Using "go get":
8 |
9 | go get "github.com/srfrog/go-relax/encoder/xml"
10 |
11 | Then import from source:
12 |
13 | import "github.com/srfrog/go-relax/encoder/xml"
14 |
15 | ## Usage
16 |
17 | To accept and respond with xml, you must add an object to the Service.Encoders map.
18 |
19 | ```go
20 | package main
21 |
22 | import (
23 | "github.com/srfrog/go-relax"
24 | "github.com/srfrog/go-relax/encoder/xml"
25 | "net/http"
26 | )
27 |
28 | func main() {
29 | mysrv := relax.NewService("/api")
30 |
31 | // create and configure new encoder object
32 | enc := xmlenc.NewEncoder()
33 | enc.Indented = true
34 |
35 | // assign it to service "mysrv".
36 | // this maps "application/xml" media queries to this encoder.
37 | mysrv.Use(enc)
38 |
39 | // done. now you can continue with your resource routes etc...
40 | mysrv.Run()
41 | }
42 | ```
43 |
44 | ### Options
45 |
46 | encoder := &xmlenc.Encoder{Indented: true, MaxBodySize: 10000, AcceptHeader: "text/xml"}
47 |
48 | ``Indented``: boolean; set to true to encode indented XML. Default is **false**.
49 |
50 | ``MaxBodySize``: int; the maximum size (in bytes) of XML content to be read. Default is **4194304** (4MB)
51 |
52 | ``AcceptHeader``: the MIME media type expected in Accept header. Default is "application/xml"
53 |
54 | ``ContentTypeHeader``: the MIME media type, and optionally character set, expected in Content-Type header. Default is "application/xml;charset=utf-8"
55 |
--------------------------------------------------------------------------------
/encoder/xml/xml.go:
--------------------------------------------------------------------------------
1 | // Copyright 2014 Codehack http://codehack.com
2 | // Use of this source code is governed by a MIT-style
3 | // license that can be found in the LICENSE file.
4 |
5 | package xmlenc
6 |
7 | import (
8 | "encoding/xml"
9 | "io"
10 |
11 | "github.com/srfrog/go-relax"
12 | )
13 |
14 | // EncoderXML implements the relax.Encoder interface. It encode/decodes XML.
15 | type EncoderXML struct {
16 | // MaxBodySize is the maximum size (in bytes) of XML content to be read (io.Reader)
17 | // Defaults to 4194304 (4MB)
18 | MaxBodySize int64
19 |
20 | // Indented indicates whether or not to output indented XML.
21 | // Defaults to false
22 | Indented bool
23 |
24 | // AcceptHeader is the media type used in Accept HTTP header.
25 | // Defaults to "application/xml"
26 | AcceptHeader string
27 |
28 | // ContentTypeHeader is the media type used in Content-Type HTTP header
29 | // Defaults to "application/xml;charset=utf-8"
30 | ContentTypeHeader string
31 | }
32 |
33 | // NewEncoder returns an EncoderXML object. This function will initiallize
34 | // the object with sane defaults, for use with Service.encoders.
35 | // Returns the new EncoderXML object.
36 | func NewEncoder() *EncoderXML {
37 | return &EncoderXML{
38 | MaxBodySize: 4194304, // 4MB
39 | Indented: false,
40 | AcceptHeader: "application/xml",
41 | ContentTypeHeader: "application/xml;charset=utf-8",
42 | }
43 | }
44 |
45 | // Accept returns the media type for XML content, used in Accept header.
46 | func (e *EncoderXML) Accept() string {
47 | return e.AcceptHeader
48 | }
49 |
50 | // ContentType returns the media type for XML content, used in the
51 | // Content-Type header.
52 | func (e *EncoderXML) ContentType() string {
53 | return e.ContentTypeHeader
54 | }
55 |
56 | // Encode will try to encode the value of v into XML. If EncoderJSON.Indented
57 | // is true, then the XML will be indented with tabs.
58 | // Returns the nil on success, and error on failure.
59 | func (e *EncoderXML) Encode(writer io.Writer, v interface{}) error {
60 | _, err := writer.Write([]byte(xml.Header))
61 | if err != nil {
62 | return err
63 | }
64 | enc := xml.NewEncoder(writer)
65 | if e.Indented {
66 | enc.Indent("", "\t")
67 | }
68 | return enc.Encode(v)
69 | }
70 |
71 | // Decode reads an XML payload (usually from Request.Body) and tries to
72 | // set it to a variable v. If the payload is too large, with maximum
73 | // EncoderXML.MaxBodySize, it will fail with error ErrBodyTooLarge
74 | // Returns nil on success and error on failure.
75 | func (e *EncoderXML) Decode(reader io.Reader, v interface{}) error {
76 | r := &io.LimitedReader{R: reader, N: e.MaxBodySize}
77 | err := xml.NewDecoder(r).Decode(v)
78 | if err != nil && r.N == 0 {
79 | return relax.ErrBodyTooLarge
80 | }
81 | return err
82 | }
83 |
--------------------------------------------------------------------------------
/encoder/xml/xml_test.go:
--------------------------------------------------------------------------------
1 | // Copyright 2014 Codehack http://codehack.com
2 | // Use of this source code is governed by a MIT-style
3 | // license that can be found in the LICENSE file.
4 |
5 | package xmlenc
6 |
7 | import (
8 | "bytes"
9 | "encoding/xml"
10 | "testing"
11 | )
12 |
13 | type Object struct {
14 | XMLName xml.Name `xml:"object"`
15 | Name string `xml:"name"`
16 | Number int `xml:"number,attr"`
17 | Strings []string `xml:"strings>value"`
18 | }
19 |
20 | func TestEncoder(t *testing.T) {
21 | var bb bytes.Buffer
22 |
23 | xmlstr := []byte(`
24 | `)
32 |
33 | reader := bytes.NewReader(xmlstr)
34 | object := &Object{}
35 |
36 | encoder := NewEncoder()
37 | encoder.Indented = true
38 |
39 | err := encoder.Decode(reader, object)
40 | if err != nil {
41 | t.Error(err.Error())
42 | }
43 |
44 | err = encoder.Encode(&bb, object)
45 | if err != nil {
46 | t.Error(err.Error())
47 | }
48 | if string(xmlstr) != bb.String() {
49 | t.Errorf("expected xmlstr but got something else.")
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/errors.go:
--------------------------------------------------------------------------------
1 | // Copyright 2014 Codehack http://codehack.com
2 | // Use of this source code is governed by a MIT-style
3 | // license that can be found in the LICENSE file.
4 |
5 | package relax
6 |
7 | // StatusError is an error with a HTTP Status code. It allows errors to be
8 | // complete and uniform.
9 | type StatusError struct {
10 | // Code is meant for a HTTP status code or any other numeric ID.
11 | Code int `json:"code"`
12 |
13 | // Message is the default error message used in logs.
14 | Message string `json:"message"`
15 |
16 | // Details can be any data structure that gives more information about the
17 | // error.
18 | Details interface{} `json:"details,omitempty"`
19 | }
20 |
21 | // StatusError implements the error interface.
22 | func (e *StatusError) Error() string { return e.Message }
23 |
24 | // BUG(TODO): StatusError is too shallow, need to implement better error system with locale support.
25 |
--------------------------------------------------------------------------------
/example/example_test.go:
--------------------------------------------------------------------------------
1 | // Copyright 2014 Codehack http://codehack.com
2 | // Use of this source code is governed by a MIT-style
3 | // license that can be found in the LICENSE file.
4 |
5 | package relax_test
6 |
7 | import (
8 | "log"
9 | "net/http"
10 | "strconv"
11 | "time"
12 |
13 | "github.com/srfrog/go-relax"
14 | "github.com/srfrog/go-relax/filter/authbasic"
15 | "github.com/srfrog/go-relax/filter/cors"
16 | "github.com/srfrog/go-relax/filter/etag"
17 | "github.com/srfrog/go-relax/filter/gzip"
18 | "github.com/srfrog/go-relax/filter/logs"
19 | "github.com/srfrog/go-relax/filter/override"
20 | "github.com/srfrog/go-relax/filter/security"
21 | )
22 |
23 | // User could be a struct mapping a DB table.
24 | type User struct {
25 | ID int `json:"id"`
26 | Name string `json:"name"`
27 | DOB time.Time `json:"dob"`
28 | }
29 |
30 | // Users will be our resource object.
31 | type Users struct {
32 | Group string `json:"group"`
33 | People []*User `json:"people"`
34 | }
35 |
36 | // FindByID searches users.People for a user matching ID and returns it;
37 | // or StatusError if not found. This could do a search in our DB and
38 | // handle the error logic.
39 | func (u *Users) FindByID(idstr string) (*User, error) {
40 | id, err := strconv.Atoi(idstr)
41 | if err != nil {
42 | return nil, &relax.StatusError{Code: http.StatusInternalServerError, Message: err.Error()}
43 | }
44 | for _, user := range u.People {
45 | if id == user.ID {
46 | // user found, return it.
47 | return user, nil
48 | }
49 | }
50 | // user not found.
51 | return nil, &relax.StatusError{Code: http.StatusNotFound, Message: "That user was not found"}
52 | }
53 |
54 | // Index handles "GET /v1/users"
55 | func (u *Users) Index(ctx *relax.Context) {
56 | ctx.Header().Set("X-Custom-Header", "important header info from my framework")
57 | // list all users in the resource.
58 | ctx.Respond(u)
59 | }
60 |
61 | // Create handles "POST /v1/users"
62 | func (u *Users) Create(ctx *relax.Context) {
63 | user := &User{}
64 | // decode json payload from client
65 | if err := ctx.Decode(ctx.Request.Body, &user); err != nil {
66 | ctx.Error(http.StatusBadRequest, err.Error())
67 | return
68 | }
69 | // some validation
70 | if user.Name == "" {
71 | ctx.Error(http.StatusBadRequest, "must supply a name")
72 | return
73 | }
74 | if user.DOB.IsZero() {
75 | user.DOB = time.Now() // lies!
76 | }
77 | // create new user
78 | user.ID = len(u.People) + 1
79 | u.People = append(u.People, user)
80 | // send restful response
81 | ctx.Respond(user, http.StatusCreated)
82 | }
83 |
84 | // Read handles "GET /v1/users/ID"
85 | func (u *Users) Read(ctx *relax.Context) {
86 | user, err := u.FindByID(ctx.PathValues.Get("id"))
87 | if err != nil {
88 | ctx.Error(err.(*relax.StatusError).Code, err.Error(), "more details here")
89 | return
90 | }
91 | ctx.Respond(user)
92 | }
93 |
94 | // Update handles "PUT /v1/users/ID" for changes to items.
95 | func (u *Users) Update(ctx *relax.Context) {
96 | user, err := u.FindByID(ctx.PathValues.Get("id"))
97 | if err != nil {
98 | ctx.Error(err.(*relax.StatusError).Code, err.Error(), "more details here")
99 | return
100 | }
101 | // maybe some validation should go here...
102 |
103 | // decode json payload from client
104 | if err := ctx.Decode(ctx.Request.Body, &user); err != nil {
105 | ctx.Error(http.StatusBadRequest, err.Error())
106 | return
107 | }
108 | ctx.Respond(user)
109 | }
110 |
111 | // Delete handles "DELETE /v1/users/ID" to remove items.
112 | // Note: this function wont be used because we override the route below.
113 | func (u *Users) Delete(ctx *relax.Context) {
114 | ctx.Error(http.StatusInternalServerError, "not reached!")
115 | }
116 |
117 | // SampleHandler prints out all filter info, and responds with all path values.
118 | func SampleHandler(ctx *relax.Context) {
119 | ctx.Respond(ctx.PathValues)
120 | }
121 |
122 | // Example_basic creates a new service under path "/v1" and serves requests
123 | // for the users resource.
124 | func Example_basic() {
125 | // Create our resource object.
126 | users := &Users{Group: "Influential Scientists"}
127 |
128 | // Fill-in the users.People list with some scientists (this could be from DB table).
129 | users.People = []*User{
130 | &User{1, "Issac Newton", time.Date(1643, 1, 4, 0, 0, 0, 0, time.UTC)},
131 | &User{2, "Albert Einstein", time.Date(1879, 3, 14, 0, 0, 0, 0, time.UTC)},
132 | &User{3, "Nikola Tesla", time.Date(1856, 7, 10, 0, 0, 0, 0, time.UTC)},
133 | &User{4, "Charles Darwin", time.Date(1809, 2, 12, 0, 0, 0, 0, time.UTC)},
134 | &User{5, "Neils Bohr", time.Date(1885, 10, 7, 0, 0, 0, 0, time.UTC)},
135 | }
136 |
137 | // Create a service under "/v1". If using absolute URI, it will limit requests
138 | // to a specific host. This service has FilterLog as service-level filter.
139 | svc := relax.NewService("/v1", &logs.Filter{})
140 |
141 | // More service-level filters (these could go inside NewService()).
142 | svc.Use(&etag.Filter{}) // ETag with cache conditionals
143 | svc.Use(&cors.Filter{
144 | AllowAnyOrigin: true,
145 | AllowCredentials: true,
146 | })
147 | svc.Use(&gzip.Filter{}) // on-the-fly gzip encoding
148 | svc.Use(&override.Filter{}) // method override support
149 |
150 | // I prefer pretty indentation.
151 | json := relax.NewEncoder()
152 | json.Indented = true
153 | svc.Use(json)
154 |
155 | // Basic authentication, used as needed.
156 | needsAuth := &authbasic.Filter{
157 | Realm: "Masters of Science",
158 | Authenticate: func(user, pass string) bool {
159 | if user == "Pi" && pass == "3.14159" {
160 | return true
161 | }
162 | return false
163 | },
164 | }
165 |
166 | // Serve our resource with CRUD routes, using unsigned ints as ID's.
167 | // This resource has Security filter at resource-level.
168 | res := svc.Resource(users, &security.Filter{CacheDisable: true}).CRUD("{uint:id}")
169 | {
170 | // Although CRUD added a route for "DELETE /v1/users/{uint:id}",
171 | // we can change it here and respond with status 418.
172 | teapotted := func(ctx *relax.Context) {
173 | ctx.Error(418, "YOU are the teapot!", []string{"more details here...", "use your own struct"})
174 | }
175 | res.DELETE("{uint:id}", teapotted)
176 |
177 | // Some other misc. routes to test route expressions.
178 | // Shese routes will be added under "/v1/users/".
179 | res.GET("dob/{date:date}", SampleHandler) // Get by ISO 8601 datetime string
180 | res.PUT("issues/{int:int}", SampleHandler) // PUT by signed int
181 | res.GET("apikey/{hex:hex}", res.NotImplemented) // Get by APIKey (hex value) - 501-"Not Implemented"
182 | res.GET("@{word:word}", SampleHandler) // Get by username (twitterish)
183 | res.GET("stuff/{whatever}/*", teapotted) // sure, stuff whatever...
184 | res.POST("{uint:id}/checkin", SampleHandler, needsAuth) // POST with route-level filter
185 | res.GET("born/{date:d1}/to/{date:d2}", SampleHandler) // Get by DOB in date range
186 | res.PATCH("", res.MethodNotAllowed) // PATCH method is not allowed for this resource.
187 |
188 | // Custom regexp PSE matching.
189 | // Example matching US phone numbers. Any of these values are ok:
190 | // +1-999-999-1234, +1 999-999-1234, +1 (999) 999-1234, 1-999-999-1234
191 | // 1 (999) 999-1234, 999-999-1234, (999) 999-1234
192 | res.GET(`phone/{re:(?:\+?(1)[\- ])?(\([0-9]{3}\)|[0-9]{3})[\- ]([0-9]{3})\-([0-9]{4})}`, SampleHandler)
193 | // Example matching month digits 01-12
194 | res.GET(`todos/month/{re:([0][1-9]|[1][0-2])}`, SampleHandler)
195 |
196 | // New internal method extension (notice the X).
197 | res.Route("XMODIFY", "properties", SampleHandler)
198 | }
199 |
200 | // Let http.ServeMux handle basic routing.
201 | http.Handle(svc.Handler())
202 |
203 | log.Fatal(http.ListenAndServe(":8000", nil))
204 | // Output:
205 | }
206 |
--------------------------------------------------------------------------------
/example/logrus/README.md:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/srfrog/go-relax/f9127c917e1916c9ae23105b8cb84f7d3570fa40/example/logrus/README.md
--------------------------------------------------------------------------------
/example/logrus/logrus.go:
--------------------------------------------------------------------------------
1 | // Copyright 2014 Codehack http://codehack.com
2 | // Use of this source code is governed by a MIT-style
3 | // license that can be found in the LICENSE file.
4 |
5 | // This is an example showing how to integrate logrus package with Relax.
6 |
7 | package main
8 |
9 | import (
10 | "github.com/sirupsen/logrus"
11 | "github.com/srfrog/go-relax"
12 | "github.com/srfrog/go-relax/filter/logs"
13 | )
14 |
15 | // HelloIndex just says Hello
16 | func HelloIndex(ctx *relax.Context) {
17 | ctx.Respond("Hello, " + ctx.PathValues.Get("_1"))
18 | }
19 |
20 | func main() {
21 | // log all service requests with standard log
22 | log1 := logrus.StandardLogger()
23 | svc := relax.NewService("/hello", &logs.Filter{Logger: log1})
24 |
25 | // the hello index also gets a json log
26 | log2 := logrus.New()
27 | log2.Formatter = new(logrus.JSONFormatter)
28 | svc.Root().GET("*", HelloIndex, &logs.Filter{Logger: log2})
29 | svc.Run()
30 | }
31 |
--------------------------------------------------------------------------------
/filter.go:
--------------------------------------------------------------------------------
1 | // Copyright 2014 Codehack http://codehack.com
2 | // Use of this source code is governed by a MIT-style
3 | // license that can be found in the LICENSE file.
4 |
5 | package relax
6 |
7 | /*
8 | Filter is a function closure that is chained in FILO (First-In Last-Out) order.
9 | Filters pre and post process all requests. At any time, a filter can stop a request by
10 | returning before the next chained filter is called. The final link points to the
11 | resource handler.
12 |
13 | Filters are run at different times during a request, and in order: Service, Resource and, Route.
14 | Service filters are run before resource filters, and resource filters before route filters.
15 | This allows some granularity to filters.
16 |
17 | Relax comes with filters that provide basic functionality needed by most REST API's.
18 | Some included filters: CORS, method override, security, basic auth and content negotiation.
19 | Adding filters is a matter of creating new objects that implement the Filter interface.
20 | The position of the ``next()`` handler function is important to the effect of the particular
21 | filter execution.
22 | */
23 | type Filter interface {
24 | // Run executes the current filter in a chain.
25 | // It takes a HandlerFunc function argument, which is executed within the
26 | // closure returned.
27 | Run(HandlerFunc) HandlerFunc
28 | }
29 |
30 | /*
31 | LimitedFilter are filters that only can be used with a set of resources.
32 | Where resource is one of: ``Router`` (interface), ``*Resource`` and ``*Service``
33 | The ``RunIn()`` func should return true for the type(s) allowed, false otherwise.
34 |
35 | func (f *MyFilter) RunIn(r interface{}) bool {
36 | switch r.(type) {
37 | case relax.Router:
38 | return true
39 | case *relax.Resource:
40 | return true
41 | case *relax.Service:
42 | return false
43 | }
44 | return false
45 | }
46 |
47 | */
48 | type LimitedFilter interface {
49 | RunIn(interface{}) bool
50 | }
51 |
--------------------------------------------------------------------------------
/filter/authbasic/authbasic.go:
--------------------------------------------------------------------------------
1 | // Copyright 2014 Codehack http://codehack.com
2 | // Use of this source code is governed by a MIT-style
3 | // license that can be found in the LICENSE file.
4 |
5 | package authbasic
6 |
7 | import (
8 | "encoding/base64"
9 | "errors"
10 | "net/http"
11 | "strings"
12 |
13 | "github.com/srfrog/go-relax"
14 | )
15 |
16 | // Filter AuthBasic is a Filter that implements HTTP Basic Authentication as
17 | // described in http://www.ietf.org/rfc/rfc2617.txt
18 | type Filter struct {
19 | // Realm is the authentication realm.
20 | // This defaults to "Authorization Required"
21 | Realm string
22 |
23 | // Authenticate is a function that will perform the actual authentication
24 | // check.
25 | // It should expect a username and password, then return true if those
26 | // credentials are accepted; false otherwise.
27 | // If no function is assigned, it defaults to a function that denies all
28 | // (false).
29 | Authenticate func(string, string) bool
30 | }
31 |
32 | // Errors returned by Filter AuthBasic that are general and could be reused.
33 | var (
34 | // ErrAuthInvalidRequest is returned when the auth request don't match the expected
35 | // challenge.
36 | ErrAuthInvalidRequest = errors.New("auth: Invalid authorization request")
37 |
38 | // ErrAuthInvalidSyntax is returned when the syntax of the credentials is not what is
39 | // expected.
40 | ErrAuthInvalidSyntax = errors.New("auth: Invalid credentials syntax")
41 | )
42 |
43 | // denyAllAccess is the default Authenticate function, and as the name
44 | // implies, will deny all access by returning false.
45 | func denyAllAccess(username, password string) bool {
46 | return false
47 | }
48 |
49 | func getUserPass(header string) ([]string, error) {
50 | credentials := strings.Split(header, " ")
51 | if len(credentials) != 2 || credentials[0] != "Basic" {
52 | return nil, ErrAuthInvalidRequest
53 | }
54 |
55 | authstr, err := base64.StdEncoding.DecodeString(credentials[1])
56 | if err != nil {
57 | return nil, err
58 | }
59 |
60 | userpass := strings.Split(string(authstr), ":")
61 | if len(userpass) != 2 {
62 | return nil, ErrAuthInvalidSyntax
63 | }
64 |
65 | return userpass, nil
66 | }
67 |
68 | // Run runs the filter and passes down the following Info:
69 | //
70 | // ctx.Get("auth.user") // auth user
71 | // ctx.Get("auth.type") // auth scheme type. e.g., "basic"
72 | //
73 | func (f *Filter) Run(next relax.HandlerFunc) relax.HandlerFunc {
74 | if f.Realm == "" {
75 | f.Realm = "Authorization Required"
76 | }
77 | f.Realm = strings.Replace(f.Realm, `"'`, "", -1)
78 |
79 | if f.Authenticate == nil {
80 | f.Authenticate = denyAllAccess
81 | }
82 |
83 | return func(ctx *relax.Context) {
84 | header := ctx.Request.Header.Get("Authorization")
85 | if header == "" {
86 | MustAuthenticate(ctx, "Basic realm=\""+f.Realm+"\"")
87 | return
88 | }
89 |
90 | userpass, err := getUserPass(header)
91 | if err != nil {
92 | http.Error(ctx, err.Error(), http.StatusBadRequest)
93 | return
94 | }
95 |
96 | if !f.Authenticate(userpass[0], userpass[1]) {
97 | MustAuthenticate(ctx, "Basic realm=\""+f.Realm+"\"")
98 | return
99 | }
100 |
101 | ctx.Set("auth.user", userpass[0])
102 | ctx.Set("auth.type", "basic")
103 |
104 | next(ctx)
105 | }
106 | }
107 |
108 | // MustAuthenticate is a helper function used to send the WWW-Authenticate
109 | // HTTP header.
110 | // challenge is the auth scheme and the realm, as specified in section 2 of
111 | // RFC 2617.
112 | func MustAuthenticate(w http.ResponseWriter, challenge string) {
113 | w.Header().Set("WWW-Authenticate", challenge)
114 | http.Error(w, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)
115 | }
116 |
--------------------------------------------------------------------------------
/filter/authbasic/doc.go:
--------------------------------------------------------------------------------
1 | // Copyright 2014 Codehack http://codehack.com
2 | // Use of this source code is governed by a MIT-style
3 | // license that can be found in the LICENSE file.
4 |
5 | package authbasic
6 |
7 | // Version is the semantic version of this package
8 | // More info: https://semver.org
9 | const Version = "1.0.0"
10 |
--------------------------------------------------------------------------------
/filter/cors/cors.go:
--------------------------------------------------------------------------------
1 | // Copyright 2014 Codehack http://codehack.com
2 | // Use of this source code is governed by a MIT-style
3 | // license that can be found in the LICENSE file.
4 |
5 | package cors
6 |
7 | import (
8 | "net/http"
9 | "net/url"
10 | "regexp"
11 | "strconv"
12 | "strings"
13 |
14 | "github.com/srfrog/go-relax"
15 | "github.com/srfrog/go-strarr"
16 | )
17 |
18 | const defaultCORSMaxAge = 86400 // 24 hours
19 |
20 | var (
21 | // simpleMethods and simpleHeaders per the CORS recommendation - http://www.w3.org/TR/cors/#terminology
22 | simpleMethods = []string{"GET", "HEAD", "POST"}
23 | simpleHeaders = []string{"Cache-Control", "Content-Language", "Content-Type", "Expires", "Last-Modified", "Pragma"}
24 |
25 | // allowMethodsDefault are methods generally used in REST, leaving simple methods to be complete.
26 | allowMethodsDefault = []string{"GET", "POST", "PATCH", "PUT", "DELETE"}
27 |
28 | // allowHeadersDefault are reasonably useful headers in REST.
29 | allowHeadersDefault = []string{"Authorization", "Content-Type", "If-Match", "If-Modified-Since", "If-None-Match", "If-Unmodified-Since", "X-Requested-With"}
30 |
31 | // exposeHeadersDefault are headers used regularly by both client/server
32 | exposeHeadersDefault = []string{"Etag", "Link", "RateLimit-Limit", "RateLimit-Remaining", "RateLimit-Reset", "X-Poll-Interval"}
33 |
34 | // allowOriginRegexp holds our pre-compiled origin regex patterns.
35 | allowOriginRegexp = []*regexp.Regexp{}
36 | )
37 |
38 | // Filter CORS implements the Cross-Origin Resource Sharing (CORS) recommendation, as
39 | // described in http://www.w3.org/TR/cors/ (W3C).
40 | type Filter struct {
41 | // AllowOrigin is the list of URI patterns that are allowed to use the resource.
42 | // The patterns consist of text with zero or more wildcards '*' '?' '+'.
43 | //
44 | // '*' matches zero or more characters.
45 | // '?' matches exactly one character.
46 | // '_' matches zero or one character.
47 | // '+' matches at least one character.
48 | //
49 | // Note that a single pattern of '*' will match all origins, if that's what you need
50 | // then use AllowAnyOrigin=true instead. If AllowOrigin is empty and AllowAnyOrigin=false,
51 | // then all CORS requests (simple and preflight) will fail with an HTTP error response.
52 | //
53 | // Examples:
54 | // http://*example.com - matches example.com and all its subdomains.
55 | // http_://+.example.com - matches SSL and non-SSL, and subdomains of example.com, but not example.com
56 | // http://foo??.example.com - matches subdomains fooXX.example.com where X can be any character.
57 | // chrome-extension://* - good for testing from Chrome.
58 | //
59 | // Default: empty
60 | AllowOrigin []string
61 |
62 | // AllowAnyOrigin if set to true, it will allow all origin requests.
63 | // This is effectively "Access-Control-Allow-Origin: *" as in the CORS specification.
64 | //
65 | // Default: false
66 | AllowAnyOrigin bool
67 |
68 | // AllowMethods is the list of HTTP methods that can be used in a request. If AllowMethods
69 | // is empty, all permission requests (preflight) will fail with an HTTP error response.
70 | //
71 | // Default: "GET", "POST", "PATCH", "PUT", "DELETE"
72 | AllowMethods []string
73 |
74 | // AllowHeaders is the list of HTTP headers that can be used in a request. If AllowHeaders
75 | // is empty, then only simple common HTTP headers are allowed.
76 | //
77 | // Default: "Authorization", "Content-Type", "If-Match", "If-Modified-Since", "If-None-Match", "If-Unmodified-Since", "X-Requested-With"
78 | AllowHeaders []string
79 |
80 | // AllowCredentials whether or not to allow user credendials to propagate through a request.
81 | // If AllowCredentials is false, then all authentication and cookies are disabled.
82 | //
83 | // Default: false
84 | AllowCredentials bool
85 |
86 | // ExposeHeaders is a list of HTTP headers that can be exposed to the API. This list should
87 | // include any custom headers that are needed to complete the response.
88 | //
89 | // Default: "Etag", "Link", "RateLimit-Limit", "RateLimit-Remaining", "RateLimit-Reset", "X-Poll-Interval"
90 | ExposeHeaders []string
91 |
92 | // MaxAge is a number of seconds the permission request (preflight) results should be cached.
93 | // This number should be large enough to complete all request from a client, but short enough to
94 | // keep the API secure. Set to -1 to disable caching.
95 | //
96 | // Default: 86400
97 | MaxAge int
98 |
99 | // Strict specifies whether or not to adhere strictly to the W3C CORS recommendation. If
100 | // Strict=false then the focus is performance instead of correctness. Also, Strict=true
101 | // will add more security checks to permission requests (preflight) and other security decisions.
102 | //
103 | // Default: false
104 | Strict bool
105 | }
106 |
107 | func (f *Filter) corsHeaders(origin string) http.Header {
108 | headers := make(http.Header)
109 | switch {
110 | case f.AllowCredentials:
111 | headers.Set("Access-Control-Allow-Origin", origin)
112 | headers.Set("Access-Control-Allow-Credentials", "true")
113 | headers.Add("Vary", "Origin")
114 | case f.Strict:
115 | if f.AllowOrigin == nil {
116 | headers.Set("Access-Control-Allow-Origin", "null")
117 | return headers
118 | }
119 | headers.Set("Access-Control-Allow-Origin", origin)
120 | headers.Add("Vary", "Origin")
121 | default:
122 | headers.Set("Access-Control-Allow-Origin", "*")
123 | }
124 | return headers
125 | }
126 |
127 | // XXX: handlePreflightRequest does not do preflight steps 9 & 10 checks because they are too strict.
128 | // XXX: It will skip steps 9 & 10, as per the recommendation.
129 | func (f *Filter) handlePreflightRequest(origin, rmethod, rheaders string) (http.Header, error) {
130 | if !strarr.Contains(simpleMethods, rmethod) && !strarr.Contains(f.AllowMethods, rmethod) {
131 | return nil, &relax.StatusError{Code: http.StatusMethodNotAllowed, Message: "Invalid method in preflight"}
132 | }
133 | if rheaders != "" {
134 | arr := strarr.Map(strings.TrimSpace, strings.Split(rheaders, ","))
135 | if len(strarr.Diff(arr, f.AllowHeaders)) == 0 {
136 | return nil, &relax.StatusError{Code: http.StatusForbidden, Message: "Invalid header in preflight"}
137 | }
138 | }
139 |
140 | headers := f.corsHeaders(origin)
141 | if f.MaxAge > 0 {
142 | headers.Set("Access-Control-Max-Age", strconv.Itoa(f.MaxAge))
143 | }
144 | if f.AllowMethods != nil {
145 | headers.Set("Access-Control-Allow-Methods", strings.Join(f.AllowMethods, ", "))
146 | }
147 | if f.AllowHeaders != nil {
148 | headers.Set("Access-Control-Allow-Headers", strings.Join(f.AllowHeaders, ", "))
149 | }
150 | headers.Set("Content-Length", "0")
151 |
152 | return headers, nil
153 | }
154 |
155 | func (f *Filter) handleSimpleRequest(origin string) http.Header {
156 | headers := f.corsHeaders(origin)
157 | if len(f.ExposeHeaders) > 0 {
158 | headers.Set("Access-Control-Expose-Headers", strings.Join(f.ExposeHeaders, ", "))
159 | }
160 | return headers
161 | }
162 |
163 | func (f *Filter) isOriginAllowed(origin string) bool {
164 | for _, re := range allowOriginRegexp {
165 | if re.MatchString(origin) {
166 | return true
167 | }
168 | }
169 | return false
170 | }
171 |
172 | // Run runs the filter and passes down the following Info:
173 | //
174 | // ctx.Get("cors.request") // boolean, whether or not this was a CORS request.
175 | // ctx.Get("cors.origin") // Origin of the request, if it's a CORS request.
176 | //
177 | func (f *Filter) Run(next relax.HandlerFunc) relax.HandlerFunc {
178 | if f.AllowMethods == nil {
179 | f.AllowMethods = allowMethodsDefault
180 | }
181 | if f.AllowHeaders == nil {
182 | f.AllowHeaders = allowHeadersDefault
183 | }
184 | if f.ExposeHeaders == nil {
185 | f.ExposeHeaders = exposeHeadersDefault
186 | }
187 | if f.MaxAge == 0 {
188 | f.MaxAge = defaultCORSMaxAge
189 | }
190 | f.AllowMethods = strarr.Map(strings.ToUpper, f.AllowMethods)
191 | f.AllowHeaders = strarr.Map(http.CanonicalHeaderKey, f.AllowHeaders)
192 | f.ExposeHeaders = strarr.Map(http.CanonicalHeaderKey,
193 | strarr.Diff(f.ExposeHeaders, simpleHeaders))
194 |
195 | for _, v := range f.AllowOrigin {
196 | str := regexp.QuoteMeta(strings.ToLower(v))
197 | str = strings.Replace(str, `\+`, `.+`, -1)
198 | str = strings.Replace(str, `\*`, `.*`, -1)
199 | str = strings.Replace(str, `\?`, `.`, -1)
200 | str = strings.Replace(str, `_`, `.?`, -1)
201 | allowOriginRegexp = append(allowOriginRegexp, regexp.MustCompile(str))
202 | }
203 |
204 | return func(ctx *relax.Context) {
205 | origin := ctx.Request.Header.Get("Origin")
206 |
207 | // ctx.Set("cors.request", false)
208 |
209 | // This is not a CORS request, carry on.
210 | if origin == "" {
211 | next(ctx)
212 | return
213 | }
214 |
215 | if !f.AllowAnyOrigin && !f.isOriginAllowed(origin) {
216 | if f.Strict {
217 | ctx.Error(http.StatusForbidden, "Invalid CORS origin")
218 | return
219 | }
220 | next(ctx)
221 | return
222 | }
223 |
224 | // Check that Origin: is sane and does not match Host:
225 | // http://www.w3.org/TR/cors/#resource-security
226 | if f.Strict {
227 | u, err := url.ParseRequestURI(origin)
228 | if err != nil {
229 | ctx.Error(http.StatusBadRequest, err.Error())
230 | return
231 | }
232 | if ctx.Request.Host == u.Host || u.Path != "" || !strings.HasPrefix(u.Scheme, "http") {
233 | ctx.Error(http.StatusBadRequest, "Invalid CORS origin syntax")
234 | return
235 | }
236 | }
237 |
238 | // Method requested
239 | method := ctx.Request.Header.Get("Access-Control-Request-Method")
240 |
241 | // Preflight request
242 | if ctx.Request.Method == "OPTIONS" && method != "" {
243 | headers, err := f.handlePreflightRequest(origin, method, ctx.Request.Header.Get("Access-Control-Request-Headers"))
244 | if err != nil {
245 | if (err.(*relax.StatusError)).Code == http.StatusMethodNotAllowed {
246 | ctx.Header().Set("Allow", strings.Join(f.AllowMethods, ", "))
247 | }
248 | ctx.Error(err.(*relax.StatusError).Code, err.Error())
249 | return
250 | }
251 | for k, v := range headers {
252 | ctx.Header()[k] = v
253 | }
254 | ctx.WriteHeader(http.StatusNoContent)
255 | return
256 | }
257 |
258 | // Simple request
259 | headers := f.handleSimpleRequest(origin)
260 | for k, v := range headers {
261 | ctx.Header()[k] = v
262 | }
263 |
264 | // let other downstream filters know that this is a CORS request
265 | ctx.Set("cors.request", true)
266 | ctx.Set("cors.origin", origin)
267 |
268 | next(ctx)
269 | }
270 | }
271 |
--------------------------------------------------------------------------------
/filter/cors/doc.go:
--------------------------------------------------------------------------------
1 | // Copyright 2014 Codehack http://codehack.com
2 | // Use of this source code is governed by a MIT-style
3 | // license that can be found in the LICENSE file.
4 |
5 | package cors
6 |
7 | // Version is the semantic version of this package
8 | // More info: https://semver.org
9 | const Version = "1.0.0"
10 |
--------------------------------------------------------------------------------
/filter/etag/doc.go:
--------------------------------------------------------------------------------
1 | // Copyright 2014 Codehack http://codehack.com
2 | // Use of this source code is governed by a MIT-style
3 | // license that can be found in the LICENSE file.
4 |
5 | package etag
6 |
7 | // Version is the semantic version of this package
8 | // More info: https://semver.org
9 | const Version = "1.0.0"
10 |
--------------------------------------------------------------------------------
/filter/etag/etag.go:
--------------------------------------------------------------------------------
1 | // Copyright 2014 Codehack http://codehack.com
2 | // Use of this source code is governed by a MIT-style
3 | // license that can be found in the LICENSE file.
4 |
5 | package etag
6 |
7 | import (
8 | "crypto/sha1"
9 | "encoding/hex"
10 | "net/http"
11 | "strings"
12 | "time"
13 |
14 | "github.com/srfrog/go-relax"
15 | )
16 |
17 | // Filter ETag generates an entity-tag header "ETag" for body content of a response.
18 | // It will use pre-generated etags from the underlying filters or handlers, if available.
19 | // Optionally, it will also handle the conditional response based on If-Match
20 | // and If-None-Match checks on specific entity-tag values.
21 | // This implementation follows the recommendation in http://tools.ietf.org/html/rfc7232
22 | type Filter struct {
23 | // DisableConditionals will make this filter ignore the values from the headers
24 | // If-None-Match and If-Match and not do conditional entity tests. An ETag will
25 | // still be generated, if possible.
26 | // Defaults to false
27 | DisableConditionals bool
28 | }
29 |
30 | // etagStrongCmp does strong comparison of If-Match entity values.
31 | func etagStrongCmp(etags, etag string) bool {
32 | if etag == "" || strings.HasPrefix(etag, "W/") {
33 | return false
34 | }
35 | for _, v := range strings.Split(etags, ",") {
36 | if strings.TrimSpace(v) == etag {
37 | return true
38 | }
39 | }
40 | return false
41 | }
42 |
43 | // etagWeakCmp does weak comparison of If-None-Match entity values.
44 | func etagWeakCmp(etags, etag string) bool {
45 | if etag == "" {
46 | return false
47 | }
48 | return strings.Contains(etags, strings.Trim(etag, `"`))
49 | }
50 |
51 | // Run runs the filter and passes down the following Info:
52 | func (f *Filter) Run(next relax.HandlerFunc) relax.HandlerFunc {
53 | return func(ctx *relax.Context) {
54 | var etag string
55 |
56 | // Start a buffered context. All writes are diverted to a ResponseBuffer.
57 | rb := relax.NewResponseBuffer(ctx)
58 | next(ctx.Clone(rb))
59 | defer rb.Flush(ctx)
60 |
61 | // Do not pass GO. Do not collect $200
62 | if rb.Status() < 200 || rb.Status() == http.StatusNoContent ||
63 | (rb.Status() > 299 && rb.Status() != http.StatusPreconditionFailed) ||
64 | !strings.Contains("DELETE GET HEAD PATCH POST PUT", ctx.Request.Method) {
65 | goto Finish
66 | }
67 |
68 | etag = rb.Header().Get("ETag")
69 |
70 | if isEtagMethod(ctx.Request.Method) && rb.Status() == http.StatusOK {
71 | if etag == "" {
72 | alter := ""
73 | // Change etag when using content encoding.
74 | if ce := rb.Header().Get("Content-Encoding"); ce != "" {
75 | alter = "-" + ce
76 | }
77 | h := sha1.New()
78 | h.Write(rb.Bytes())
79 | etag = `"` + hex.EncodeToString(h.Sum(nil)) + alter + `"`
80 | }
81 | }
82 |
83 | if !f.DisableConditionals {
84 | // If-Match
85 | ifmatch := ctx.Request.Header.Get("If-Match")
86 | if ifmatch != "" && ((ifmatch == "*" && etag == "") || !etagStrongCmp(ifmatch, etag)) {
87 | /*
88 | // FIXME: need to verify Status per request.
89 | if strings.Contains("DELETE PATCH POST PUT", ctx.Request.Method) && rb.Status() != http.StatusPreconditionFailed {
90 | // XXX: we cant confirm it's the same resource item without re-GET'ing it.
91 | // XXX: maybe etag should be changed from strong to weak.
92 | etag = ""
93 | goto Finish
94 | }
95 | */
96 | ctx.WriteHeader(http.StatusPreconditionFailed)
97 | rb.Free()
98 | return
99 | }
100 |
101 | // If-Unmodified-Since
102 | ifunmod := ctx.Request.Header.Get("If-Unmodified-Since")
103 | if ifmatch == "" && ifunmod != "" {
104 | modtime, _ := time.Parse(http.TimeFormat, ifunmod)
105 | lastmod, _ := time.Parse(http.TimeFormat, rb.Header().Get("Last-Modified"))
106 | if !modtime.IsZero() && !lastmod.IsZero() && lastmod.After(modtime) {
107 | ctx.WriteHeader(http.StatusPreconditionFailed)
108 | rb.Free()
109 | return
110 | }
111 | }
112 |
113 | // If-None-Match
114 | ifnone := ctx.Request.Header.Get("If-None-Match")
115 | if ifnone != "" && ((ifnone == "*" && etag != "") || etagWeakCmp(ifnone, etag)) {
116 | // defer rb.Reset()
117 | if isEtagMethod(ctx.Request.Method) {
118 | rb.Header().Set("ETag", etag)
119 | rb.Header().Add("Vary", "If-None-Match")
120 | rb.WriteHeader(http.StatusNotModified)
121 | rb.Reset()
122 | return
123 | }
124 | ctx.WriteHeader(http.StatusPreconditionFailed)
125 | rb.Free()
126 | return
127 | }
128 |
129 | // If-Modified-Since
130 | ifmods := ctx.Request.Header.Get("If-Modified-Since")
131 | if ifnone == "" && ifmods != "" && !isEtagMethod(ctx.Request.Method) {
132 | modtime, _ := time.Parse(http.TimeFormat, ifmods)
133 | lastmod, _ := time.Parse(http.TimeFormat, rb.Header().Get("Last-Modified"))
134 | if !modtime.IsZero() && !lastmod.IsZero() && (lastmod.Before(modtime) || lastmod.Equal(modtime)) {
135 | if etag != "" {
136 | rb.Header().Set("ETag", etag)
137 | rb.Header().Add("Vary", "If-None-Match")
138 | }
139 | rb.Header().Add("Vary", "If-Modified-Since")
140 | rb.WriteHeader(http.StatusNotModified)
141 | rb.Reset()
142 | return
143 | }
144 | }
145 | }
146 |
147 | Finish:
148 | if etag != "" {
149 | rb.Header().Set("ETag", etag)
150 | rb.Header().Add("Vary", "If-None-Match")
151 | }
152 | }
153 | }
154 |
155 | func isEtagMethod(m string) bool {
156 | return m == "GET" || m == "HEAD"
157 | }
158 |
--------------------------------------------------------------------------------
/filter/gzip/doc.go:
--------------------------------------------------------------------------------
1 | // Copyright 2014 Codehack http://codehack.com
2 | // Use of this source code is governed by a MIT-style
3 | // license that can be found in the LICENSE file.
4 |
5 | package gzip
6 |
7 | // Version is the semantic version of this package
8 | // More info: https://semver.org
9 | const Version = "1.0.0"
10 |
--------------------------------------------------------------------------------
/filter/gzip/gzip.go:
--------------------------------------------------------------------------------
1 | // Copyright 2014 Codehack http://codehack.com
2 | // Use of this source code is governed by a MIT-style
3 | // license that can be found in the LICENSE file.
4 |
5 | package gzip
6 |
7 | import (
8 | "compress/gzip"
9 | "strings"
10 |
11 | "github.com/srfrog/go-relax"
12 | )
13 |
14 | // Filter Gzip compresses the response with gzip encoding, if the client
15 | // indicates support for it.
16 | type Filter struct {
17 | // CompressionLevel specifies the level of compression used for gzip.
18 | // Value must be between -1 (gzip.DefaultCompression) to 9 (gzip.BestCompression)
19 | // A value of 0 (gzip.DisableCompression) will disable compression.
20 | // Defaults to ``gzip.BestSpeed``
21 | CompressionLevel int
22 |
23 | // MinLength is the minimum content length, in bytes, required to do compression.
24 | // Defaults to 100
25 | MinLength int
26 | }
27 |
28 | /*
29 | Run runs the filter and passes down the following Info:
30 |
31 | ctx.Get("content.gzip") // boolean; whether gzip actually happened.
32 |
33 | The info passed is used by ETag to generate distinct entity-tags for gzip'ed
34 | content.
35 | */
36 | func (f *Filter) Run(next relax.HandlerFunc) relax.HandlerFunc {
37 | if f.CompressionLevel == 0 || f.CompressionLevel > gzip.BestCompression {
38 | f.CompressionLevel = gzip.BestSpeed
39 | }
40 | if f.MinLength == 0 {
41 | f.MinLength = 100
42 | }
43 | return func(ctx *relax.Context) {
44 | // ctx.Set("content.gzip", false)
45 | ctx.Header().Add("Vary", "Accept-Encoding")
46 |
47 | encodings := ctx.Request.Header.Get("Accept-Encoding")
48 | if f.CompressionLevel == 0 || !(strings.Contains(encodings, "gzip") || encodings == "*") {
49 | next(ctx)
50 | return
51 | }
52 |
53 | // don't compress ranged responses.
54 | if ctx.Request.Header.Get("If-Range") != "" {
55 | next(ctx)
56 | return
57 | }
58 |
59 | // Check for encoding preferences.
60 | if prefs, err := relax.ParsePreferences(encodings); err == nil && len(prefs) > 1 {
61 | if xgzip, ok := prefs["x-gzip"]; ok {
62 | prefs["gzip"] = xgzip
63 | }
64 | for _, value := range prefs {
65 | // Client prefers another encoding better, we may support it in another
66 | // filter. Let that filter handle it instead.
67 | if value > prefs["gzip"] {
68 | next(ctx)
69 | return
70 | }
71 | }
72 | }
73 |
74 | rb := relax.NewResponseBuffer(ctx)
75 | next(ctx.Clone(rb))
76 | defer rb.Flush(ctx)
77 |
78 | switch {
79 | // this might happen when FilterETag runs after GZip
80 | case rb.Status() == 304:
81 | ctx.WriteHeader(304)
82 | case rb.Status() == 204, rb.Status() > 299, rb.Status() < 200:
83 | break
84 | case rb.Header().Get("Content-Range") != "":
85 | break
86 | case strings.Contains(rb.Header().Get("Content-Encoding"), "gzip"):
87 | break
88 | case rb.Len() < f.MinLength:
89 | break
90 | default:
91 | gz, err := gzip.NewWriterLevel(ctx.ResponseWriter, f.CompressionLevel)
92 | if err != nil {
93 | return
94 | }
95 | defer gz.Close()
96 |
97 | // Only set if gzip actually happened.
98 | ctx.Set("content.gzip", true)
99 |
100 | rb.Header().Add("Content-Encoding", "gzip")
101 |
102 | // Check if ETag is set, alter it to reflect gzip content.
103 | if etag := rb.Header().Get("ETag"); etag != "" && !strings.Contains(etag, "gzip") {
104 | etagGzip := strings.TrimSuffix(etag, `"`) + `-gzip"`
105 | rb.Header().Set("ETag", etagGzip)
106 | }
107 |
108 | rb.FlushHeader(ctx.ResponseWriter)
109 | rb.WriteTo(gz)
110 | rb.Free()
111 | }
112 | }
113 | }
114 |
--------------------------------------------------------------------------------
/filter/limits/container.go:
--------------------------------------------------------------------------------
1 | // Copyright 2014 Codehack http://codehack.com
2 | // Use of this source code is governed by a MIT-style
3 | // license that can be found in the LICENSE file.
4 |
5 | package limits
6 |
7 | import (
8 | "time"
9 |
10 | "camlistore.org/pkg/lru"
11 | )
12 |
13 | // Container objects that implement this interface can serve as token bucket
14 | // containers.
15 | type Container interface {
16 | // Capacity returns the max number of tokens per client.
17 | Capacity() int
18 |
19 | // Consume takes tokens from a bucket.
20 | // Returns the number of tokens available, time in seconds for next one, and
21 | // a boolean indicating whether of not a token was consumed.
22 | Consume(string, int) (int, int, bool)
23 |
24 | // Reset will fill-up a bucket regardless of time/count.
25 | Reset(string)
26 | }
27 |
28 | // MemBucket implements Container using an in-memory LRU cache.
29 | // This container is ideal for single-host applications, and it's go-routine
30 | // safe.
31 | type MemBucket struct {
32 | Size int // max tokens allowed, capacity.
33 | Rate int // tokens added per minute
34 | Cache *lru.Cache // LRU cache storage
35 | }
36 |
37 | type tokenBucket struct {
38 | Tokens int // current token count
39 | When time.Time // time of last check
40 | }
41 |
42 | // NewMemBucket returns a new MemBucket container object. It initializes
43 | // the LRU cache with 'maxKeys'.
44 | func NewMemBucket(maxKeys, capacity, rate int) Container {
45 | return &MemBucket{
46 | Size: capacity,
47 | Rate: rate,
48 | Cache: lru.New(maxKeys),
49 | }
50 | }
51 |
52 | // Capacity returns the total size of the container (bucket)
53 | func (b *MemBucket) Capacity() int {
54 | return b.Size
55 | }
56 |
57 | // Consume removes a token from the key-indexed bucket at n-rate.
58 | func (b *MemBucket) Consume(key string, n int) (int, int, bool) {
59 | tb := b.fill(key)
60 | if tb.Tokens < n {
61 | return tb.Tokens, b.wait(n - tb.Tokens), false
62 | }
63 | tb.Tokens -= n
64 | return tb.Tokens, b.wait(b.Size), true
65 | }
66 |
67 | // Reset re-fills the bucket and resets the rate.
68 | func (b *MemBucket) Reset(key string) {
69 | cache, ok := b.Cache.Get(key)
70 | if ok {
71 | tb := cache.(*tokenBucket)
72 | tb.Tokens = b.Size
73 | tb.When = time.Now()
74 | }
75 | }
76 |
77 | func (b *MemBucket) wait(needed int) int {
78 | estimate := float64(needed/b.Rate) + float64(needed%b.Rate)*(1e-9/60.0)*60.0
79 | return int(estimate)
80 | }
81 |
82 | func (b *MemBucket) fill(key string) *tokenBucket {
83 | now := time.Now()
84 | cache, ok := b.Cache.Get(key)
85 | if !ok {
86 | tb := &tokenBucket{
87 | Tokens: b.Size,
88 | When: now,
89 | }
90 | b.Cache.Add(key, tb)
91 | return tb
92 | }
93 | tb := cache.(*tokenBucket)
94 | if tb.Tokens < b.Size {
95 | delta := float64(b.Rate) * time.Since(tb.When).Minutes()
96 | tb.Tokens = Min(b.Size, tb.Tokens+int(delta))
97 | }
98 | tb.When = now
99 | return tb
100 | }
101 |
--------------------------------------------------------------------------------
/filter/limits/container_redis.go:
--------------------------------------------------------------------------------
1 | // Copyright 2014 Codehack http://codehack.com
2 | // Use of this source code is governed by a MIT-style
3 | // license that can be found in the LICENSE file.
4 |
5 | package limits
6 |
7 | import (
8 | "net/url"
9 | "time"
10 |
11 | "github.com/garyburd/redigo/redis"
12 | )
13 |
14 | // RedisBucket implements Container using Redis strings.
15 | type RedisBucket struct {
16 | Size int // max tokens allowed
17 | Rate int // tokens added per second
18 | Pool *redis.Pool
19 | }
20 |
21 | // Capacity returns the max number of tokens per client
22 | func (b *RedisBucket) Capacity() int {
23 | return b.Size
24 | }
25 |
26 | // Consume takes tokens from a bucket.
27 | // Returns the number of tokens available, time in seconds for next one, and
28 | // a boolean indicating whether of not a token was consumed.
29 | func (b *RedisBucket) Consume(key string, n int) (int, int, bool) {
30 | tokens := b.fill(key)
31 | if tokens < n {
32 | return tokens, b.wait(n - tokens), false
33 | }
34 | c := b.Pool.Get()
35 | defer c.Close()
36 | tokens, _ = redis.Int(c.Do("DECRBY", key, n))
37 | return tokens, b.wait(b.Size), true
38 | }
39 |
40 | // Reset will fill-up a bucket regardless of time/count.
41 | func (b *RedisBucket) Reset(key string) {
42 | c := b.Pool.Get()
43 | defer c.Close()
44 | panicIf(c.Send("SET", key, b.Size, "EX", b.wait(b.Size), "XX"))
45 | }
46 |
47 | func (b *RedisBucket) wait(needed int) int {
48 | estimate := float64(needed/b.Rate) + float64(needed%b.Rate)*(1e-9/60.0)*60.0
49 | return int(estimate)
50 | }
51 |
52 | func (b *RedisBucket) fill(key string) int {
53 | var ttl, tokens int
54 |
55 | c := b.Pool.Get()
56 | defer c.Close()
57 |
58 | c.Send("MULTI")
59 | c.Send("TTL", key)
60 | c.Send("GET", key)
61 | values, err := redis.Values(c.Do("EXEC"))
62 | if err != nil {
63 | c.Do("DISCARD")
64 | return 0
65 | }
66 |
67 | if _, err := redis.Scan(values, &ttl, &tokens); err != nil {
68 | panicIf(err)
69 | return 0
70 | }
71 |
72 | when := b.wait(b.Size)
73 |
74 | if ttl == -2 {
75 | panicIf(c.Send("SET", key, b.Size, "EX", when))
76 | return b.Size
77 | }
78 |
79 | if tokens < b.Size {
80 | since := when - ttl
81 | if since > 60 {
82 | delta := float64(b.Rate) * (time.Duration(since) * time.Second).Minutes()
83 | tokens = Min(b.Size, tokens+int(delta))
84 | panicIf(c.Send("SET", key, tokens, "EX", when, "XX"))
85 | return tokens
86 | }
87 | }
88 |
89 | panicIf(c.Send("EXPIRE", key, when))
90 | return tokens
91 | }
92 |
93 | // newRedisPool returns a new Redis connection pool.
94 | // It expects an absolute URI with the format:
95 | //
96 | // {network}://:{auth@}{host:port}/{index}
97 | //
98 | // Where:
99 | // {network} is "tcp" or "udp" for network type.
100 | // {auth} is authentication password.
101 | // {host:[port]} host address with optional port.
102 | // {index} an optional database index
103 | //
104 | // Example:
105 | // tcp://:secret@company.com:1234/5
106 | //
107 | // Defaults to port 6379 and index 0.
108 | func newRedisPool(uri string) *redis.Pool {
109 | var auth, idx string
110 |
111 | u, err := url.Parse(uri)
112 | panicIf(err)
113 |
114 | if _, port := SplitPort(u.Host); port == "" {
115 | u.Host += ":6379"
116 | }
117 |
118 | if u.User != nil {
119 | if value, ok := u.User.Password(); ok {
120 | auth = value
121 | }
122 | }
123 |
124 | if u.Path != "" {
125 | idx = u.Path[1:]
126 | }
127 |
128 | return &redis.Pool{
129 | MaxIdle: 10,
130 | MaxActive: 100,
131 | IdleTimeout: 300 * time.Second,
132 | Dial: func() (redis.Conn, error) {
133 | c, err := redis.Dial(u.Scheme, u.Host)
134 | if err != nil {
135 | return nil, err
136 | }
137 | if auth != "" {
138 | if err = c.Send("AUTH", auth); err != nil {
139 | c.Close()
140 | return nil, err
141 | }
142 | }
143 | if idx != "" {
144 | if err := c.Send("SELECT", idx); err != nil {
145 | c.Close()
146 | return nil, err
147 | }
148 | }
149 | return c, err
150 | },
151 | TestOnBorrow: func(c redis.Conn, t time.Time) error {
152 | _, err := c.Do("PING")
153 | return err
154 | },
155 | }
156 | }
157 |
158 | // NewRedisBucket returns a new Redis bucket.
159 | func NewRedisBucket(uri string, capacity, rate int) *RedisBucket {
160 | return &RedisBucket{
161 | Size: capacity,
162 | Rate: rate,
163 | Pool: newRedisPool(uri),
164 | }
165 | }
166 |
167 | func panicIf(err error) {
168 | if err != nil {
169 | panic(err)
170 | }
171 | }
172 |
--------------------------------------------------------------------------------
/filter/limits/doc.go:
--------------------------------------------------------------------------------
1 | // Copyright 2014 Codehack http://codehack.com
2 | // Use of this source code is governed by a MIT-style
3 | // license that can be found in the LICENSE file.
4 |
5 | package limits
6 |
7 | // Version is the semantic version of this package
8 | // More info: https://semver.org
9 | const Version = "1.0.0"
10 |
--------------------------------------------------------------------------------
/filter/limits/example_test.go:
--------------------------------------------------------------------------------
1 | // Copyright 2014 Codehack http://codehack.com
2 | // Use of this source code is governed by a MIT-style
3 | // license that can be found in the LICENSE file.
4 |
5 | package limits_test
6 |
7 | import (
8 | "time"
9 |
10 | "github.com/srfrog/go-relax"
11 | "github.com/srfrog/go-relax/filter/limits"
12 | )
13 |
14 | type Count int
15 |
16 | func (c *Count) Index(ctx *relax.Context) {
17 | *c += 1
18 | ctx.Respond(c)
19 | }
20 |
21 | // Example_basic creates a new service under path "/" and serves requests
22 | // for the count resource.
23 | func Example_basic() {
24 | c := Count(0)
25 | svc := relax.NewService("/")
26 |
27 | // Memory limit check, allocation 250kb
28 | svc.Use(&limits.Memory{Alloc: 250 * 1024})
29 |
30 | // Throttle limit, 1 request per 200ms
31 | svc.Use(&limits.Throttle{
32 | Burst: 5,
33 | Requests: 1,
34 | Per: time.Minute * 3,
35 | })
36 |
37 | // Usage limit check, 10 tokens
38 | svc.Use(&limits.Usage{
39 | Container: limits.NewRedisBucket("tcp://127.0.0.1", 10, 1),
40 | })
41 |
42 | svc.Resource(&c)
43 | svc.Run()
44 | // Output:
45 | }
46 |
--------------------------------------------------------------------------------
/filter/limits/memory.go:
--------------------------------------------------------------------------------
1 | // Copyright 2014 Codehack http://codehack.com
2 | // Use of this source code is governed by a MIT-style
3 | // license that can be found in the LICENSE file.
4 |
5 | package limits
6 |
7 | import (
8 | "net/http"
9 | "runtime"
10 | "strconv"
11 | "time"
12 |
13 | "github.com/srfrog/go-relax"
14 | )
15 |
16 | // Global memstats, shared by all Filter objects.
17 | var c0 runtime.MemStats
18 |
19 | // Memory sets limits on application and system memory usage. The memory
20 | // stats are updated every minute and compared. If any limit is reached,
21 | // a response is sent with HTTP status 503-"Service Unavailable".
22 | // See also, runtime.MemStats
23 | type Memory struct {
24 | // Allow sets a limit on current used memory size, in bytes. This value
25 | // ideally should be a number multiple of 2.
26 | // Defaults to 0 (disabled)
27 | // Alloc: 5242880 // 5MB
28 | Alloc uint64
29 |
30 | // Sys sets a limit on system memory usage size, in bytes. This value
31 | // ideally should be a number multiple of 2.
32 | // Defaults to 1e9 (1000000000 bytes)
33 | Sys uint64
34 |
35 | // RetryAfter is a suggested retry-after period, in seconds, as recommended
36 | // in http://tools.ietf.org/html/rfc7231#section-6.6.4
37 | // If zero, no header is sent.
38 | // Defaults to 0 (no header sent)
39 | RetryAfter int
40 | }
41 |
42 | // Run processes the filter. No info is passed.
43 | func (f *Memory) Run(next relax.HandlerFunc) relax.HandlerFunc {
44 | if f.Sys == 0 {
45 | f.Sys = 1e9 // 1GB system memory
46 | }
47 | return func(ctx *relax.Context) {
48 | // Check memory limits
49 | if (f.Alloc != 0 && c0.Alloc > f.Alloc) || (f.Sys != 0 && c0.Sys > f.Sys) {
50 | if f.RetryAfter != 0 {
51 | ctx.Header().Set("Retry-After", strconv.Itoa(f.RetryAfter))
52 | }
53 | http.Error(ctx, http.StatusText(http.StatusServiceUnavailable), http.StatusServiceUnavailable)
54 | return
55 | }
56 |
57 | next(ctx)
58 | }
59 | }
60 |
61 | // updateMemStats will update our MemStats values every minute.
62 | func updateMemStats() {
63 | for range time.Tick(time.Minute) {
64 | runtime.MemProfileRate = 0
65 | runtime.ReadMemStats(&c0)
66 | }
67 | }
68 |
69 | func init() {
70 | runtime.MemProfileRate = 0
71 | runtime.ReadMemStats(&c0)
72 | go updateMemStats()
73 | }
74 |
--------------------------------------------------------------------------------
/filter/limits/throttle.go:
--------------------------------------------------------------------------------
1 | // Copyright 2014 Codehack http://codehack.com
2 | // Use of this source code is governed by a MIT-style
3 | // license that can be found in the LICENSE file.
4 |
5 | package limits
6 |
7 | import (
8 | "net/http"
9 | "time"
10 |
11 | "github.com/srfrog/go-relax"
12 | )
13 |
14 | // Throttle allows to limit the rate of requests to a resource per specific time duration.
15 | // It uses Go's channels to receive time tick updates. If a request is made before the channel
16 | // is updated, the request is dropped with HTTP status code 429-"Too Many Requests".
17 | type Throttle struct {
18 | // Request is the number of requests to allow per time duration.
19 | // Defaults to 100
20 | Requests int
21 |
22 | // Burst is the number of burst requests allowed before enforcing a time limit.
23 | // Defaults to 0
24 | Burst int
25 |
26 | // Per is the unit of time to quantize requests. This value is divided by the
27 | // value of Requests to get the time period to throttle.
28 | // Defaults to 1 second (time.Second)
29 | Per time.Duration
30 | }
31 |
32 | // Run processes the filter. No info is passed.
33 | func (f *Throttle) Run(next relax.HandlerFunc) relax.HandlerFunc {
34 | if f.Requests == 0 {
35 | f.Requests = 100
36 | }
37 | if f.Per == 0 {
38 | f.Per = time.Second
39 | }
40 |
41 | limiter := f.process()
42 | return func(ctx *relax.Context) {
43 | select {
44 | case <-limiter:
45 | next(ctx)
46 | default:
47 | http.Error(ctx, http.StatusText(relax.StatusTooManyRequests), relax.StatusTooManyRequests)
48 | return
49 | }
50 | }
51 | }
52 |
53 | func (f *Throttle) process() chan time.Time {
54 | limiter := make(chan time.Time, f.Burst)
55 | go func() {
56 | for i := 0; i < f.Burst; i++ {
57 | limiter <- time.Now()
58 | }
59 | for t := range time.Tick(f.Per / time.Duration(f.Requests)) {
60 | limiter <- t
61 | }
62 | }()
63 | return limiter
64 | }
65 |
--------------------------------------------------------------------------------
/filter/limits/usage.go:
--------------------------------------------------------------------------------
1 | // Copyright 2014 Codehack http://codehack.com
2 | // Use of this source code is governed by a MIT-style
3 | // license that can be found in the LICENSE file.
4 |
5 | package limits
6 |
7 | import (
8 | "net/http"
9 | "strconv"
10 |
11 | "github.com/srfrog/go-relax"
12 | )
13 |
14 | // Usage monitors request usage limits to the service, resource or to specific
15 | // route(s). It uses Container objects to implement the token-bucket (TB) algorithm.
16 | // TB is useful for limiting number of requests and burstiness.
17 | //
18 | // Each client is assigned a (semi) unique key and given a bucket of tokens
19 | // to spend per request. If a client consumes all its tokens, a response is
20 | // sent with HTTP status 429-"Too Many Requests". At this time the client won't
21 | // be allowed any more requests until a renewal period has passed. Repeated
22 | // attempts while the timeout is in effect will simply reset the timer,
23 | // prolonging the wait and dropping then new request.
24 | //
25 | // See also, https://en.wikipedia.org/wiki/Token_bucket
26 | type Usage struct {
27 | // Container is an interface implemented by the bucket device.
28 | // The default container, MemBucket, is a memory-based container which stores
29 | // keys in an LRU cache. This container monitors a maximum number of keys,
30 | // and this value should be according to the system's available memory.
31 | // Defaults to a MemBucket container, with the values:
32 | //
33 | // maxKeys = 1000 // number of keys to monitor.
34 | // capacity = 100 // total tokens per key.
35 | // fillrate = 1 // tokens renewed per minute per key.
36 | //
37 | // See also, MemBucket
38 | Container
39 |
40 | // Ration is the number of tokens to consume per request.
41 | // Defaults to 1.
42 | Ration int
43 |
44 | // Keygen is a function used to generate semi-unique ID's for each client.
45 | // The default function, MD5RequestKey, uses an MD5 hash on client address
46 | // and user agent, or the username of an authenticated client.
47 | Keygen func(relax.Context) string
48 | }
49 |
50 | // Run processes the filter. No info is passed.
51 | func (f *Usage) Run(next relax.HandlerFunc) relax.HandlerFunc {
52 | if f.Container == nil {
53 | f.Container = NewMemBucket(1000, 100, 1)
54 | }
55 | if f.Keygen == nil {
56 | f.Keygen = MD5RequestKey
57 | }
58 | if f.Ration == 0 {
59 | f.Ration = 1
60 | }
61 | return func(ctx *relax.Context) {
62 | // Usage limits
63 | key := f.Keygen(*ctx)
64 | tokens, when, ok := f.Consume(key, f.Ration)
65 | if !ok {
66 | ctx.Header().Set("Retry-After", strconv.Itoa(when))
67 | http.Error(ctx, http.StatusText(relax.StatusTooManyRequests), relax.StatusTooManyRequests)
68 | return
69 | }
70 | ctx.Header().Set("RateLimit-Limit", strconv.Itoa(f.Capacity()))
71 | ctx.Header().Set("RateLimit-Remaining", strconv.Itoa(tokens))
72 | ctx.Header().Set("RateLimit-Reset", strconv.Itoa(when))
73 |
74 | next(ctx)
75 | }
76 | }
77 |
--------------------------------------------------------------------------------
/filter/limits/util.go:
--------------------------------------------------------------------------------
1 | // Copyright 2014 Codehack http://codehack.com
2 | // Use of this source code is governed by a MIT-style
3 | // license that can be found in the LICENSE file.
4 |
5 | package limits
6 |
7 | import (
8 | "crypto/md5"
9 | "encoding/hex"
10 |
11 | "github.com/srfrog/go-relax"
12 | )
13 |
14 | // Min returns the smaller integer between a and b.
15 | // If a is lesser than b it returns a, otherwise returns b.
16 | func Min(a, b int) int {
17 | if a < b {
18 | return a
19 | }
20 | return b
21 | }
22 |
23 | // MD5RequestKey returns a key made from MD5 hash of Request.RemoteAddr and
24 | // Request.UserAgent.
25 | func MD5RequestKey(c relax.Context) string {
26 | h := md5.New()
27 | host, _ := SplitPort(c.Request.RemoteAddr)
28 | h.Write([]byte(host))
29 | h.Write([]byte(c.Request.UserAgent()))
30 | return "quota:" + hex.EncodeToString(h.Sum(nil))
31 | }
32 |
33 | // SplitPort splits an host:port address and returns the parts.
34 | func SplitPort(addr string) (string, string) {
35 | for i := len(addr) - 1; i >= 0; i-- {
36 | if addr[i] == ':' {
37 | return addr[:i], addr[i+1:]
38 | }
39 | }
40 | return addr, ""
41 | }
42 |
--------------------------------------------------------------------------------
/filter/logs/doc.go:
--------------------------------------------------------------------------------
1 | // Copyright 2014 Codehack http://codehack.com
2 | // Use of this source code is governed by a MIT-style
3 | // license that can be found in the LICENSE file.
4 |
5 | package logs
6 |
7 | // Version is the semantic version of this package
8 | // More info: https://semver.org
9 | const Version = "1.0.0"
10 |
--------------------------------------------------------------------------------
/filter/logs/logs.go:
--------------------------------------------------------------------------------
1 | // Copyright 2014 Codehack http://codehack.com
2 | // Use of this source code is governed by a MIT-style
3 | // license that can be found in the LICENSE file.
4 |
5 | package logs
6 |
7 | import (
8 | "log"
9 | "os"
10 |
11 | "github.com/srfrog/go-relax"
12 | )
13 |
14 | // Pre-made log formats. Most are based on Apache HTTP's.
15 | // Note: the [n] notation will index an specific argument from Sprintf list.
16 | const (
17 | // LogFormatRelax is the default Relax post-event format
18 | LogFormatRelax = "%s [%-.8[1]L] \"%#[1]r\" => \"%#[1]s\" done in %.6[1]Ds"
19 |
20 | // LogFormatCommon is similar to Apache HTTP's Common Log Format (CLF)
21 | LogFormatCommon = "%h %[1]l %[1]u %[1]t \"%[1]r\" %#[1]s %[1]b"
22 |
23 | // LogFormatExtended is similar to NCSA extended/combined log format
24 | LogFormatExtended = LogFormatCommon + " \"%[1]R\" \"%[1]A\""
25 |
26 | // LogFormatReferer is similar to Apache HTTP's Referer log format
27 | LogFormatReferer = "%R -> %[1]U"
28 | )
29 |
30 | /*
31 | Filter Log provides pre- and post-request event logs. It uses a custom
32 | log format similar to the one used for Apache HTTP CustomLog directive.
33 |
34 | myservice.Use(logrus.New())
35 | log := &log.Filter{Logger: myservice.Logger(), PreLogFormat: LogFormatReferer}
36 | log.Println("Filter implements Logger.")
37 |
38 | // Context-specific format verbs (see Context.Format)
39 | log.Panicf("Status is %s = bad status!", ctx)
40 |
41 | */
42 | type Filter struct {
43 | // Logger is an interface that is based on Go's log package. Any logging
44 | // system that implements Logger can be used.
45 | // Defaults to the stdlog in 'log' package.
46 | relax.Logger
47 |
48 | // PreLogFormat is the format for the pre-request log entry.
49 | // Leave empty if no log even is needed.
50 | // Default to empty (no pre-log)
51 | PreLogFormat string
52 |
53 | // PostLogFormat is the format for the post-request log entry.
54 | // Defaults to the value of LogFormatRelax
55 | PostLogFormat string
56 | }
57 |
58 | // Run processes the filter. No info is passed.
59 | func (f *Filter) Run(next relax.HandlerFunc) relax.HandlerFunc {
60 | if f.Logger == nil {
61 | f.Logger = log.New(os.Stderr, "", log.LstdFlags)
62 | }
63 | if f.PostLogFormat == "" {
64 | f.PostLogFormat = LogFormatRelax
65 | }
66 |
67 | return func(ctx *relax.Context) {
68 | if f.PreLogFormat != "" {
69 | f.Printf(f.PreLogFormat, ctx)
70 | }
71 |
72 | next(ctx)
73 |
74 | f.Printf(f.PostLogFormat, ctx)
75 | }
76 | }
77 |
--------------------------------------------------------------------------------
/filter/multipart/doc.go:
--------------------------------------------------------------------------------
1 | // Copyright 2014 Codehack http://codehack.com
2 | // Use of this source code is governed by a MIT-style
3 | // license that can be found in the LICENSE file.
4 |
5 | package multipart
6 |
7 | // Version is the semantic version of this package
8 | // More info: https://semver.org
9 | const Version = "1.0.0"
10 |
--------------------------------------------------------------------------------
/filter/multipart/multipart.go:
--------------------------------------------------------------------------------
1 | // Copyright 2016 Codehack http://codehack.com
2 | // Use of this source code is governed by a MIT-style
3 | // license that can be found in the LICENSE file.
4 |
5 | package multipart
6 |
7 | import (
8 | "mime"
9 | "net/http"
10 | "path/filepath"
11 |
12 | "github.com/srfrog/go-relax"
13 | )
14 |
15 | // DefaultMaxMemory is 4 MiB for storing a request body in memory.
16 | const (
17 | DefaultMaxMemory = 1 << 22
18 | )
19 |
20 | // Filter Multipart handles multipart file uploads via a specific path.
21 | type Filter struct {
22 | // MaxMemory total bytes of the request body that is stored in memory.
23 | // Increase this value if you expect large documents.
24 | // Default: 4 MiB
25 | MaxMemory int64
26 | }
27 |
28 | // Run runs the filter and passes down the following Info:
29 | //
30 | // ctx.Get("multipart.files") // list of files processed (*[]*FileHeader)
31 | //
32 | func (f *Filter) Run(next relax.HandlerFunc) relax.HandlerFunc {
33 | if f.MaxMemory == 0 {
34 | f.MaxMemory = DefaultMaxMemory
35 | }
36 |
37 | return func(ctx *relax.Context) {
38 | if ctx.Request.Method != "POST" {
39 | next(ctx)
40 | return
41 | }
42 |
43 | ct, _, err := mime.ParseMediaType(ctx.Request.Header.Get("Content-Type"))
44 | if err != nil {
45 | ctx.Error(http.StatusBadRequest, err.Error())
46 | return
47 | }
48 |
49 | if ct != "multipart/form-data" {
50 | ctx.Error(http.StatusUnsupportedMediaType,
51 | "That media type is not supported for transfer.",
52 | "Expecting multipart/form-data")
53 | return
54 | }
55 |
56 | if err := ctx.Request.ParseMultipartForm(f.MaxMemory); err != nil {
57 | ctx.Error(http.StatusBadRequest, err.Error())
58 | return
59 | }
60 |
61 | files, ok := ctx.Request.MultipartForm.File["files"]
62 | if !ok {
63 | ctx.Error(http.StatusBadRequest, "insufficient parameters")
64 | return
65 | }
66 |
67 | for i := range files {
68 | ext := filepath.Ext(filepath.Base(filepath.Clean(files[i].Filename)))
69 | if ext == "" {
70 | ctx.Error(http.StatusBadRequest, "could not get the file extension")
71 | return
72 | }
73 |
74 | if mime.TypeByExtension(ext) == "" {
75 | ctx.Error(http.StatusBadRequest, "file type is unknown")
76 | return
77 | }
78 | }
79 |
80 | ctx.Set("multipart.files", &files)
81 |
82 | next(ctx)
83 | }
84 | }
85 |
86 | // RunIn implements the LimitedFilter interface. This will limit this filter
87 | // to run only for router paths, not resources or service.
88 | func (f *Filter) RunIn(e interface{}) bool {
89 | switch e.(type) {
90 | case relax.Router:
91 | return true
92 | }
93 | return false
94 | }
95 |
--------------------------------------------------------------------------------
/filter/override/doc.go:
--------------------------------------------------------------------------------
1 | // Copyright 2014 Codehack http://codehack.com
2 | // Use of this source code is governed by a MIT-style
3 | // license that can be found in the LICENSE file.
4 |
5 | package override
6 |
7 | // Version is the semantic version of this package
8 | // More info: https://semver.org
9 | const Version = "1.0.0"
10 |
--------------------------------------------------------------------------------
/filter/override/override.go:
--------------------------------------------------------------------------------
1 | // Copyright 2014 Codehack http://codehack.com
2 | // Use of this source code is governed by a MIT-style
3 | // license that can be found in the LICENSE file.
4 |
5 | package override
6 |
7 | import (
8 | "net/http"
9 |
10 | "github.com/srfrog/go-relax"
11 | )
12 |
13 | // Filter Override changes the Request.Method if the client specifies
14 | // override via HTTP header or query. This allows clients with limited HTTP
15 | // verbs to send REST requests through GET/POST.
16 | type Filter struct {
17 | // Header expected for HTTP Method override
18 | // Default: "X-HTTP-Method-Override"
19 | Header string
20 |
21 | // QueryVar is used if header can't be set
22 | // Default: "_method"
23 | QueryVar string
24 |
25 | // Methods specifies the methods can be overridden.
26 | // Format is Methods["method"] = "override".
27 | // Default methods:
28 | // f.Methods = map[string]string{
29 | // "DELETE": "POST",
30 | // "OPTIONS": "GET",
31 | // "PATCH": "POST",
32 | // "PUT": "POST",
33 | // }
34 | Methods map[string]string
35 | }
36 |
37 | // Run runs the filter and passes down the following Info:
38 | //
39 | // ctx.Get("override.method") // method replaced. e.g., "DELETE"
40 | //
41 | func (f *Filter) Run(next relax.HandlerFunc) relax.HandlerFunc {
42 | if f.Header == "" {
43 | f.Header = "X-HTTP-Method-Override"
44 | }
45 | if f.QueryVar == "" {
46 | f.QueryVar = "_method"
47 | }
48 | if f.Methods == nil {
49 | f.Methods = map[string]string{
50 | "DELETE": "POST",
51 | "OPTIONS": "GET",
52 | "PATCH": "POST",
53 | "PUT": "POST",
54 | }
55 | }
56 |
57 | return func(ctx *relax.Context) {
58 | if override := ctx.Request.URL.Query().Get(f.QueryVar); override != "" {
59 | ctx.Request.Header.Set(f.Header, override)
60 | }
61 | if override := ctx.Request.Header.Get(f.Header); override != "" {
62 | if override != ctx.Request.Method {
63 | method, ok := f.Methods[override]
64 | if !ok {
65 | ctx.Error(http.StatusBadRequest, override+" method is not overridable.")
66 | return
67 | }
68 | // check that the caller method matches the expected override. e.g., used GET for OPTIONS
69 | if ctx.Request.Method != method {
70 | ctx.Error(http.StatusPreconditionFailed, "Must use "+method+" to override "+override)
71 | return
72 | }
73 | ctx.Request.Method = override
74 | ctx.Request.Header.Del(f.Header)
75 | ctx.Set("override.method", override)
76 | }
77 | }
78 | next(ctx)
79 | }
80 | }
81 |
--------------------------------------------------------------------------------
/filter/security/doc.go:
--------------------------------------------------------------------------------
1 | // Copyright 2014 Codehack http://codehack.com
2 | // Use of this source code is governed by a MIT-style
3 | // license that can be found in the LICENSE file.
4 |
5 | package security
6 |
7 | // Version is the semantic version of this package
8 | // More info: https://semver.org
9 | const Version = "1.0.0"
10 |
--------------------------------------------------------------------------------
/filter/security/security.go:
--------------------------------------------------------------------------------
1 | // Copyright 2014 Codehack http://codehack.com
2 | // Use of this source code is governed by a MIT-style
3 | // license that can be found in the LICENSE file.
4 |
5 | package security
6 |
7 | import (
8 | "net/http"
9 |
10 | "github.com/srfrog/go-relax"
11 | )
12 |
13 | const (
14 | securityUACheckErr = "Request forbidden by security rules.\nPlease make sure your request has an User-Agent header."
15 | securityXFrameDefault = "SAMEORIGIN"
16 | securityHSTSDefault = "max-age=31536000; includeSubDomains"
17 | securityCacheDefault = "no-store, must-revalidate"
18 | securityPragmaDefault = "no-cache"
19 | )
20 |
21 | // Filter Security provides some security options and checks.
22 | // Most of the options are HTTP headers sent back so that web clients can
23 | // adjust their configuration.
24 | // See https://www.owasp.org/index.php/List_of_useful_HTTP_headers
25 | type Filter struct {
26 | // UACheckDisable if false, a check is done to see if the client sent a valid non-empty
27 | // User-Agent header with the request.
28 | // Defaults to false.
29 | UACheckDisable bool
30 |
31 | // UACheckErrMsg is the response body sent when a client fails User-Agent check.
32 | // Defaults to (taken from Heroku's UA check message):
33 | // "Request forbidden by security rules.\n" +
34 | // "Please make sure your request has an User-Agent header."
35 | UACheckErrMsg string
36 |
37 | // XFrameDisable if false, will send a X-Frame-Options header with the response,
38 | // using the value in XFrameOptions. X-Frame-Options provides clickjacking protection.
39 | // For details see https://www.owasp.org/index.php/Clickjacking
40 | // https://www.rfc-editor.org/rfc/rfc7034.txt
41 | // http://tools.ietf.org/html/draft-ietf-websec-x-frame-options-12
42 | // Defaults to false.
43 | XFrameDisable bool
44 |
45 | // XFrameOptions expected values are:
46 | // "DENY" // no rendering within a frame
47 | // "SAMEORIGIN" // no rendering if origin mismatch
48 | // "ALLOW-FROM {origin}" // allow rendering if framed by frame loaded from {origin};
49 | // // where {origin} is a top-level URL. ie., http//codehack.com
50 | // Only one value can be used at a time.
51 | // Defaults to "SAMEORIGIN"
52 | XFrameOptions string
53 |
54 | // XCTODisable if false, will send a X-Content-Type-Options header with the response
55 | // using the value "nosniff". This prevents Internet Explorer and Google Chrome from
56 | // MIME-sniffing and ignoring the value set in Content-Type.
57 | // Defaults to false.
58 | XCTODisable bool
59 |
60 | // HSTSDisable if false, will send a Strict-Transport-Security (HSTS) header
61 | // with the respose, using the value in HSTSOptions. HSTS enforces secure
62 | // connections to the server. http://tools.ietf.org/html/rfc6797
63 | // If the server is not on a secure HTTPS/TLS connection, it will temporarily
64 | // change to true.
65 | // Defaults to false.
66 | HSTSDisable bool
67 |
68 | // HSTSOptions are the values sent in an HSTS header.
69 | // Expected values are one or both of:
70 | // "max-age=delta" // delta in seconds, the time this host is a known HSTS host
71 | // "includeSubDomains" // HSTS policy applies to this domain and all subdomains.
72 | // Defaults to "max-age=31536000; includeSubDomains"
73 | HSTSOptions string
74 |
75 | // CacheDisable if false, will send a Cache-Control header with the response,
76 | // using the value in CacheOptions. If this value is true, it will also
77 | // disable Pragma header (see below).
78 | // Defaults to false.
79 | CacheDisable bool
80 |
81 | // CacheOptions are the value sent in an Cache-Control header.
82 | // For details, see http://tools.ietf.org/html/rfc7234#section-5.2
83 | // Defaults to "no-store, must-revalidate"
84 | CacheOptions string
85 |
86 | // PragmaDisable if false and CacheDisable is false, will send a Pragma header
87 | // with the response, using the value "no-cache".
88 | // For details see http://tools.ietf.org/html/rfc7234#section-5.4
89 | // Defaults to false.
90 | PragmaDisable bool
91 | }
92 |
93 | // Run runs the filter.
94 | func (f *Filter) Run(next relax.HandlerFunc) relax.HandlerFunc {
95 | if f.UACheckErrMsg == "" {
96 | f.UACheckErrMsg = securityUACheckErr
97 | }
98 | if f.XFrameOptions == "" {
99 | f.XFrameOptions = securityXFrameDefault
100 | }
101 | if f.HSTSOptions == "" {
102 | f.HSTSOptions = securityHSTSDefault
103 | }
104 | if f.CacheOptions == "" {
105 | f.CacheOptions = securityCacheDefault
106 | }
107 | return func(ctx *relax.Context) {
108 | if !f.UACheckDisable {
109 | ua := ctx.Request.UserAgent()
110 | if ua == "" || ua == "Go 1.1 package http" {
111 | ctx.Error(http.StatusForbidden, f.UACheckErrMsg)
112 | return
113 | }
114 | }
115 |
116 | if !f.XCTODisable {
117 | ctx.Header().Set("X-Content-Type-Options", "nosniff")
118 | }
119 |
120 | if !f.XFrameDisable {
121 | ctx.Header().Set("X-Frame-Options", f.XFrameOptions)
122 | }
123 |
124 | // turn off HSTS if not on secure connection.
125 | if !f.HSTSDisable && relax.IsRequestSSL(ctx.Request) {
126 | ctx.Header().Set("Strict-Transport-Security", f.HSTSOptions)
127 | }
128 |
129 | if !f.CacheDisable {
130 | ctx.Header().Set("Cache-Control", f.CacheOptions)
131 | if !f.PragmaDisable {
132 | ctx.Header().Set("Pragma", "no-cache")
133 | }
134 | }
135 |
136 | next(ctx)
137 | }
138 | }
139 |
--------------------------------------------------------------------------------
/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/srfrog/go-relax
2 |
3 | go 1.17
4 |
5 | require (
6 | camlistore.org v0.0.0-20171230002226-a5a65f0d8b22
7 | github.com/garyburd/redigo v1.6.2
8 | github.com/gofrs/uuid v4.0.0+incompatible
9 | github.com/sirupsen/logrus v1.8.1
10 | github.com/srfrog/go-strarr v1.0.0
11 | )
12 |
13 | require (
14 | github.com/codehack/go-strarr v1.0.0 // indirect
15 | golang.org/x/sys v0.0.0-20191026070338-33540a1f6037 // indirect
16 | )
17 |
--------------------------------------------------------------------------------
/go.sum:
--------------------------------------------------------------------------------
1 | camlistore.org v0.0.0-20171230002226-a5a65f0d8b22 h1:VP9VuyosMHmS9zdzd5Co9TJKWPbMTfmtKc/XWctszyQ=
2 | camlistore.org v0.0.0-20171230002226-a5a65f0d8b22/go.mod h1:mzAP6ICVzPdfO0f3N9hAVWhO7qplHF7mbFhGsGdErTI=
3 | github.com/codehack/go-strarr v1.0.0 h1:L6DKn/bjetkBdnpbDA+0zai078/gQcRFVZpInfnfN90=
4 | github.com/codehack/go-strarr v1.0.0/go.mod h1:juAbRDiLuhU4fEyIIHqX/g+beXp4JnbTWKuGPrGmbF4=
5 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
6 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
7 | github.com/garyburd/redigo v1.6.2 h1:yE/pwKCrbLpLpQICzYTeZ7JsTA/C53wFTJHaEtRqniM=
8 | github.com/garyburd/redigo v1.6.2/go.mod h1:NR3MbYisc3/PwhQ00EMzDiPmrwpPxAn5GI05/YaO1SY=
9 | github.com/gofrs/uuid v4.0.0+incompatible h1:1SD/1F5pU8p29ybwgQSwpQk+mwdRrXCYuPhW6m+TnJw=
10 | github.com/gofrs/uuid v4.0.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
11 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
12 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
13 | github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE=
14 | github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
15 | github.com/srfrog/go-strarr v1.0.0 h1:UYP9F2BkH8BfVoseDo/HiyVuxM63YOsLe7rxkMlD5lk=
16 | github.com/srfrog/go-strarr v1.0.0/go.mod h1:DcnEDS6bk1IGT/yzAS97+d7ZZQ5ugCtWuxyVxYczeME=
17 | github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w=
18 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
19 | golang.org/x/sys v0.0.0-20191026070338-33540a1f6037 h1:YyJpGZS1sBuBCzLAR1VEpK193GlqGZbnPFnPV/5Rsb4=
20 | golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
21 |
--------------------------------------------------------------------------------
/linking.go:
--------------------------------------------------------------------------------
1 | // Copyright 2014 Codehack http://codehack.com
2 | // Use of this source code is governed by a MIT-style
3 | // license that can be found in the LICENSE file.
4 |
5 | package relax
6 |
7 | import (
8 | "fmt"
9 | "reflect"
10 | "strings"
11 | )
12 |
13 | /*
14 | Link an HTTP header tag that represents a hypertext relation link. It implements
15 | HTTP web links between resources that are not format specific.
16 |
17 | For details see also,
18 | Web Linking: :https://tools.ietf.org/html/rfc5988
19 | Relations: http://www.iana.org/assignments/link-relations/link-relations.xhtml
20 | Item and Collection Link Relations: http://tools.ietf.org/html/rfc6573
21 | Versioning: https://tools.ietf.org/html/rfc5829
22 | URI Template: http://tools.ietf.org/html/rfc6570
23 | Media: http://www.w3.org/TR/css3-mediaqueries/
24 |
25 | The field title* ``Titlex`` must be encoded as per RFC5987.
26 | See: http://greenbytes.de/tech/webdav/rfc5988.html#RFC5987
27 |
28 | Extension field ``Ext`` must be name lowercase and quoted-string value,
29 | as needed.
30 |
31 | Example:
32 |
33 | link := Link{
34 | URI: "/v1/schemas",
35 | Rel: "index",
36 | Ext: "priority=\"important\"",
37 | Title: "Definition of schemas",
38 | Titlex: "utf-8'es'\"Definición de esquemas\"",
39 | HrefLang: "en-US",
40 | Media: "screen, print",
41 | Type: "text/html;charset=utf-8",
42 | }
43 |
44 | */
45 | type Link struct {
46 | URI string `json:"href"`
47 | Rel string `json:"rel"`
48 | Anchor string `json:"anchor,omitempty"`
49 | Rev string `json:"rev,omitempty"`
50 | HrefLang string `json:"hreflang,omitempty"`
51 | Media string `json:"media,omitempty"`
52 | Title string `json:"title,omitempty"`
53 | Titlex string `json:"title*,omitempty"`
54 | Type string `json:"type,omitempty"`
55 | Ext string
56 | }
57 |
58 | // String returns a string representation of a Link object. Suitable for use
59 | // in "Link" HTTP headers.
60 | func (l *Link) String() string {
61 | link := fmt.Sprintf(`<%s>`, l.URI)
62 | e := reflect.ValueOf(l).Elem()
63 | for i, j := 1, e.NumField(); i < j; i++ {
64 | n, v := e.Type().Field(i).Name, e.Field(i).String()
65 | if n == "Rel" && v == "" {
66 | v = "alternate"
67 | }
68 | if v == "" {
69 | continue
70 | }
71 | if n == "Ext" {
72 | link += fmt.Sprintf(`; %s`, v)
73 | continue
74 | }
75 | if n == "Titlex" {
76 | link += fmt.Sprintf(`; title*=%s`, v)
77 | continue
78 | }
79 | link += fmt.Sprintf(`; %s=%q`, strings.ToLower(n), v)
80 | }
81 | return link
82 | }
83 |
84 | // LinkHeader returns a complete Link header value that can be plugged
85 | // into http.Header().Add(). Use this when you don't need a Link object
86 | // for your relation, just a header.
87 | // uri is the URI of target.
88 | // param is one or more name=value pairs for link values. if nil, will default
89 | // to rel="alternate" (as per https://tools.ietf.org/html/rfc4287#section-4.2.7).
90 | // Returns two strings: "Link","Link header spec"
91 | func LinkHeader(uri string, param ...string) (string, string) {
92 | value := []string{fmt.Sprintf(`<%s>`, uri)}
93 | if param == nil {
94 | param = []string{`rel="alternate"`}
95 | }
96 | value = append(value, param...)
97 | return "Link", strings.Join(value, "; ")
98 | }
99 |
100 | // relationHandler is a filter that adds link relations to the response.
101 | func (r *Resource) relationHandler(next HandlerFunc) HandlerFunc {
102 | return func(ctx *Context) {
103 | for _, link := range r.links {
104 | ctx.Header().Add("Link", link.String())
105 | }
106 | next(ctx)
107 | }
108 | }
109 |
110 | // NewLink inserts new link relation for a resource. If the relation already exists,
111 | // determined by comparing URI and relation type, then it is replaced with the new one.
112 | func (r *Resource) NewLink(link *Link) {
113 | for k, v := range r.links {
114 | if v.URI == link.URI && v.Rel == link.Rel {
115 | r.links[k] = link
116 | return
117 | }
118 | }
119 | r.links = append(r.links, link)
120 | }
121 |
--------------------------------------------------------------------------------
/resource.go:
--------------------------------------------------------------------------------
1 | // Copyright 2014 Codehack http://codehack.com
2 | // Use of this source code is governed by a MIT-style
3 | // license that can be found in the LICENSE file.
4 |
5 | package relax
6 |
7 | import (
8 | "fmt"
9 | "net/http"
10 | "strings"
11 | )
12 |
13 | /*
14 | Resourcer is any object that implements the this interface. A resource
15 | is a namespace where all operations for that resource happen.
16 |
17 | type Locations struct{
18 | City string
19 | Country string
20 | }
21 |
22 | // This function is needed for Locations to implement Resourcer
23 | func (l *Locations) Index (ctx *Context) { ctx.Respond(l) }
24 |
25 | loc := &Locations{City: "Scottsdale", Country: "US"}
26 | myresource := service.Resource(loc)
27 |
28 | */
29 | type Resourcer interface {
30 | // Index may serve the entry GET request to a resource. Such as the listing
31 | // of a collection.
32 | Index(*Context)
33 | }
34 |
35 | // Optioner is implemented by Resourcer objects that want to provide their own
36 | // response to OPTIONS requests.
37 | type Optioner interface {
38 | // Options may display details about the resource or how to access it.
39 | Options(*Context)
40 | }
41 |
42 | // CRUD is an interface for Resourcer objects that provide create, read,
43 | // update, and delete operations; also known as CRUD.
44 | type CRUD interface {
45 | // Create may allow the creation of new resource items via methods POST/PUT.
46 | Create(*Context)
47 |
48 | // Read may display a specific resource item given an ID or name via method GET.
49 | Read(*Context)
50 |
51 | // Update may allow updating resource items via methods PATCH/PUT.
52 | Update(*Context)
53 |
54 | // Delete may allow removing items from a resource via method DELETE.
55 | Delete(*Context)
56 | }
57 |
58 | // Resource is an object that implements Resourcer; serves requests for a resource.
59 | type Resource struct {
60 | service *Service // service points to the service this resource belongs
61 | name string // name of this resource, derived from collection
62 | path string // path is the URI to this resource
63 | collection interface{} // the object that implements Resourcer; a collection
64 | links []*Link // links contains all the relation links
65 | filters []Filter // list of resource-level filters
66 | }
67 |
68 | // Path similar to Service.Path but returns the path to this resource.
69 | // absolute whether or not it should return an absolute URL.
70 | func (r *Resource) Path(absolute bool) string {
71 | return r.service.Path(absolute) + strings.TrimPrefix(r.path[len(r.service.Path(false))-1:], "/")
72 | }
73 |
74 | // NotImplemented is a handler used to send a response when a resource route is
75 | // not yet implemented.
76 | // // Route "GET /myresource/apikey" => 501 Not Implemented
77 | // myresource.GET("apikey", myresource.NotImplemented)
78 | func (r *Resource) NotImplemented(ctx *Context) {
79 | ctx.Error(http.StatusNotImplemented, "That route is not implemented.")
80 | }
81 |
82 | // MethodNotAllowed is a handler used to send a response when a method is not
83 | // allowed.
84 | // // Route "PATCH /users/profile" => 405 Method Not Allowed
85 | // users.PATCH("profile", users.MethodNotAllowed)
86 | func (r *Resource) MethodNotAllowed(ctx *Context) {
87 | ctx.Header().Set("Allow", r.service.router.PathMethods(ctx.Request.URL.Path))
88 | ctx.Error(http.StatusMethodNotAllowed, "The method "+ctx.Request.Method+" is not allowed.")
89 | }
90 |
91 | // OptionsHandler responds to OPTION requests. It returns an Allow header listing
92 | // the methods allowed for an URI. If the URI is the Service's path then it returns information
93 | // about the service.
94 | func (r *Resource) OptionsHandler(ctx *Context) {
95 | methods := r.service.router.PathMethods(ctx.Request.URL.Path)
96 | ctx.Header().Set("Allow", methods)
97 | if strings.Contains(methods, "PATCH") {
98 | // FIXME: this is wrong! perhaps we need Patch.ContentType() or even Service.encoders keys.
99 | ctx.Header().Set("Accept-Patch", ctx.Get("content.encoding").(string))
100 | }
101 | if options, ok := r.collection.(Optioner); ok {
102 | options.Options(ctx)
103 | return
104 | }
105 | ctx.WriteHeader(http.StatusNoContent)
106 | }
107 |
108 | /*
109 | Route adds a resource route (method + path) and its handler to the router.
110 |
111 | 'method' is the HTTP method verb (GET, POST, ...). 'path' is the URI path and
112 | optional path matching expressions (PSE). 'h' is the handler function with
113 | signature HandlerFunc. 'filters' are route-level filters run before the handler.
114 | If the resource has its own filters, those are prepended to the filters list;
115 | resource-level filters will run before route-level filters.
116 |
117 | Returns the resource itself for chaining.
118 | */
119 | func (r *Resource) Route(method, path string, h HandlerFunc, filters ...Filter) *Resource {
120 | handler := r.relationHandler(h)
121 |
122 | // route-specific filters
123 | r.attachFilters(handler, filters...)
124 |
125 | // inherited resource filters
126 | r.attachFilters(handler, r.filters...)
127 |
128 | r.service.router.AddRoute(strings.ToUpper(method), r.path+"/"+path, handler)
129 |
130 | return r
131 | }
132 |
133 | func (r *Resource) attachFilters(h HandlerFunc, filters ...Filter) {
134 | if filters == nil {
135 | return
136 | }
137 | for i := len(filters) - 1; i >= 0; i-- {
138 | if l, ok := filters[i].(LimitedFilter); ok && !l.RunIn(r.service.Router) {
139 | continue
140 | }
141 | h = filters[i].Run(h)
142 | }
143 | }
144 |
145 | // DELETE is a convenient alias to Route using DELETE as method
146 | func (r *Resource) DELETE(path string, h HandlerFunc, filters ...Filter) *Resource {
147 | return r.Route("DELETE", path, h, filters...)
148 | }
149 |
150 | // GET is a convenient alias to Route using GET as method
151 | func (r *Resource) GET(path string, h HandlerFunc, filters ...Filter) *Resource {
152 | return r.Route("GET", path, h, filters...)
153 | }
154 |
155 | // OPTIONS is a convenient alias to Route using OPTIONS as method
156 | func (r *Resource) OPTIONS(path string, h HandlerFunc, filters ...Filter) *Resource {
157 | return r.Route("OPTIONS", path, h, filters...)
158 | }
159 |
160 | // PATCH is a convenient alias to Route using PATCH as method
161 | func (r *Resource) PATCH(path string, h HandlerFunc, filters ...Filter) *Resource {
162 | return r.Route("PATCH", path, h, filters...)
163 | }
164 |
165 | // POST is a convenient alias to Route using POST as method
166 | func (r *Resource) POST(path string, h HandlerFunc, filters ...Filter) *Resource {
167 | return r.Route("POST", path, h, filters...)
168 | }
169 |
170 | // PUT is a convenient alias to Route using PUT as method
171 | func (r *Resource) PUT(path string, h HandlerFunc, filters ...Filter) *Resource {
172 | return r.Route("PUT", path, h, filters...)
173 | }
174 |
175 | /*
176 | CRUD adds Create/Read/Update/Delete routes using the handlers in CRUD interface,
177 | if the object implements it. A typical resource will implement one or all of the
178 | handlers, but those that aren't implemented should respond with
179 | "Method Not Allowed" or "Not Implemented".
180 |
181 | pse is a route path segment expression (PSE) - see Router for details. If pse is
182 | empty string "", then CRUD() will guess a value or use "{item}".
183 |
184 | type Jobs struct{}
185 |
186 | // functions needed for Jobs to implement CRUD.
187 | func (l *Jobs) Create (ctx *Context) {}
188 | func (l *Jobs) Read (ctx *Context) {}
189 | func (l *Jobs) Update (ctx *Context) {}
190 | func (l *Jobs) Delete (ctx *Context) {}
191 |
192 | // CRUD() will add routes handled using "{uint:ticketid}" as PSE.
193 | jobs := &Jobs{}
194 | myservice.Resource(jobs).CRUD("{uint:ticketid}")
195 |
196 | The following routes are added:
197 |
198 | GET /api/jobs/{uint:ticketid} => use handler jobs.Read()
199 | POST /api/jobs => use handler jobs.Create()
200 | PUT /api/jobs => Status: 405 Method not allowed
201 | PUT /api/jobs/{uint:ticketid} => use handler jobs.Update()
202 | DELETE /api/jobs => Status: 405 Method not allowed
203 | DELETE /api/jobs/{uint:ticketid} => use handler jobs.Delete()
204 |
205 | Specific uses of PUT/PATCH/DELETE are dependent on the application, so CRUD()
206 | won't make any assumptions for those.
207 | */
208 | func (r *Resource) CRUD(pse string) *Resource {
209 | coll := r.collection.(CRUD)
210 |
211 | if pse == "" {
212 | // use resource collection name
213 | pse = "{" + strings.TrimRight(r.name, "s") + "}"
214 | if pse == "{}" {
215 | pse = "{item}" // give up
216 | }
217 | }
218 |
219 | r.Route("GET", pse, coll.Read)
220 | r.Route("POST", "", coll.Create)
221 | r.Route("PUT", "", r.MethodNotAllowed)
222 | r.Route("PUT", pse, coll.Update)
223 | r.Route("DELETE", "", r.MethodNotAllowed)
224 | r.Route("DELETE", pse, coll.Delete)
225 |
226 | r.NewLink(&Link{URI: r.Path(true) + "/" + pse, Rel: "item"})
227 |
228 | return r
229 | }
230 |
231 | /*
232 | Resource creates a new Resource object within a Service, and returns it.
233 | It will add an OPTIONS route that replies with an Allow header listing
234 | the methods available. Also, it will create a GET route to the handler in
235 | Resourcer.Index.
236 |
237 | collection is an object that implements the Resourcer interface.
238 |
239 | filters are resource-level filters that are ran before a resource handler, but
240 | after service-level filters.
241 |
242 | This function will panic if it can't determine the name of a collection
243 | through reflection.
244 | */
245 | func (svc *Service) Resource(collection Resourcer, filters ...Filter) *Resource {
246 | if collection == nil {
247 | panic("relax: Resource collection cannot be nil")
248 | }
249 |
250 | // check if the collection is the root resource
251 | cs := fmt.Sprintf("%T", collection)
252 | if cs == "*relax.Service" {
253 | return svc.Root()
254 | }
255 |
256 | // reflect name from object's type
257 | name := strings.ToLower(cs[strings.LastIndex(cs, ".")+1:])
258 | if name == "" {
259 | panic("relax: Resource naming failed: " + cs)
260 | }
261 |
262 | res := &Resource{
263 | service: svc,
264 | name: name,
265 | path: svc.Path(false) + name,
266 | collection: collection,
267 | links: make([]*Link, 0),
268 | filters: nil,
269 | }
270 |
271 | // user-specified filters
272 | if filters != nil {
273 | for i := range filters {
274 | if l, ok := filters[i].(LimitedFilter); ok && !l.RunIn(res) {
275 | svc.Logf("relax: Filter not usable for resource: %T", filters[i])
276 | continue
277 | }
278 | res.filters = append(res.filters, filters[i])
279 | }
280 | }
281 |
282 | // OPTIONS lists the methods allowed.
283 | res.Route("OPTIONS", "", res.OptionsHandler)
284 |
285 | // GET on the collection will access the Index handler
286 | res.Route("GET", "", collection.Index)
287 |
288 | // Relation: index -> resource.path
289 | res.NewLink(&Link{URI: res.Path(true), Rel: svc.Path(true) + "rel/" + name})
290 |
291 | // Relation: resource -> service
292 | res.NewLink(&Link{URI: res.Path(true), Rel: "collection"})
293 |
294 | // update service resources list
295 | svc.resources = append(svc.resources, res)
296 |
297 | return res
298 | }
299 |
--------------------------------------------------------------------------------
/response_buffer.go:
--------------------------------------------------------------------------------
1 | // Copyright 2014 Codehack http://codehack.com
2 | // Use of this source code is governed by a MIT-style
3 | // license that can be found in the LICENSE file.
4 |
5 | package relax
6 |
7 | import (
8 | "bytes"
9 | "io"
10 | "net/http"
11 | "sync"
12 | )
13 |
14 | /*
15 | ResponseBuffer implements http.ResponseWriter, but redirects all
16 | writes and headers to a buffer. This allows to inspect the response before
17 | sending it. When a response is buffered, it needs an explicit call to
18 | Flush or WriteTo to send it.
19 |
20 | ResponseBuffer also implements io.WriteTo to write data to any object that
21 | implements io.Writer.
22 | */
23 | type ResponseBuffer struct {
24 | bytes.Buffer
25 | wroteHeader bool
26 | status int
27 | header http.Header
28 | }
29 |
30 | // Header returns the buffered header map.
31 | func (rb *ResponseBuffer) Header() http.Header {
32 | return rb.header
33 | }
34 |
35 | // Write writes the data to the buffer.
36 | // Returns the number of bytes written or error on failure.
37 | func (rb *ResponseBuffer) Write(b []byte) (int, error) {
38 | return rb.Buffer.Write(b)
39 | }
40 |
41 | // WriteHeader stores the value of status code.
42 | func (rb *ResponseBuffer) WriteHeader(code int) {
43 | if rb.wroteHeader {
44 | return
45 | }
46 | rb.wroteHeader = true
47 | rb.status = code
48 | }
49 |
50 | // Status returns the last known status code saved. If no status has been set,
51 | // it returns http.StatusOK which is the default in ``net/http``.
52 | func (rb *ResponseBuffer) Status() int {
53 | if rb.wroteHeader {
54 | return rb.status
55 | }
56 | return http.StatusOK
57 | }
58 |
59 | // WriteTo implements io.WriterTo. It sends the buffer, except headers,
60 | // to any object that implements io.Writer. The buffer will be empty after
61 | // this call.
62 | // Returns the number of bytes written or error on failure.
63 | func (rb *ResponseBuffer) WriteTo(w io.Writer) (int64, error) {
64 | return rb.Buffer.WriteTo(w)
65 | }
66 |
67 | // FlushHeader sends the buffered headers and status, but not the content, to
68 | // 'w' an object that implements http.ResponseWriter.
69 | // This function won't free the buffer or reset the headers but it will send
70 | // the status using ResponseWriter.WriterHeader, if status was saved before.
71 | // See also: ResponseBuffer.Flush, ResponseBuffer.WriteHeader
72 | func (rb *ResponseBuffer) FlushHeader(w http.ResponseWriter) {
73 | for k, v := range rb.header {
74 | w.Header()[k] = v
75 | }
76 | if rb.wroteHeader {
77 | w.WriteHeader(rb.status)
78 | }
79 | }
80 |
81 | // Flush sends the headers, status and buffered content to 'w', an
82 | // http.ResponseWriter object. The ResponseBuffer object is freed after this call.
83 | // Returns the number of bytes written to 'w' or error on failure.
84 | // See also: ResponseBuffer.Free, ResponseBuffer.FlushHeader, ResponseBuffer.WriteTo
85 | func (rb *ResponseBuffer) Flush(w http.ResponseWriter) (int64, error) {
86 | defer rb.Free()
87 | rb.FlushHeader(w)
88 | return rb.WriteTo(w)
89 | }
90 |
91 | // responseBufferPool allows us to reuse some ResponseBuffer objects to
92 | // conserve system resources.
93 | var responseBufferPool = sync.Pool{
94 | New: func() interface{} { return new(ResponseBuffer) },
95 | }
96 |
97 | // NewResponseBuffer returns a ResponseBuffer object initialized with the headers
98 | // of 'w', an object that implements ``http.ResponseWriter``.
99 | // Objects returned using this function are pooled to save resources.
100 | // See also: ResponseBuffer.Free
101 | func NewResponseBuffer(w http.ResponseWriter) *ResponseBuffer {
102 | rb := responseBufferPool.Get().(*ResponseBuffer)
103 | rb.header = make(http.Header)
104 | for k, v := range w.Header() {
105 | rb.header[k] = v
106 | }
107 | return rb
108 | }
109 |
110 | // Free frees a ResponseBuffer object returning it back to the usage pool.
111 | // Use with ``defer`` after calling NewResponseBuffer if WriteTo or Flush
112 | // arent used. The values of the ResponseBuffer are reset and must be
113 | // re-initialized.
114 | func (rb *ResponseBuffer) Free() {
115 | rb.Buffer.Reset()
116 | rb.wroteHeader = false
117 | rb.status = 0
118 | rb.header = nil
119 | responseBufferPool.Put(rb)
120 | }
121 |
--------------------------------------------------------------------------------
/router.go:
--------------------------------------------------------------------------------
1 | // Copyright 2014 Codehack http://codehack.com
2 | // Use of this source code is governed by a MIT-style
3 | // license that can be found in the LICENSE file.
4 |
5 | package relax
6 |
7 | import (
8 | "fmt"
9 | "net/http"
10 | "net/url"
11 | "regexp"
12 | "strings"
13 | )
14 |
15 | /*
16 | Router defines the routing system. Objects that implement it have functions
17 | that add routes, find a handle to resources and provide information about routes.
18 |
19 | Relax's default router is trieRegexpRouter. It takes full routes, with HTTP method and path, and
20 | inserts them in a trie that can use regular expressions to match individual path segments.
21 |
22 | PSE: trieRegexpRouter's path segment expressions (PSE) are match strings that are pre-compiled as
23 | regular expressions. PSE's provide a simple layer of security when accepting values from
24 | the path. Each PSE is made out of a {type:varname} format, where type is the expected type
25 | for a value and varname is the name to give the variable that matches the value.
26 |
27 | "{word:varname}" // matches any word; alphanumeric and underscore.
28 |
29 | "{uint:varname}" // matches an unsigned integer.
30 |
31 | "{int:varname}" // matches a signed integer.
32 |
33 | "{float:varname}" // matches a floating-point number in decimal notation.
34 |
35 | "{date:varname}" // matches a date in ISO 8601 format.
36 |
37 | "{geo:varname}" // matches a geo location as described in RFC 5870
38 |
39 | "{hex:varname}" // matches a hex number, with optional "0x" prefix.
40 |
41 | "{uuid:varname}" // matches an UUID.
42 |
43 | "{varname}" // catch-all; matches anything. it may overlap other matches.
44 |
45 | "*" // translated into "{wild}"
46 |
47 | "{re:pattern}" // custom regexp pattern.
48 |
49 | Some sample routes supported by trieRegexpRouter:
50 |
51 | GET /api/users/@{word:name}
52 |
53 | GET /api/users/{uint:id}/*
54 |
55 | POST /api/users/{uint:id}/profile
56 |
57 | DELETE /api/users/{date:from}/to/{date:to}
58 |
59 | GET /api/cities/{geo:location}
60 |
61 | PUT /api/investments/\${float:dollars}/fund
62 |
63 | GET /api/todos/month/{re:([0][1-9]|[1][0-2])}
64 |
65 | Since PSE's are compiled to regexp, care must be taken to escape characters that
66 | might break the compilation.
67 | */
68 | type Router interface {
69 | // FindHandler should match request parameters to an existing resource handler and
70 | // return it. If no match is found, it should return an StatusError error which will
71 | // be sent to the requester. The default errors ErrRouteNotFound and
72 | // ErrRouteBadMethod cover the default cases.
73 | FindHandler(string, string, *url.Values) (HandlerFunc, error)
74 |
75 | // AddRoute is used to create new routes to resources. It expects the HTTP method
76 | // (GET, POST, ...) followed by the resource path and the handler function.
77 | AddRoute(string, string, HandlerFunc)
78 |
79 | // PathMethods returns a comma-separated list of HTTP methods that are matched
80 | // to a path. It will do PSE expansion.
81 | PathMethods(string) string
82 | }
83 |
84 | // These are errors returned by the default routing engine. You are encouraged to
85 | // reuse them with your own Router.
86 | var (
87 | // ErrRouteNotFound is returned when the path searched didn't reach a resource handler.
88 | ErrRouteNotFound = &StatusError{http.StatusNotFound, "That route was not found.", nil}
89 |
90 | // ErrRouteBadMethod is returned when the path did not match a given HTTP method.
91 | ErrRouteBadMethod = &StatusError{http.StatusMethodNotAllowed, "That method is not supported", nil}
92 | )
93 |
94 | // pathRegexpCache is a cache of all compiled regexp's so they can be reused.
95 | var pathRegexpCache = make(map[string]*regexp.Regexp)
96 |
97 | // trieRegexpRouter implements Router with a trie that can store regular expressions.
98 | // root points to the top of the tree from which all routes are searched and matched.
99 | // methods is a list of all the methods used in routes.
100 | type trieRegexpRouter struct {
101 | root *trieNode
102 | methods []string
103 | }
104 |
105 | // trieNode contains the routing information.
106 | // handler, if not nil, points to the resource handler served by a specific route.
107 | // numExp is non-zero if the current path segment has regexp links.
108 | // depth is the path depth of the current segment; 0 == HTTP verb.
109 | // links are the contiguous path segments.
110 | //
111 | // For example, given the following route and handler:
112 | // "GET /api/users/111" -> users.GetUser()
113 | // - the path segment links are ["GET", "api", "users", "111"]
114 | // - "GET" has depth=0 and "111" has depth=3
115 | // - suppose "111" might be matched via regexp, then "users".numExp > 0
116 | // - "111" segment will point to the handler users.GetUser()
117 | type trieNode struct {
118 | pseg string
119 | handler HandlerFunc
120 | numExp int
121 | depth int
122 | links []*trieNode
123 | }
124 |
125 | func (n *trieNode) findLink(pseg string) *trieNode {
126 | for i := range n.links {
127 | if n.links[i].pseg == pseg {
128 | return n.links[i]
129 | }
130 | }
131 | return nil
132 | }
133 |
134 | // segmentExp compiles the pattern string into a regexp so it can used in a
135 | // path segment match. This function will panic if the regexp compilation fails.
136 | func segmentExp(pattern string) *regexp.Regexp {
137 | // custom regexp pattern.
138 | if strings.HasPrefix(pattern, "{re:") {
139 | return regexp.MustCompile(pattern[4 : len(pattern)-1])
140 | }
141 |
142 | // turn "*" => "{wild}"
143 | pattern = strings.Replace(pattern, "*", `{wild}`, -1)
144 | // any: catch-all pattern
145 | p := regexp.MustCompile(`\{\w+\}`).
146 | ReplaceAllStringFunc(pattern, func(m string) string {
147 | return fmt.Sprintf(`(?P<%s>.+)`, m[1:len(m)-1])
148 | })
149 | // word: matches an alphanumeric word, with underscores.
150 | p = regexp.MustCompile(`\{(?:word\:)\w+\}`).
151 | ReplaceAllStringFunc(p, func(m string) string {
152 | return fmt.Sprintf(`(?P<%s>\w+)`, m[6:len(m)-1])
153 | })
154 | // date: matches a date as described in ISO 8601. see: https://en.wikipedia.org/wiki/ISO_8601
155 | // accepted values:
156 | // YYYY
157 | // YYYY-MM
158 | // YYYY-MM-DD
159 | // YYYY-MM-DDTHH
160 | // YYYY-MM-DDTHH:MM
161 | // YYYY-MM-DDTHH:MM:SS[.NN]
162 | // YYYY-MM-DDTHH:MM:SS[.NN]Z
163 | // YYYY-MM-DDTHH:MM:SS[.NN][+-]HH
164 | // YYYY-MM-DDTHH:MM:SS[.NN][+-]HH:MM
165 | //
166 | p = regexp.MustCompile(`\{(?:date\:)\w+\}`).
167 | ReplaceAllStringFunc(p, func(m string) string {
168 | name := m[6 : len(m)-1]
169 | return fmt.Sprintf(`(?P<%[1]s>(`+
170 | `(?P<%[1]s_year>\d{4})([/-]?`+
171 | `(?P<%[1]s_mon>(0[1-9])|(1[012]))([/-]?`+
172 | `(?P<%[1]s_mday>(0[1-9])|([12]\d)|(3[01])))?)?`+
173 | `(?:T(?P<%[1]s_hour>([01][0-9])|(?:2[0123]))(\:?(?P<%[1]s_min>[0-5][0-9])(\:?(?P<%[1]s_sec>[0-5][0-9]([\,\.]\d{1,10})?))?)?(?:Z|([\-+](?:([01][0-9])|(?:2[0123]))(\:?(?:[0-5][0-9]))?))?)?`+
174 | `))`, name)
175 | })
176 | // geo: geo location in decimal. See http://tools.ietf.org/html/rfc5870
177 | // accepted values:
178 | // lat,lon (point)
179 | // lat,lon,alt (3d point)
180 | // lag,lon;u=unc (circle)
181 | // lat,lon,alt;u=unc (sphere)
182 | // lat,lon;crs=name (point with coordinate reference system (CRS) value)
183 | p = regexp.MustCompile(`\{(?:geo\:)\w+\}`).ReplaceAllStringFunc(p, func(m string) string {
184 | name := m[5 : len(m)-1]
185 | return fmt.Sprintf(`(?P<%[1]s_lat>\-?\d+(\.\d+)?)[,;]`+
186 | `(?P<%[1]s_lon>\-?\d+(\.\d+)?)([,;]`+
187 | `(?P<%[1]s_alt>\-?\d+(\.\d+)?))?(((?:;crs=)`+
188 | `(?P<%[1]s_crs>[\w\-]+))?((?:;u=)`+
189 | `(?P<%[1]s_u>\-?\d+(\.\d+)?))?)?`, name)
190 | })
191 | // hex: matches a hexadecimal number.
192 | // accepted value: 0xNN
193 | p = regexp.MustCompile(`\{(?:hex\:)\w+\}`).
194 | ReplaceAllStringFunc(p, func(m string) string {
195 | return fmt.Sprintf(`(?P<%s>(?:0x)?[[:xdigit:]]+)`, m[5:len(m)-1])
196 | })
197 | // uuid: matches an UUID using hex octets, with optional dashes.
198 | // accepted value: NNNNNNNN-NNNN-NNNN-NNNN-NNNNNNNNNNNN
199 | p = regexp.MustCompile(`\{(?:uuid\:)\w+\}`).
200 | ReplaceAllStringFunc(p, func(m string) string {
201 | return fmt.Sprintf(`(?P<%s>[[:xdigit:]]{8}\-?`+
202 | `[[:xdigit:]]{4}\-?`+
203 | `[[:xdigit:]]{4}\-?`+
204 | `[[:xdigit:]]{4}\-?`+
205 | `[[:xdigit:]]{12})`, m[6:len(m)-1])
206 | })
207 | // float: matches a floating-point number
208 | p = regexp.MustCompile(`\{(?:float\:)\w+\}`).
209 | ReplaceAllStringFunc(p, func(m string) string {
210 | return fmt.Sprintf(`(?P<%s>[\-+]?\d+\.\d+)`, m[7:len(m)-1])
211 | })
212 | // uint: matches an unsigned integer number (64bit)
213 | p = regexp.MustCompile(`\{(?:uint\:)\w+\}`).
214 | ReplaceAllStringFunc(p, func(m string) string {
215 | return fmt.Sprintf(`(?P<%s>\d{1,18})`, m[6:len(m)-1])
216 | })
217 | // int: matches a signed integer number (64bit)
218 | p = regexp.MustCompile(`\{(?:int\:)\w+\}`).
219 | ReplaceAllStringFunc(p, func(m string) string {
220 | return fmt.Sprintf(`(?P<%s>[-+]?\d{1,18})`, m[5:len(m)-1])
221 | })
222 | return regexp.MustCompile(p)
223 | }
224 |
225 | // AddRoute breaks a path into segments and inserts them in the tree. If a
226 | // segment contains matching {}'s then it is tried as a regexp segment, otherwise it is
227 | // treated as a regular string segment.
228 | func (r *trieRegexpRouter) AddRoute(method, path string, handler HandlerFunc) {
229 | node := r.root
230 | pseg := strings.Split(method+strings.TrimRight(path, "/"), "/")
231 | for i := range pseg {
232 | if (strings.Contains(pseg[i], "{") && strings.Contains(pseg[i], "}")) || strings.Contains(pseg[i], "*") {
233 | if _, ok := pathRegexpCache[pseg[i]]; !ok {
234 | pathRegexpCache[pseg[i]] = segmentExp(pseg[i])
235 | }
236 | node.numExp++
237 | }
238 | link := node.findLink(pseg[i])
239 | if link == nil {
240 | link = &trieNode{
241 | pseg: pseg[i],
242 | depth: node.depth + 1,
243 | }
244 | node.links = append(node.links, link)
245 | }
246 | node = link
247 | }
248 |
249 | node.handler = handler
250 |
251 | // update methods list
252 | if !strings.Contains(strings.Join(r.methods, ","), method) {
253 | r.methods = append(r.methods, method)
254 | }
255 | }
256 |
257 | // matchSegment tries to match a path segment 'pseg' to the node's regexp links.
258 | // This function will return any path values matched so they can be used in
259 | // Request.PathValues.
260 | func (n *trieNode) matchSegment(pseg string, depth int, values *url.Values) *trieNode {
261 | if n.numExp == 0 {
262 | return n.findLink(pseg)
263 | }
264 | for pexp := range n.links {
265 | rx := pathRegexpCache[n.links[pexp].pseg]
266 | if rx == nil {
267 | continue
268 | }
269 | // this prevents the matching to be side-tracked by smaller paths.
270 | if depth > n.links[pexp].depth && n.links[pexp].links == nil {
271 | continue
272 | }
273 | m := rx.FindStringSubmatch(pseg)
274 | if len(m) > 1 && m[0] == pseg {
275 | if values != nil {
276 | if *values == nil {
277 | *values = make(url.Values)
278 | }
279 | sub := rx.SubexpNames()
280 | for i, n := 1, len(*values)/2; i < len(m); i++ {
281 | _n := fmt.Sprintf("_%d", n+i)
282 | (*values).Set(_n, m[i])
283 | if sub[i] != "" {
284 | (*values).Add(sub[i], m[i])
285 | }
286 | }
287 | }
288 | return n.links[pexp]
289 | }
290 | }
291 | return n.findLink(pseg)
292 | }
293 |
294 | // FindHandler returns a resource handler that matches the requested route; or
295 | // an error (StatusError) if none found.
296 | // method is the HTTP verb.
297 | // path is the relative URI path.
298 | // values is a pointer to an url.Values map to store parameters from the path.
299 | func (r *trieRegexpRouter) FindHandler(method, path string, values *url.Values) (HandlerFunc, error) {
300 | if method == "HEAD" {
301 | method = "GET"
302 | }
303 | node := r.root
304 | pseg := strings.Split(method+strings.TrimRight(path, "/"), "/") // ex: GET/api/users
305 | slen := len(pseg)
306 | for i := range make([]struct{}, slen) {
307 | if node == nil {
308 | if i <= 1 {
309 | return nil, ErrRouteBadMethod
310 | }
311 | return nil, ErrRouteNotFound
312 | }
313 | node = node.matchSegment(pseg[i], slen, values)
314 | }
315 |
316 | if node == nil || node.handler == nil {
317 | return nil, ErrRouteNotFound
318 | }
319 | return node.handler, nil
320 | }
321 |
322 | // PathMethods returns a string with comma-separated HTTP methods that match
323 | // the path. This list is suitable for Allow header response. Note that this
324 | // function only lists the methods, not if they are allowed.
325 | func (r *trieRegexpRouter) PathMethods(path string) string {
326 | var node *trieNode
327 | methods := "HEAD" // cheat
328 | pseg := strings.Split("*"+strings.TrimRight(path, "/"), "/")
329 | slen := len(pseg)
330 | for _, method := range r.methods {
331 | node = r.root
332 | pseg[0] = method
333 | for i := range pseg {
334 | if node == nil {
335 | continue
336 | }
337 | node = node.matchSegment(pseg[i], slen, nil)
338 | }
339 | if node == nil || node.handler == nil {
340 | continue
341 | }
342 | methods += ", " + method
343 | }
344 | return methods
345 | }
346 |
347 | // newRouter returns a new trieRegexpRouter object with an initialized tree.
348 | func newRouter() *trieRegexpRouter {
349 | return &trieRegexpRouter{root: new(trieNode)}
350 | }
351 |
--------------------------------------------------------------------------------
/router_test.go:
--------------------------------------------------------------------------------
1 | // Copyright 2014 Codehack http://codehack.com
2 | // Use of this source code is governed by a MIT-style
3 | // license that can be found in the LICENSE file.
4 |
5 | package relax
6 |
7 | import (
8 | "net/url"
9 | "testing"
10 | )
11 |
12 | var testRouter = newRouter()
13 |
14 | var testRoutes = []struct {
15 | Method string
16 | Path string
17 | }{
18 | {"GET", "/posts"},
19 | {"GET", "/posts/{uint:id}"},
20 | {"GET", "/posts/{uint:id}/links"},
21 | {"GET", "/posts/{word:tag}"},
22 | {"GET", "/posts/{word:tag}/{uint:uid}"},
23 | }
24 |
25 | func testHandler(ctx *Context) {}
26 |
27 | var testRequests = []struct {
28 | Method string
29 | Path string
30 | Must bool
31 | }{
32 | {"GET", "/posts", true},
33 | {"GET", "/posts/123", true},
34 | {"GET", "/posts/444/links", true},
35 | {"GET", "/posts/something", true},
36 | {"GET", "/posts/tagged/666", true},
37 | }
38 |
39 | func TestFindHandler(t *testing.T) {
40 | for i := range testRoutes {
41 | testRouter.AddRoute(testRoutes[i].Method, testRoutes[i].Path, testHandler)
42 | }
43 |
44 | for i := range testRequests {
45 | var v url.Values
46 | _, err := testRouter.FindHandler(testRequests[i].Method, testRequests[i].Path, &v)
47 | if testRequests[i].Must && err != nil {
48 | t.Error(testRequests[i].Method, testRequests[i].Path, err.Error())
49 | }
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/service.go:
--------------------------------------------------------------------------------
1 | // Copyright 2014 Codehack http://codehack.com
2 | // Use of this source code is governed by a MIT-style
3 | // license that can be found in the LICENSE file.
4 |
5 | package relax
6 |
7 | import (
8 | "log"
9 | "net/http"
10 | "net/url"
11 | "strings"
12 | "time"
13 |
14 | "context"
15 | )
16 |
17 | // serverVersion is used with the Server HTTP header.
18 | const serverVersion = "Go-Relax/" + Version
19 |
20 | // Logger interface is based on Go's ``log`` package. Objects that implement
21 | // this interface can provide logging to Relax resources.
22 | type Logger interface {
23 | Print(...interface{})
24 | Printf(string, ...interface{})
25 | Println(...interface{})
26 | }
27 |
28 | // Service contains all the information about the service and resources handled.
29 | // Specifically, the routing, encoding and service filters.
30 | // Additionally, a Service is a collection of resources making it a resource by itself.
31 | // Therefore, it implements the Resourcer interface. See: ``Service.Root``
32 | type Service struct {
33 | // URI is the full reference URI to the service.
34 | URI *url.URL
35 | // router is the routing engine
36 | router Router
37 | // encoders contains a list of our service media encoders.
38 | // Format: {mediatype}:{encoder object}. e.g., encoders["application/json"].
39 | encoders map[string]Encoder
40 | // filters are the service-level filters; which are run for all incoming requests.
41 | filters []Filter
42 | // resources is a list of all mapped resources
43 | resources []*Resource
44 | // uptime is a timestamp when service was started
45 | uptime time.Time
46 | // logger is the service logging system.
47 | logger Logger
48 | // Recovery is a handler function used to intervene after panic occur.
49 | Recovery http.HandlerFunc
50 | }
51 |
52 | // Logf prints an log entry to logger if set, or stdlog if nil.
53 | // Based on the unexported function logf() in ``net/http``.
54 | func (svc *Service) Logf(format string, args ...interface{}) {
55 | if svc.logger == nil {
56 | log.Printf(format, args...)
57 | return
58 | }
59 | svc.logger.Printf(format, args...)
60 | }
61 |
62 | // Index is a handler that responds with a list of all resources managed
63 | // by the service. This is the default route to the base URI.
64 | // With this function Service implements the Resourcer interface which is
65 | // a resource of itself (the "root" resource).
66 | // FIXME: this pukes under XML (maps of course).
67 | func (svc *Service) Index(ctx *Context) {
68 | resources := make(map[string]string)
69 | for _, r := range svc.resources {
70 | resources[r.name] = r.Path(true)
71 | for _, l := range r.links {
72 | if l.Rel == "collection" {
73 | ctx.Header().Add("Link", l.String())
74 | }
75 | }
76 | }
77 | ctx.Respond(resources)
78 | }
79 |
80 | // BUG(TODO): Complete PATCH support - http://tools.ietf.org/html/rfc5789, http://tools.ietf.org/html/rfc6902
81 |
82 | // Options implements the Optioner interface to handle OPTION requests for the root
83 | // resource service.
84 | func (svc *Service) Options(ctx *Context) {
85 | options := map[string]string{
86 | "base_href": svc.URI.String(),
87 | "mediatype_template": Content.Mediatype + "+{subtype}; version={version}; lang={language}",
88 | "version_default": Content.Version,
89 | "language_default": Content.Language,
90 | "encoding_default": svc.encoders["application/json"].Accept(),
91 | }
92 | ctx.Respond(options)
93 | }
94 |
95 | // InternalServerError responds with HTTP status code 500-"Internal Server Error".
96 | // This function is the default service recovery handler.
97 | func InternalServerError(w http.ResponseWriter, r *http.Request) {
98 | http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
99 | }
100 |
101 | // dispatch tries to connect the request to a resource handler. If it can't find
102 | // an appropriate handler it will return an HTTP error response.
103 | func (svc *Service) dispatch(ctx *Context) {
104 | handler, err := svc.router.FindHandler(ctx.Request.Method, ctx.Request.URL.Path, &ctx.PathValues)
105 | if err != nil {
106 | ctx.Header().Set("Cache-Control", "max-age=300, stale-if-error=600")
107 | if err == ErrRouteBadMethod { // 405-Method Not Allowed
108 | ctx.Header().Set("Allow", svc.router.PathMethods(ctx.Request.URL.Path))
109 | }
110 | ctx.Error(err.(*StatusError).Code, err.Error(), err.(*StatusError).Details)
111 | return
112 | }
113 | handler(ctx)
114 | }
115 |
116 | /*
117 | Adapter creates a new request context, sets default HTTP headers, creates the
118 | link-chain of service filters, then passes the request to content negotiation.
119 | Also, it uses a recovery function for panics, that responds with HTTP status
120 | 500-"Internal Server Error" and logs the event.
121 |
122 | Info passed down by the adapter:
123 |
124 | ctx.Get("request.start_time").(time.Time) // Time when request started, as string time.Time.
125 | ctx.Get("request.id").(string) // Unique or user-supplied request ID.
126 |
127 | Returns an http.HandlerFunc function that can be used with http.Handle.
128 | */
129 | func (svc *Service) Adapter() http.HandlerFunc {
130 | handler := svc.dispatch
131 | for i := len(svc.filters) - 1; i >= 0; i-- {
132 | handler = svc.filters[i].Run(handler)
133 | }
134 | handler = svc.content(handler)
135 |
136 | // parent context
137 | parent := context.Background()
138 |
139 | return func(w http.ResponseWriter, r *http.Request) {
140 | defer func() {
141 | if err := recover(); err != nil {
142 | svc.Recovery(w, r)
143 | svc.Logf("relax: Panic recovery: %s", err)
144 | }
145 | }()
146 |
147 | ctx := newContext(parent, w, r)
148 | defer ctx.free()
149 |
150 | requestID := NewRequestID(r.Header.Get("Request-Id"))
151 |
152 | ctx.Set("request.start_time", time.Now())
153 | ctx.Set("request.id", requestID)
154 |
155 | // set our default headers
156 | ctx.Header().Set("Server", serverVersion)
157 | ctx.Header().Set("Request-Id", requestID)
158 |
159 | handler(ctx)
160 | }
161 | }
162 |
163 | /*
164 | Handler is a function that returns the values needed by http.Handle
165 | to handle a path. This allows Relax services to work along http.ServeMux.
166 | It returns the path of the service and the Service.Adapter handler.
167 |
168 | // restrict requests to host "api.codehack.com"
169 | myAPI := relax.NewService("http://api.codehack.com/v1")
170 |
171 | // ... your resources might go here ...
172 |
173 | // maps "api.codehack.com/v1" in http.ServeMux
174 | http.Handle(myAPI.Handler())
175 |
176 | // map other resources independently
177 | http.Handle("/docs", DocsHandler)
178 | http.Handle("/help", HelpHandler)
179 | http.Handle("/blog", BlogHandler)
180 |
181 | log.Fatal(http.ListenAndServe(":8000", nil))
182 |
183 | Using this function with http.Handle is _recommended_ over using Service.Adapter
184 | directly. You benefit from the security options built-in to http.ServeMux; like
185 | restricting to specific hosts, clean paths, and separate path matching.
186 | */
187 | func (svc *Service) Handler() (string, http.Handler) {
188 | if svc.URI.Host != "" {
189 | svc.Logf("relax: Matching requests to host %q", svc.URI.Host)
190 | }
191 | return svc.URI.Host + svc.URI.Path, svc.Adapter()
192 | }
193 |
194 | /*
195 | ServeHTTP implements http.HandlerFunc. It lets the Service route all requests
196 | directly, bypassing http.ServeMux.
197 |
198 | myService := relax.NewService("/")
199 | // ... your resources might go here ...
200 |
201 | // your service has complete handling of all the routes.
202 | log.Fatal(http.ListenAndServe(":8000", myService))
203 |
204 | Using Service.Handler has more benefits than this method.
205 | */
206 | func (svc *Service) ServeHTTP(w http.ResponseWriter, r *http.Request) {
207 | svc.Adapter().ServeHTTP(w, r)
208 | }
209 |
210 | /*
211 | Use adds one or more encoders, filters and/or router to the service.
212 | Returns the service itself, for chaining.
213 |
214 | To add new filters, assign an object that implements the Filter interface.
215 | Filters are not replaced or updated, only appended to the service list.
216 | Examples:
217 |
218 | myservice.Use(&cors.Filter{})
219 | myservice.Use(&security.Filter{CacheDisable: true})
220 |
221 | To add encoders, assign an object that implements the Encoder interface.
222 | Encoders will replace any matching existing encoder(s), and they will
223 | be discoverable on the service encoders map.
224 |
225 | newenc := NewEncoderXML() // encoder with default settings
226 | newenc.Indented = true // change a setting
227 | myservice.Use(newenc) // assign it to service
228 |
229 | To change the routing engine, assign an object that implements the
230 | Router interface:
231 |
232 | myservice.Use(MyFastRouter())
233 |
234 | To change the logging system, assign an object that implements the Logger
235 | interface:
236 |
237 | // Use the excellent logrus package.
238 | myservice.Use(logrus.New())
239 |
240 | // With advanced usage
241 | log := &logrus.Logger{
242 | Out: os.Stderr,
243 | Formatter: new(JSONFormatter),
244 | Level: logrus.Debug,
245 | }
246 | myservice.Use(log)
247 |
248 | Any entities that don't implement the required interfaces, will be ignored.
249 | */
250 | func (svc *Service) Use(entities ...interface{}) *Service {
251 | for _, e := range entities {
252 | switch entity := e.(type) {
253 | case LimitedFilter:
254 | if !e.(LimitedFilter).RunIn(svc) {
255 | svc.Logf("relax: Filter not usable for service: %T", entity)
256 | }
257 | case Encoder:
258 | svc.encoders[entity.Accept()] = entity
259 | case Filter:
260 | svc.filters = append(svc.filters, entity)
261 | case Router:
262 | svc.router = entity
263 | case Logger:
264 | svc.logger = entity
265 | default:
266 | svc.Logf("relax: Unknown entity to use: %T", entity)
267 | }
268 | }
269 | return svc
270 | }
271 |
272 | /*
273 | Router returns the service routing engine.
274 |
275 | The routing engine is responsible for creating routes (method + path)
276 | to service resources, and accessing them for each request.
277 | To add new routes you can use this interface directly:
278 |
279 | myservice.Router().AddRoute(method, path, handler)
280 |
281 | Any route added directly with AddRoute() must reside under the service
282 | URI base path, otherwise it won't work. No checks are made.
283 | To find a handler to a request:
284 |
285 | h := myservice.Router().FindHandler(ctx)
286 |
287 | This will return the handler for the route in request context 'ctx'.
288 | */
289 | func (svc *Service) Router() Router {
290 | return svc.router
291 | }
292 |
293 | // Logger returns the service logging system.
294 | func (svc *Service) Logger() Logger {
295 | return svc.logger
296 | }
297 |
298 | // Uptime returns the service uptime in seconds.
299 | func (svc *Service) Uptime() int {
300 | return int(time.Since(svc.uptime) / time.Second)
301 | }
302 |
303 | // Path returns the base path of this service.
304 | // absolute whether or not it should return an absolute URL.
305 | func (svc *Service) Path(absolute bool) string {
306 | path := svc.URI.Path
307 | if absolute {
308 | path = svc.URI.String()
309 | }
310 | return path
311 | }
312 |
313 | // Root points to the root resource, the service itself -- a collection of resources.
314 | // This allows us to manipulate the service as a resource.
315 | //
316 | // Example:
317 | //
318 | // // Create a new service mapped to "/v2"
319 | // svc := relax.NewService("/v2")
320 | //
321 | // // Route /v2/status/{level} to SystemStatus() via root
322 | // svc.Root().GET("status/{word:level}", SystemStatus, &etag.Filter{})
323 | //
324 | // This is similar to:
325 | //
326 | // svc.AddRoute("GET", "/v2/status/{level}", SystemStatus)
327 | //
328 | // Except that route-level filters can be used, without needing to meddle with
329 | // service filters (which are global).
330 | //
331 | func (svc *Service) Root() *Resource {
332 | return svc.resources[0]
333 | }
334 |
335 | /*
336 | Run will start the service using basic defaults or using arguments
337 | supplied. If 'args' is nil, it will start the service on port 8000.
338 | If 'args' is not nil, it expects in order: address (host:port),
339 | certificate file and key file for TLS.
340 |
341 | Run() is equivalent to:
342 | http.Handle(svc.Handler())
343 | http.ListenAndServe(":8000", nil)
344 |
345 | Run(":3000") is equivalent to:
346 | ...
347 | http.ListenAndServe(":3000", nil)
348 |
349 | Run("10.1.1.100:10443", "tls/cert.pem", "tls/key.pem") is eq. to:
350 | ...
351 | http.ListenAndServeTLS("10.1.1.100:10443", "tls/cert.pem", "tls/key.pem", nil)
352 |
353 | If the key file is missing, TLS is not used.
354 |
355 | */
356 | func (svc *Service) Run(args ...string) {
357 | var err error
358 |
359 | addr := ":8000"
360 | if args != nil {
361 | addr = args[0]
362 | }
363 |
364 | http.Handle(svc.Handler())
365 |
366 | if len(args) == 3 {
367 | svc.Logf("relax: Listening on %q (TLS)", addr)
368 | err = http.ListenAndServeTLS(addr, args[1], args[2], nil)
369 | } else {
370 | svc.Logf("relax: Listening on %q", addr)
371 | err = http.ListenAndServe(addr, nil)
372 | }
373 |
374 | if err != nil {
375 | log.Fatal(err)
376 | }
377 | }
378 |
379 | /*
380 | NewService returns a new Service that can serve resources.
381 |
382 | 'uri' is the URI to this service, it should be an absolute URI but not required.
383 | If an existing path is specified, the last path is used. 'entities' is an
384 | optional value that contains a list of Filter, Encoder, Router objects that
385 | are assigned at the service-level; the same as Service.Use().
386 |
387 | myservice := NewService("https://api.codehack.com/v1", &eTag.Filter{})
388 |
389 | This function will panic if it can't parse 'uri'.
390 | */
391 | func NewService(uri string, entities ...interface{}) *Service {
392 | u, err := url.Parse(uri)
393 | if err != nil {
394 | log.Panicln("relax: Service URI parsing failed:", err.Error())
395 | }
396 |
397 | // the service path must end (and begin) with "/", this way ServeMux can
398 | // make a redirect for the non-absolute path.
399 | if u.Path == "" || u.Path[len(u.Path)-1] != '/' {
400 | u.Path += "/"
401 | }
402 | u.User = nil
403 | u.RawQuery = ""
404 | u.Fragment = ""
405 |
406 | svc := &Service{
407 | URI: u,
408 | router: newRouter(),
409 | encoders: make(map[string]Encoder),
410 | filters: make([]Filter, 0),
411 | resources: make([]*Resource, 0),
412 | uptime: time.Now(),
413 | Recovery: InternalServerError,
414 | }
415 |
416 | // Make JSON the default encoder
417 | svc.Use(NewEncoder())
418 | // svc.encoders["application/json"] = NewEncoder()
419 |
420 | // Assign initial service entities
421 | if entities != nil {
422 | svc.Use(entities...)
423 | }
424 |
425 | // Setup the root resource
426 | root := &Resource{
427 | service: svc,
428 | name: "_root",
429 | path: strings.TrimSuffix(u.Path, "/"),
430 | collection: svc,
431 | }
432 |
433 | // Default service routes
434 | root.Route("GET", "", svc.Index)
435 | root.Route("OPTIONS", "", root.OptionsHandler)
436 |
437 | svc.resources = append(svc.resources, root)
438 |
439 | log.Printf("relax: New service %q", u.String())
440 |
441 | return svc
442 | }
443 |
--------------------------------------------------------------------------------
/util.go:
--------------------------------------------------------------------------------
1 | // Copyright 2014 Codehack http://codehack.com
2 | // Use of this source code is governed by a MIT-style
3 | // license that can be found in the LICENSE file.
4 |
5 | package relax
6 |
7 | import (
8 | "net/http"
9 | "strconv"
10 | "strings"
11 |
12 | "github.com/gofrs/uuid"
13 | )
14 |
15 | // These status codes are inaccessible in net/http but they work with http.StatusText().
16 | // They are included here as they might be useful.
17 | // See: https://tools.ietf.org/html/rfc6585
18 | const (
19 | // StatusUnprocessableEntity indicates the user sent content that while it is
20 | // syntactically correct, it might be erroneous.
21 | // See: http://tools.ietf.org/html/rfc4918#section-11.2
22 | StatusUnprocessableEntity = 422
23 | // StatusPreconditionRequired indicates that the origin server requires the
24 | // request to be conditional.
25 | StatusPreconditionRequired = 428
26 | // StatusTooManyRequests indicates that the user has sent too many requests
27 | // in a given amount of time ("rate limiting").
28 | StatusTooManyRequests = 429
29 | // StatusRequestHeaderFieldsTooLarge indicates that the server is unwilling to
30 | // process the request because its header fields are too large.
31 | StatusRequestHeaderFieldsTooLarge = 431
32 | // StatusNetworkAuthenticationRequired indicates that the client needs to
33 | // authenticate to gain network access.
34 | StatusNetworkAuthenticationRequired = 511
35 | )
36 |
37 | // NewRequestID returns a new request ID value based on UUID; or checks
38 | // an id specified if it's valid for use as a request ID. If the id is not
39 | // valid then it returns a new ID.
40 | //
41 | // A valid ID must be between 20 and 200 chars in length, and URL-encoded.
42 | func NewRequestID(id string) string {
43 | if id == "" {
44 | return uuid.Must(uuid.NewV4()).String()
45 | }
46 | l := 0
47 | for i, c := range id {
48 | switch {
49 | case 'A' <= c && c <= 'Z':
50 | case 'a' <= c && c <= 'z':
51 | case '0' <= c && c <= '9':
52 | case c == '-', c == '_', c == '.', c == '~', c == '%', c == '+':
53 | case i > 199:
54 | fallthrough
55 | default:
56 | return uuid.Must(uuid.NewV4()).String()
57 | }
58 | l = i
59 | }
60 | if l < 20 {
61 | return uuid.Must(uuid.NewV4()).String()
62 | }
63 | return id
64 | }
65 |
66 | /*
67 | PathExt returns the media subtype extension in an URL path.
68 | The extension begins from the last dot:
69 |
70 | /api/v1/tickets.xml => ".xml"
71 |
72 | Returns the extension with dot, or empty string "" if not found.
73 | */
74 | func PathExt(path string) string {
75 | dot := strings.LastIndex(path, ".")
76 | if dot > -1 {
77 | return path[dot:]
78 | }
79 | return ""
80 | }
81 |
82 | // ParsePreferences is a very naive and simple parser for header value preferences.
83 | // Returns a map of preference=quality values for each preference with a quality value.
84 | // If a preference doesn't specify quality, then a value of 1.0 is assumed (bad!).
85 | // If the quality float value can't be parsed from string, an error is returned.
86 | func ParsePreferences(values string) (map[string]float32, error) {
87 | prefs := make(map[string]float32)
88 | for _, rawval := range strings.Split(values, ",") {
89 | val := strings.SplitN(strings.TrimSpace(rawval), ";q=", 2)
90 | prefs[val[0]] = 1.0
91 | if len(val) == 2 {
92 | f, err := strconv.ParseFloat(val[1], 32)
93 | if err != nil {
94 | return nil, err
95 | }
96 | prefs[val[0]] = float32(f)
97 | }
98 | }
99 | return prefs, nil
100 | }
101 |
102 | // IsRequestSSL returns true if the request 'r' is done via SSL/TLS.
103 | // SSL status is guessed from value of Request.TLS. It also checks the value
104 | // of the X-Forwarded-Proto header, in case the request is proxied.
105 | // Returns true if the request is via SSL, false otherwise.
106 | func IsRequestSSL(r *http.Request) bool {
107 | return (r.TLS != nil || r.URL.Scheme == "https" || r.Header.Get("X-Forwarded-Proto") == "https")
108 | }
109 |
110 | // GetRealIP returns the client address if the request is proxied. This is
111 | // a best-guess based on the headers sent. The function will check the following
112 | // headers, in order, to find a proxied client: Forwarded, X-Forwarded-For and
113 | // X-Real-IP.
114 | // Returns the client address or "unknown".
115 | func GetRealIP(r *http.Request) string {
116 | // check if the IP address is hidden behind a proxy request.
117 | // See http://tools.ietf.org/html/rfc7239
118 | if v := r.Header.Get("Forwarded"); v != "" {
119 | values := strings.Split(v, ",")
120 | if strings.HasPrefix(values[0], "for=") {
121 | value := strings.Trim(values[0][4:], `"][`)
122 | if value[0] != '_' {
123 | return value
124 | }
125 | }
126 | }
127 |
128 | if v := r.Header.Get("X-Forwarded-For"); v != "" {
129 | values := strings.Split(v, ", ")
130 | if values[0] != "unknown" {
131 | return values[0]
132 | }
133 | }
134 |
135 | if v := r.Header.Get("X-Real-IP"); v != "" {
136 | return v
137 | }
138 |
139 | return "unknown"
140 | }
141 |
--------------------------------------------------------------------------------