switch local rest middlewares to go-pkgz/rest
This commit is contained in:
+13
@@ -0,0 +1,13 @@
|
||||
# Binaries for programs and plugins
|
||||
*.exe
|
||||
*.exe~
|
||||
*.dll
|
||||
*.so
|
||||
*.dylib
|
||||
|
||||
# Test binary, build with `go test -c`
|
||||
*.test
|
||||
|
||||
# Output of the go coverage tool, specifically when used with LiteIDE
|
||||
*.out
|
||||
vendor
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
language: go
|
||||
|
||||
go:
|
||||
- "1.11.x"
|
||||
|
||||
install: true
|
||||
|
||||
before_install:
|
||||
- go get github.com/mattn/goveralls
|
||||
- go get gopkg.in/alecthomas/gometalinter.v2
|
||||
- $GOPATH/bin/gometalinter.v2 --install
|
||||
|
||||
script:
|
||||
- GO111MODULE=on go get ./...
|
||||
- GO111MODULE=on go mod vendor
|
||||
- GO111MODULE=on go test -v -mod=vendor ./...
|
||||
- $GOPATH/bin/gometalinter.v2 --exclude=test --exclude=mock --exclude=vendor ./...
|
||||
- $GOPATH/bin/goveralls -service=travis-ci
|
||||
Generated
Vendored
-15
@@ -1,15 +0,0 @@
|
||||
ISC License
|
||||
|
||||
Copyright (c) 2012-2016 Dave Collins <dave@davec.name>
|
||||
|
||||
Permission to use, copy, modify, and distribute this software for any
|
||||
purpose with or without fee is hereby granted, provided that the above
|
||||
copyright notice and this permission notice appear in all copies.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
-20
@@ -1,20 +0,0 @@
|
||||
Copyright (c) 2015-present Peter Kieltyka (https://github.com/pkieltyka), Google Inc.
|
||||
|
||||
MIT License
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
this software and associated documentation files (the "Software"), to deal in
|
||||
the Software without restriction, including without limitation the rights to
|
||||
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
-20
@@ -1,20 +0,0 @@
|
||||
Copyright (c) 2016-Present https://github.com/go-chi authors
|
||||
|
||||
MIT License
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
this software and associated documentation files (the "Software"), to deal in
|
||||
the Software without restriction, including without limitation the rights to
|
||||
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
-23
@@ -1,23 +0,0 @@
|
||||
Copyright (c) 2015, Dave Cheney <dave@cheney.net>
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are met:
|
||||
|
||||
* Redistributions of source code must retain the above copyright notice, this
|
||||
list of conditions and the following disclaimer.
|
||||
|
||||
* Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation
|
||||
and/or other materials provided with the distribution.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
||||
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
Generated
Vendored
-27
@@ -1,27 +0,0 @@
|
||||
Copyright (c) 2013, Patrick Mezard
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are
|
||||
met:
|
||||
|
||||
Redistributions of source code must retain the above copyright
|
||||
notice, this list of conditions and the following disclaimer.
|
||||
Redistributions in binary form must reproduce the above copyright
|
||||
notice, this list of conditions and the following disclaimer in the
|
||||
documentation and/or other materials provided with the distribution.
|
||||
The names of its contributors may not be used to endorse or promote
|
||||
products derived from this software without specific prior written
|
||||
permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
|
||||
IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
|
||||
TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
|
||||
PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||
HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
|
||||
TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
|
||||
PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
|
||||
LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
|
||||
NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
Generated
Vendored
-22
@@ -1,22 +0,0 @@
|
||||
Copyright (c) 2012 - 2013 Mat Ryer and Tyler Bunnell
|
||||
|
||||
Please consider promoting this project if you find it useful.
|
||||
|
||||
Permission is hereby granted, free of charge, to any person
|
||||
obtaining a copy of this software and associated documentation
|
||||
files (the "Software"), to deal in the Software without restriction,
|
||||
including without limitation the rights to use, copy, modify, merge,
|
||||
publish, distribute, sublicense, and/or sell copies of the Software,
|
||||
and to permit persons to whom the Software is furnished to do so,
|
||||
subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included
|
||||
in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
|
||||
OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||||
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
|
||||
DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT
|
||||
OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE
|
||||
OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
+71
@@ -0,0 +1,71 @@
|
||||
## REST helpers and middleware [](https://travis-ci.org/go-pkgz/rest) [](https://goreportcard.com/report/github.com/go-pkgz/rest) [](https://coveralls.io/github/go-pkgz/rest?branch=master)
|
||||
|
||||
## Install and update
|
||||
|
||||
`go get -u github.com/go-pkgz/rest`
|
||||
|
||||
## Middlewares
|
||||
|
||||
### AppInfo middleware
|
||||
|
||||
Adds info to every response header:
|
||||
- App-Name - application name
|
||||
- App-Version - application version
|
||||
- Org - organization
|
||||
- M-Host - host name from instance-level `$MHOST` env
|
||||
|
||||
### Ping-Pong middleware
|
||||
|
||||
Responds with `pong` on `GET /ping`. Also responds to anything with `/ping` suffix, like `/v2/ping`
|
||||
|
||||
example for both:
|
||||
|
||||
```
|
||||
> http GET https://remark42.radio-t.com/ping
|
||||
|
||||
HTTP/1.1 200 OK
|
||||
Date: Sun, 15 Jul 2018 19:40:31 GMT
|
||||
Content-Type: text/plain
|
||||
Content-Length: 4
|
||||
Connection: keep-alive
|
||||
App-Name: remark42
|
||||
App-Version: master-ed92a0b-20180630-15:59:56
|
||||
Org: Umputun
|
||||
|
||||
pong
|
||||
```
|
||||
|
||||
### Logger middleware
|
||||
|
||||
Logs all info about request, including user, method, status code, response size, url, elapsed time, request body (optional).
|
||||
Can be customized by passing flags - LogNone, LogAll, LogUser and LogBody. Flags can be combined (provided multiple times)
|
||||
|
||||
### Recoverer middleware
|
||||
|
||||
Recoverer is a middleware that recovers from panics, logs the panic (and a backtrace),
|
||||
and returns a HTTP 500 (Internal Server Error) status if possible.
|
||||
|
||||
### OnlyFrom middleware
|
||||
|
||||
OnlyFrom middleware allows access for limited list of source IPs.
|
||||
Such IPs can be defined as complete ip (like 192.168.1.12), prefix (129.168.) or CIDR (192.168.0.0/16)
|
||||
|
||||
### Metrics middleware
|
||||
|
||||
Metrics middleware responds to GET /metrics with list of [expvar](https://golang.org/pkg/expvar/). Optionally allows to restrict list of source ips.
|
||||
|
||||
### BlackWords middleware
|
||||
|
||||
BlackWords middleware doesn't allow user-defined words in the request body.
|
||||
|
||||
## Helpers
|
||||
|
||||
- `rest.JSON` - map alias, just for convenience `type JSON map[string]interface{}`
|
||||
- `rest.RenderJSON` - renders json response from `interface{}`
|
||||
- `rest.RenderJSONFromBytes` - renders json response from `[]byte`
|
||||
- `rest.RenderJSONWithHTML` - renders json response with html tags and forced `charset=utf-8`
|
||||
- `rest.SendErrorJSON` - makes `{error: blah, details: blah}` json body and responds with given error code. Also adds context to logged message
|
||||
|
||||
## Caching
|
||||
|
||||
Cache wrapper provides loading cache for rest/http responses. See [cache readme](https://github.com/go-pkgz/rest/tree/master/cache) for more details and examples.
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
package rest
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// BlackWords middleware doesn't allow some words in the request body
|
||||
func BlackWords(words ...string) func(http.Handler) http.Handler {
|
||||
|
||||
return func(h http.Handler) http.Handler {
|
||||
fn := func(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
if content, err := ioutil.ReadAll(r.Body); err == nil {
|
||||
body := strings.ToLower(string(content))
|
||||
r.Body = ioutil.NopCloser(bytes.NewReader(content))
|
||||
|
||||
if len(body) > 0 {
|
||||
for _, word := range words {
|
||||
if strings.Contains(body, strings.ToLower(word)) {
|
||||
w.WriteHeader(http.StatusForbidden)
|
||||
RenderJSON(w, r, JSON{"error": "one of blacklisted words detected"})
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
h.ServeHTTP(w, r)
|
||||
}
|
||||
return http.HandlerFunc(fn)
|
||||
}
|
||||
}
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
module github.com/go-pkgz/rest
|
||||
|
||||
require (
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/hashicorp/golang-lru v0.5.0
|
||||
github.com/pkg/errors v0.8.0
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/stretchr/testify v1.2.2
|
||||
)
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/hashicorp/golang-lru v0.5.0 h1:CL2msUPvZTLb5O648aiLNJw3hnBxN2+1Jq8rCOH9wdo=
|
||||
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
||||
github.com/pkg/errors v0.8.0 h1:WdK/asTD0HN+q6hsWO3/vpuAkAr+tw6aNJNDFFf0+qw=
|
||||
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w=
|
||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
package rest
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"runtime"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// SendErrorJSON makes {error: blah, details: blah} json body and responds with error code
|
||||
func SendErrorJSON(w http.ResponseWriter, r *http.Request, code int, err error, details string) {
|
||||
log.Printf("[DEBUG] %s", errDetailsMsg(r, code, err, details))
|
||||
w.WriteHeader(code)
|
||||
RenderJSON(w, r, map[string]interface{}{"error": err.Error(), "details": details})
|
||||
}
|
||||
|
||||
func errDetailsMsg(r *http.Request, code int, err error, details string) string {
|
||||
|
||||
q := r.URL.String()
|
||||
if qun, e := url.QueryUnescape(q); e == nil {
|
||||
q = qun
|
||||
}
|
||||
|
||||
srcFileInfo := ""
|
||||
if pc, file, line, ok := runtime.Caller(2); ok {
|
||||
fnameElems := strings.Split(file, "/")
|
||||
funcNameElems := strings.Split(runtime.FuncForPC(pc).Name(), "/")
|
||||
srcFileInfo = fmt.Sprintf(" [caused by %s:%d %s]", strings.Join(fnameElems[len(fnameElems)-3:], "/"),
|
||||
line, funcNameElems[len(funcNameElems)-1])
|
||||
}
|
||||
|
||||
remoteIP := r.RemoteAddr
|
||||
if pos := strings.Index(remoteIP, ":"); pos >= 0 {
|
||||
remoteIP = remoteIP[:pos]
|
||||
}
|
||||
return fmt.Sprintf("%s - %v - %d - %s - %s%s", details, err, code, remoteIP, q, srcFileInfo)
|
||||
}
|
||||
+192
@@ -0,0 +1,192 @@
|
||||
package logger
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
var reMultWhtsp = regexp.MustCompile(`[\s\p{Zs}]{2,}`)
|
||||
|
||||
// Middleware for logging rest requests
|
||||
type Middleware struct {
|
||||
prefix string
|
||||
maxBodySize int
|
||||
flags []Flag
|
||||
ipFn func(ip string) string
|
||||
userFn func(r *http.Request) (string, error)
|
||||
}
|
||||
|
||||
// Flag type
|
||||
type Flag int
|
||||
|
||||
// logger flags enum
|
||||
const (
|
||||
All Flag = iota
|
||||
User
|
||||
Body
|
||||
None
|
||||
)
|
||||
|
||||
// New makes rest Logger with given options
|
||||
func New(options ...Option) *Middleware {
|
||||
res := Middleware{
|
||||
prefix: "",
|
||||
maxBodySize: 1024,
|
||||
flags: []Flag{All},
|
||||
}
|
||||
for _, opt := range options {
|
||||
opt(&res)
|
||||
}
|
||||
return &res
|
||||
}
|
||||
|
||||
// Handler middleware prints http log
|
||||
func (l *Middleware) Handler(next http.Handler) http.Handler {
|
||||
|
||||
fn := func(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
if l.inLogFlags(None) { // skip logging
|
||||
next.ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
ww := newCustomResponseWriter(w)
|
||||
body, user := l.getBodyAndUser(r)
|
||||
t1 := time.Now()
|
||||
defer func() {
|
||||
t2 := time.Now()
|
||||
|
||||
q := l.sanitizeQuery(r.URL.String())
|
||||
if qun, err := url.QueryUnescape(q); err == nil {
|
||||
q = qun
|
||||
}
|
||||
|
||||
remoteIP := strings.Split(r.RemoteAddr, ":")[0]
|
||||
if strings.HasPrefix(r.RemoteAddr, "[") {
|
||||
remoteIP = strings.Split(r.RemoteAddr, "]:")[0] + "]"
|
||||
}
|
||||
|
||||
if l.ipFn != nil { // mask ip with ipFn
|
||||
remoteIP = l.ipFn(remoteIP)
|
||||
}
|
||||
|
||||
log.Printf("%s %s - %s - %s - %d (%d) - %v %s %s",
|
||||
l.prefix, r.Method, q, remoteIP, ww.status, ww.size, t2.Sub(t1), user, body)
|
||||
}()
|
||||
|
||||
next.ServeHTTP(ww, r)
|
||||
}
|
||||
return http.HandlerFunc(fn)
|
||||
}
|
||||
|
||||
func (l *Middleware) getBodyAndUser(r *http.Request) (body string, user string) {
|
||||
ctx := r.Context()
|
||||
if ctx == nil {
|
||||
return "", ""
|
||||
}
|
||||
|
||||
if l.inLogFlags(Body) {
|
||||
if content, err := ioutil.ReadAll(r.Body); err == nil {
|
||||
body = string(content)
|
||||
r.Body = ioutil.NopCloser(bytes.NewReader(content))
|
||||
|
||||
if len(body) > 0 {
|
||||
body = strings.Replace(body, "\n", " ", -1)
|
||||
body = reMultWhtsp.ReplaceAllString(body, " ")
|
||||
}
|
||||
|
||||
if len(body) > l.maxBodySize {
|
||||
body = body[:l.maxBodySize] + "..."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if l.inLogFlags(User) && l.userFn != nil {
|
||||
u, err := l.userFn(r)
|
||||
if err == nil && u != "" {
|
||||
user = fmt.Sprintf(" - %s", u)
|
||||
}
|
||||
}
|
||||
|
||||
return body, user
|
||||
}
|
||||
|
||||
func (l *Middleware) inLogFlags(f Flag) bool {
|
||||
for _, flg := range l.flags {
|
||||
if (flg == All && f != None) || flg == f {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (l *Middleware) sanitizeQuery(inp string) string {
|
||||
out := []rune(inp)
|
||||
hide := []string{"password", "passwd", "secret", "credentials"}
|
||||
for _, h := range hide {
|
||||
if strings.Contains(strings.ToLower(inp), h+"=") {
|
||||
stPos := strings.Index(strings.ToLower(inp), h+"=") + len(h) + 1
|
||||
fnPos := strings.Index(inp[stPos:], "&")
|
||||
if fnPos == -1 {
|
||||
fnPos = len(inp)
|
||||
} else {
|
||||
fnPos = stPos + fnPos
|
||||
}
|
||||
for i := stPos; i < fnPos; i++ {
|
||||
out[i] = rune('*')
|
||||
}
|
||||
}
|
||||
}
|
||||
return string(out)
|
||||
}
|
||||
|
||||
// customResponseWriter implements ResponseWriter and keeping status and size
|
||||
type customResponseWriter struct {
|
||||
http.ResponseWriter
|
||||
status int
|
||||
size int
|
||||
}
|
||||
|
||||
func newCustomResponseWriter(w http.ResponseWriter) *customResponseWriter {
|
||||
return &customResponseWriter{
|
||||
ResponseWriter: w,
|
||||
status: 200,
|
||||
}
|
||||
}
|
||||
|
||||
// WriteHeader implements ResponseWriter and saves status
|
||||
func (c *customResponseWriter) WriteHeader(status int) {
|
||||
c.status = status
|
||||
c.ResponseWriter.WriteHeader(status)
|
||||
}
|
||||
|
||||
// WriteHeader implements ResponseWriter and tracking size
|
||||
func (c *customResponseWriter) Write(b []byte) (int, error) {
|
||||
size, err := c.ResponseWriter.Write(b)
|
||||
c.size += size
|
||||
return size, err
|
||||
}
|
||||
|
||||
// Flush implements ResponseWriter
|
||||
func (c *customResponseWriter) Flush() {
|
||||
if f, ok := c.ResponseWriter.(http.Flusher); ok {
|
||||
f.Flush()
|
||||
}
|
||||
}
|
||||
|
||||
// Hijack implements ResponseWriter
|
||||
func (c *customResponseWriter) Hijack() (net.Conn, *bufio.ReadWriter, error) {
|
||||
if hj, ok := c.ResponseWriter.(http.Hijacker); ok {
|
||||
return hj.Hijack()
|
||||
}
|
||||
return nil, nil, fmt.Errorf("ResponseWriter does not implement the Hijacker interface")
|
||||
}
|
||||
+45
@@ -0,0 +1,45 @@
|
||||
package logger
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
)
|
||||
|
||||
// Option func type
|
||||
type Option func(l *Middleware)
|
||||
|
||||
// Flags functional option defines output modes
|
||||
func Flags(flags ...Flag) Option {
|
||||
return func(l *Middleware) {
|
||||
l.flags = flags
|
||||
}
|
||||
}
|
||||
|
||||
// MaxBodySize functional option defines the largest body size to log.
|
||||
func MaxBodySize(max int) Option {
|
||||
return func(l *Middleware) {
|
||||
if max >= 0 {
|
||||
l.maxBodySize = max
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Prefix functional option defines log line prefix.
|
||||
func Prefix(prefix string) Option {
|
||||
return func(l *Middleware) {
|
||||
l.prefix = prefix
|
||||
}
|
||||
}
|
||||
|
||||
// IPfn functional option defines ip masking function.
|
||||
func IPfn(ipFn func(ip string) string) Option {
|
||||
return func(l *Middleware) {
|
||||
l.ipFn = ipFn
|
||||
}
|
||||
}
|
||||
|
||||
// UserFn functional option defines user name function.
|
||||
func UserFn(userFn func(r *http.Request) (string, error)) Option {
|
||||
return func(l *Middleware) {
|
||||
l.userFn = userFn
|
||||
}
|
||||
}
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
package rest
|
||||
|
||||
import (
|
||||
"expvar"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Metrics responds to GET /metrics with list of expvar
|
||||
func Metrics(onlyIps ...string) func(http.Handler) http.Handler {
|
||||
|
||||
return func(h http.Handler) http.Handler {
|
||||
|
||||
fn := func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method == "GET" && strings.HasSuffix(strings.ToLower(r.URL.Path), "/metrics") {
|
||||
if matched, ip := matchSourceIP(r, onlyIps); !matched {
|
||||
w.WriteHeader(http.StatusForbidden)
|
||||
RenderJSON(w, r, JSON{"error": fmt.Sprintf("ip %s rejected", ip)})
|
||||
return
|
||||
}
|
||||
expvar.Handler().ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
h.ServeHTTP(w, r)
|
||||
}
|
||||
|
||||
return http.HandlerFunc(fn)
|
||||
}
|
||||
}
|
||||
+58
@@ -0,0 +1,58 @@
|
||||
package rest
|
||||
|
||||
import (
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"runtime/debug"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// AppInfo adds custom app-info to the response header
|
||||
func AppInfo(app string, author string, version string) func(http.Handler) http.Handler {
|
||||
f := func(h http.Handler) http.Handler {
|
||||
fn := func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Author", author)
|
||||
w.Header().Set("App-Name", app)
|
||||
w.Header().Set("App-Version", version)
|
||||
if mhost := os.Getenv("MHOST"); mhost != "" {
|
||||
w.Header().Set("Host", mhost)
|
||||
}
|
||||
h.ServeHTTP(w, r)
|
||||
}
|
||||
return http.HandlerFunc(fn)
|
||||
}
|
||||
return f
|
||||
}
|
||||
|
||||
// Ping middleware response with pong to /ping. Stops chain if ping request detected
|
||||
func Ping(next http.Handler) http.Handler {
|
||||
fn := func(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
if r.Method == "GET" && strings.HasSuffix(strings.ToLower(r.URL.Path), "/ping") {
|
||||
w.Header().Set("Content-Type", "text/plain")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
if _, err := w.Write([]byte("pong")); err != nil {
|
||||
log.Printf("[WARN] can't send pong, %s", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
next.ServeHTTP(w, r)
|
||||
}
|
||||
return http.HandlerFunc(fn)
|
||||
}
|
||||
|
||||
// Recoverer is a middleware that recovers from panics, logs the panic and returns a HTTP 500 status if possible.
|
||||
func Recoverer(next http.Handler) http.Handler {
|
||||
fn := func(w http.ResponseWriter, r *http.Request) {
|
||||
defer func() {
|
||||
if rvr := recover(); rvr != nil {
|
||||
log.Printf("[WARN] request panic, %v", rvr)
|
||||
log.Print(string(debug.Stack()))
|
||||
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||
}
|
||||
}()
|
||||
next.ServeHTTP(w, r)
|
||||
}
|
||||
return http.HandlerFunc(fn)
|
||||
}
|
||||
+58
@@ -0,0 +1,58 @@
|
||||
package rest
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// OnlyFrom middleware allows access for limited list of source IPs.
|
||||
// Such IPs can be defined as complete ip (like 192.168.1.12), prefix (129.168.) or CIDR (192.168.0.0/16)
|
||||
func OnlyFrom(onlyIps ...string) func(http.Handler) http.Handler {
|
||||
|
||||
return func(h http.Handler) http.Handler {
|
||||
|
||||
fn := func(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
matched, ip := matchSourceIP(r, onlyIps)
|
||||
if matched {
|
||||
h.ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
w.WriteHeader(http.StatusForbidden)
|
||||
RenderJSON(w, r, JSON{"error": fmt.Sprintf("ip %s rejected", ip)})
|
||||
}
|
||||
return http.HandlerFunc(fn)
|
||||
}
|
||||
}
|
||||
|
||||
// matchSourceIP returns true if request's ip matches any of ips
|
||||
func matchSourceIP(r *http.Request, ips []string) (bool, string) {
|
||||
|
||||
// try X-Real-IP first then fail back to X-Forwarded-For and finally to RemoteAddr
|
||||
ip := r.Header.Get("X-Real-IP")
|
||||
if ip == "" {
|
||||
ip = strings.Split(r.Header.Get("X-Forwarded-For"), ", ")[0]
|
||||
}
|
||||
if ip == "" {
|
||||
ip = r.Header.Get("RemoteAddr")
|
||||
}
|
||||
if ip == "" {
|
||||
ip = strings.Split(r.RemoteAddr, ":")[0]
|
||||
}
|
||||
|
||||
// check for ip prefix or CIDR
|
||||
for _, exclIP := range ips {
|
||||
if _, cidrnet, err := net.ParseCIDR(exclIP); err == nil {
|
||||
if cidrnet.Contains(net.ParseIP(ip)) {
|
||||
return true, ip
|
||||
}
|
||||
}
|
||||
if strings.HasPrefix(ip, exclIP) {
|
||||
return true, ip
|
||||
}
|
||||
}
|
||||
return false, ip
|
||||
}
|
||||
+54
@@ -0,0 +1,54 @@
|
||||
package rest
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
// JSON is a map alias, just for convenience
|
||||
type JSON map[string]interface{}
|
||||
|
||||
// RenderJSON sends data as json
|
||||
func RenderJSON(w http.ResponseWriter, r *http.Request, data interface{}) {
|
||||
buf := &bytes.Buffer{}
|
||||
enc := json.NewEncoder(buf)
|
||||
enc.SetEscapeHTML(true)
|
||||
if err := enc.Encode(data); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||
w.Write(buf.Bytes()) // nolint: errcheck, gosec
|
||||
}
|
||||
|
||||
// RenderJSONFromBytes sends binary data as json
|
||||
func RenderJSONFromBytes(w http.ResponseWriter, r *http.Request, data []byte) error {
|
||||
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||
if _, err := w.Write(data); err != nil {
|
||||
return errors.Wrapf(err, "failed to send response to %s", r.RemoteAddr)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// RenderJSONWithHTML allows html tags and forces charset=utf-8
|
||||
func RenderJSONWithHTML(w http.ResponseWriter, r *http.Request, v interface{}) error {
|
||||
|
||||
encodeJSONWithHTML := func(v interface{}) ([]byte, error) {
|
||||
buf := &bytes.Buffer{}
|
||||
enc := json.NewEncoder(buf)
|
||||
enc.SetEscapeHTML(false)
|
||||
if err := enc.Encode(v); err != nil {
|
||||
return nil, errors.Wrap(err, "json encoding failed")
|
||||
}
|
||||
return buf.Bytes(), nil
|
||||
}
|
||||
|
||||
data, err := encodeJSONWithHTML(v)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "json encoding failed")
|
||||
}
|
||||
return RenderJSONFromBytes(w, r, data)
|
||||
}
|
||||
Reference in New Issue
Block a user