├── .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 [![GoDoc](https://pkg.go.dev/badge/github.com/srfrog/go-relax)](https://pkg.go.dev/github.com/srfrog/go-relax) [![Go Report Card](https://goreportcard.com/badge/github.com/srfrog/go-relax?svg=1)](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 | 25 | Full Name 26 | 27 | some 28 | strings 29 | here 30 | 31 | `) 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 | --------------------------------------------------------------------------------