chore: migrate to Go modules
This commit is contained in:
-1
Submodule vendor/github.com/gorilla/context deleted from 51ce91d2ea
+20
@@ -0,0 +1,20 @@
|
||||
; https://editorconfig.org/
|
||||
|
||||
root = true
|
||||
|
||||
[*]
|
||||
insert_final_newline = true
|
||||
charset = utf-8
|
||||
trim_trailing_whitespace = true
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
|
||||
[{Makefile,go.mod,go.sum,*.go,.gitmodules}]
|
||||
indent_style = tab
|
||||
indent_size = 4
|
||||
|
||||
[*.md]
|
||||
indent_size = 4
|
||||
trim_trailing_whitespace = false
|
||||
|
||||
eclint_indent_style = unset
|
||||
+1
@@ -0,0 +1 @@
|
||||
coverage.coverprofile
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
linters:
|
||||
enable:
|
||||
- errcheck
|
||||
- gosimple
|
||||
- govet
|
||||
- ineffassign
|
||||
- staticcheck
|
||||
- unused
|
||||
- contextcheck
|
||||
- goconst
|
||||
- gofmt
|
||||
- misspell
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
Copyright (c) 2012-2023 The Gorilla web toolkit authors. 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.
|
||||
* Neither the name of Google Inc. nor the names of its
|
||||
contributors may 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
|
||||
OWNER 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.
|
||||
+52
@@ -0,0 +1,52 @@
|
||||
GO_LINT=$(shell which golangci-lint 2> /dev/null || echo '')
|
||||
GO_LINT_URI=github.com/golangci/golangci-lint/cmd/golangci-lint@latest
|
||||
|
||||
GO_SEC=$(shell which gosec 2> /dev/null || echo '')
|
||||
GO_SEC_URI=github.com/securego/gosec/v2/cmd/gosec@latest
|
||||
|
||||
GO_VULNCHECK=$(shell which govulncheck 2> /dev/null || echo '')
|
||||
GO_VULNCHECK_URI=golang.org/x/vuln/cmd/govulncheck@latest
|
||||
|
||||
.PHONY: golangci-lint
|
||||
golangci-lint: ## Run golangci-lint. Example: make golangci-lint
|
||||
$(if $(GO_LINT), ,go install $(GO_LINT_URI))
|
||||
@echo "##### Running golangci-lint #####"
|
||||
golangci-lint run -v
|
||||
|
||||
.PHONY: verify
|
||||
verify: ## Run all verifications [golangci-lint]. Example: make verify
|
||||
@echo "##### Running verifications #####"
|
||||
$(MAKE) golangci-lint
|
||||
|
||||
.PHONY: gosec
|
||||
gosec: ## Run gosec. Example: make gosec
|
||||
$(if $(GO_SEC), ,go install $(GO_SEC_URI))
|
||||
@echo "##### Running gosec #####"
|
||||
gosec ./...
|
||||
|
||||
.PHONY: govulncheck
|
||||
govulncheck: ## Run govulncheck. Example: make govulncheck
|
||||
$(if $(GO_VULNCHECK), ,go install $(GO_VULNCHECK_URI))
|
||||
@echo "##### Running govulncheck #####"
|
||||
govulncheck ./...
|
||||
|
||||
.PHONY: security
|
||||
security: ## Run all security checks [gosec, govulncheck]. Example: make security
|
||||
@echo "##### Running security checks #####"
|
||||
$(MAKE) gosec
|
||||
$(MAKE) govulncheck
|
||||
|
||||
.PHONY: test-unit
|
||||
test-unit: ## Run unit tests. Example: make test-unit
|
||||
@echo "##### Running unit tests #####"
|
||||
go test -race -cover -coverprofile=coverage.coverprofile -covermode=atomic -v ./...
|
||||
|
||||
.PHONY: test
|
||||
test: ## Run all tests [test-unit]. Example: make test
|
||||
@echo "##### Running tests #####"
|
||||
$(MAKE) test-unit
|
||||
|
||||
.PHONY: help
|
||||
help: ## Print this help. Example: make help
|
||||
@echo "##### Printing help #####"
|
||||
@awk 'BEGIN {FS = ":.*?## "} /^[a-zA-Z_-]+:.*?## / {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}' $(MAKEFILE_LIST)
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
# gorilla/context
|
||||
|
||||
[](https://img.shields.io/github/license/gorilla/.github)
|
||||

|
||||
[](https://codecov.io/github/gorilla/context)
|
||||
[](https://godoc.org/github.com/gorilla/context)
|
||||
[](https://sourcegraph.com/github.com/gorilla/context?badge)
|
||||
[](https://bestpractices.coreinfrastructure.org/projects/7656)
|
||||
|
||||

|
||||
|
||||
> ⚠⚠⚠ **Note** ⚠⚠⚠ gorilla/context, having been born well before `context.Context` existed, does not play well
|
||||
> with the shallow copying of the request that [`http.Request.WithContext`](https://golang.org/pkg/net/http/#Request.WithContext) (added to net/http Go 1.7 onwards) performs.
|
||||
>
|
||||
> Using gorilla/context may lead to memory leaks under those conditions, as the pointers to each `http.Request` become "islanded" and will not be cleaned up when the response is sent.
|
||||
>
|
||||
> You should use the `http.Request.Context()` feature in Go 1.7.
|
||||
|
||||
gorilla/context is a general purpose registry for global request variables.
|
||||
|
||||
* It stores a `map[*http.Request]map[interface{}]interface{}` as a global singleton, and thus tracks variables by their HTTP request.
|
||||
|
||||
|
||||
### License
|
||||
|
||||
See the LICENSE file for details.
|
||||
+139
@@ -0,0 +1,139 @@
|
||||
package context
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
var (
|
||||
mutex sync.RWMutex
|
||||
data = make(map[*http.Request]map[interface{}]interface{})
|
||||
datat = make(map[*http.Request]int64)
|
||||
)
|
||||
|
||||
// Set stores a value for a given key in a given request.
|
||||
func Set(r *http.Request, key, val interface{}) {
|
||||
mutex.Lock()
|
||||
if data[r] == nil {
|
||||
data[r] = make(map[interface{}]interface{})
|
||||
datat[r] = time.Now().Unix()
|
||||
}
|
||||
data[r][key] = val
|
||||
mutex.Unlock()
|
||||
}
|
||||
|
||||
// Get returns a value stored for a given key in a given request.
|
||||
func Get(r *http.Request, key interface{}) interface{} {
|
||||
mutex.RLock()
|
||||
if ctx := data[r]; ctx != nil {
|
||||
value := ctx[key]
|
||||
mutex.RUnlock()
|
||||
return value
|
||||
}
|
||||
mutex.RUnlock()
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetOk returns stored value and presence state like multi-value return of map access.
|
||||
func GetOk(r *http.Request, key interface{}) (interface{}, bool) {
|
||||
mutex.RLock()
|
||||
if _, ok := data[r]; ok {
|
||||
value, ok := data[r][key]
|
||||
mutex.RUnlock()
|
||||
return value, ok
|
||||
}
|
||||
mutex.RUnlock()
|
||||
return nil, false
|
||||
}
|
||||
|
||||
// GetAll returns all stored values for the request as a map. Nil is returned for invalid requests.
|
||||
func GetAll(r *http.Request) map[interface{}]interface{} {
|
||||
mutex.RLock()
|
||||
if context, ok := data[r]; ok {
|
||||
result := make(map[interface{}]interface{}, len(context))
|
||||
for k, v := range context {
|
||||
result[k] = v
|
||||
}
|
||||
mutex.RUnlock()
|
||||
return result
|
||||
}
|
||||
mutex.RUnlock()
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetAllOk returns all stored values for the request as a map and a boolean value that indicates if
|
||||
// the request was registered.
|
||||
func GetAllOk(r *http.Request) (map[interface{}]interface{}, bool) {
|
||||
mutex.RLock()
|
||||
context, ok := data[r]
|
||||
result := make(map[interface{}]interface{}, len(context))
|
||||
for k, v := range context {
|
||||
result[k] = v
|
||||
}
|
||||
mutex.RUnlock()
|
||||
return result, ok
|
||||
}
|
||||
|
||||
// Delete removes a value stored for a given key in a given request.
|
||||
func Delete(r *http.Request, key interface{}) {
|
||||
mutex.Lock()
|
||||
if data[r] != nil {
|
||||
delete(data[r], key)
|
||||
}
|
||||
mutex.Unlock()
|
||||
}
|
||||
|
||||
// Clear removes all values stored for a given request.
|
||||
//
|
||||
// This is usually called by a handler wrapper to clean up request
|
||||
// variables at the end of a request lifetime. See ClearHandler().
|
||||
func Clear(r *http.Request) {
|
||||
mutex.Lock()
|
||||
clear(r)
|
||||
mutex.Unlock()
|
||||
}
|
||||
|
||||
// clear is Clear without the lock.
|
||||
func clear(r *http.Request) {
|
||||
delete(data, r)
|
||||
delete(datat, r)
|
||||
}
|
||||
|
||||
// Purge removes request data stored for longer than maxAge, in seconds.
|
||||
// It returns the amount of requests removed.
|
||||
//
|
||||
// If maxAge <= 0, all request data is removed.
|
||||
//
|
||||
// This is only used for sanity check: in case context cleaning was not
|
||||
// properly set some request data can be kept forever, consuming an increasing
|
||||
// amount of memory. In case this is detected, Purge() must be called
|
||||
// periodically until the problem is fixed.
|
||||
func Purge(maxAge int) int {
|
||||
mutex.Lock()
|
||||
count := 0
|
||||
if maxAge <= 0 {
|
||||
count = len(data)
|
||||
data = make(map[*http.Request]map[interface{}]interface{})
|
||||
datat = make(map[*http.Request]int64)
|
||||
} else {
|
||||
min := time.Now().Unix() - int64(maxAge)
|
||||
for r := range data {
|
||||
if datat[r] < min {
|
||||
clear(r)
|
||||
count++
|
||||
}
|
||||
}
|
||||
}
|
||||
mutex.Unlock()
|
||||
return count
|
||||
}
|
||||
|
||||
// ClearHandler wraps an http.Handler and clears request values at the end
|
||||
// of a request lifetime.
|
||||
func ClearHandler(h http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
defer Clear(r)
|
||||
h.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
+88
@@ -0,0 +1,88 @@
|
||||
// Copyright 2012 The Gorilla Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
/*
|
||||
Package context stores values shared during a request lifetime.
|
||||
|
||||
Note: gorilla/context, having been born well before `context.Context` existed,
|
||||
does not play well > with the shallow copying of the request that
|
||||
[`http.Request.WithContext`](https://golang.org/pkg/net/http/#Request.WithContext)
|
||||
(added to net/http Go 1.7 onwards) performs. You should either use *just*
|
||||
gorilla/context, or moving forward, the new `http.Request.Context()`.
|
||||
|
||||
For example, a router can set variables extracted from the URL and later
|
||||
application handlers can access those values, or it can be used to store
|
||||
sessions values to be saved at the end of a request. There are several
|
||||
others common uses.
|
||||
|
||||
The idea was posted by Brad Fitzpatrick to the go-nuts mailing list:
|
||||
|
||||
http://groups.google.com/group/golang-nuts/msg/e2d679d303aa5d53
|
||||
|
||||
Here's the basic usage: first define the keys that you will need. The key
|
||||
type is interface{} so a key can be of any type that supports equality.
|
||||
Here we define a key using a custom int type to avoid name collisions:
|
||||
|
||||
package foo
|
||||
|
||||
import (
|
||||
"github.com/gorilla/context"
|
||||
)
|
||||
|
||||
type key int
|
||||
|
||||
const MyKey key = 0
|
||||
|
||||
Then set a variable. Variables are bound to an http.Request object, so you
|
||||
need a request instance to set a value:
|
||||
|
||||
context.Set(r, MyKey, "bar")
|
||||
|
||||
The application can later access the variable using the same key you provided:
|
||||
|
||||
func MyHandler(w http.ResponseWriter, r *http.Request) {
|
||||
// val is "bar".
|
||||
val := context.Get(r, foo.MyKey)
|
||||
|
||||
// returns ("bar", true)
|
||||
val, ok := context.GetOk(r, foo.MyKey)
|
||||
// ...
|
||||
}
|
||||
|
||||
And that's all about the basic usage. We discuss some other ideas below.
|
||||
|
||||
Any type can be stored in the context. To enforce a given type, make the key
|
||||
private and wrap Get() and Set() to accept and return values of a specific
|
||||
type:
|
||||
|
||||
type key int
|
||||
|
||||
const mykey key = 0
|
||||
|
||||
// GetMyKey returns a value for this package from the request values.
|
||||
func GetMyKey(r *http.Request) SomeType {
|
||||
if rv := context.Get(r, mykey); rv != nil {
|
||||
return rv.(SomeType)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// SetMyKey sets a value for this package in the request values.
|
||||
func SetMyKey(r *http.Request, val SomeType) {
|
||||
context.Set(r, mykey, val)
|
||||
}
|
||||
|
||||
Variables must be cleared at the end of a request, to remove all values
|
||||
that were stored. This can be done in an http.Handler, after a request was
|
||||
served. Just call Clear() passing the request:
|
||||
|
||||
context.Clear(r)
|
||||
|
||||
...or use ClearHandler(), which conveniently wraps an http.Handler to clear
|
||||
variables at the end of a request lifetime.
|
||||
|
||||
The Routers from the packages gorilla/mux and gorilla/pat call Clear()
|
||||
so if you are using either of them you don't need to clear the context manually.
|
||||
*/
|
||||
package context
|
||||
-1
Submodule vendor/github.com/gorilla/handlers deleted from 7e6a874cdc
+20
@@ -0,0 +1,20 @@
|
||||
language: go
|
||||
sudo: false
|
||||
|
||||
matrix:
|
||||
include:
|
||||
- go: 1.4
|
||||
- go: 1.5
|
||||
- go: 1.6
|
||||
- go: 1.7
|
||||
- go: 1.8
|
||||
- go: tip
|
||||
allow_failures:
|
||||
- go: tip
|
||||
|
||||
script:
|
||||
- go get -t -v ./...
|
||||
- diff -u <(echo -n) <(gofmt -d .)
|
||||
- go vet $(go list ./... | grep -v /vendor/)
|
||||
- go test -v -race ./...
|
||||
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
Copyright (c) 2013 The Gorilla Handlers Authors. 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.
|
||||
+55
@@ -0,0 +1,55 @@
|
||||
gorilla/handlers
|
||||
================
|
||||
[](https://godoc.org/github.com/gorilla/handlers) [](https://travis-ci.org/gorilla/handlers)
|
||||
[](https://sourcegraph.com/github.com/gorilla/handlers?badge)
|
||||
|
||||
|
||||
Package handlers is a collection of handlers (aka "HTTP middleware") for use
|
||||
with Go's `net/http` package (or any framework supporting `http.Handler`), including:
|
||||
|
||||
* [**LoggingHandler**](https://godoc.org/github.com/gorilla/handlers#LoggingHandler) for logging HTTP requests in the Apache [Common Log
|
||||
Format](http://httpd.apache.org/docs/2.2/logs.html#common).
|
||||
* [**CombinedLoggingHandler**](https://godoc.org/github.com/gorilla/handlers#CombinedLoggingHandler) for logging HTTP requests in the Apache [Combined Log
|
||||
Format](http://httpd.apache.org/docs/2.2/logs.html#combined) commonly used by
|
||||
both Apache and nginx.
|
||||
* [**CompressHandler**](https://godoc.org/github.com/gorilla/handlers#CompressHandler) for gzipping responses.
|
||||
* [**ContentTypeHandler**](https://godoc.org/github.com/gorilla/handlers#ContentTypeHandler) for validating requests against a list of accepted
|
||||
content types.
|
||||
* [**MethodHandler**](https://godoc.org/github.com/gorilla/handlers#MethodHandler) for matching HTTP methods against handlers in a
|
||||
`map[string]http.Handler`
|
||||
* [**ProxyHeaders**](https://godoc.org/github.com/gorilla/handlers#ProxyHeaders) for populating `r.RemoteAddr` and `r.URL.Scheme` based on the
|
||||
`X-Forwarded-For`, `X-Real-IP`, `X-Forwarded-Proto` and RFC7239 `Forwarded`
|
||||
headers when running a Go server behind a HTTP reverse proxy.
|
||||
* [**CanonicalHost**](https://godoc.org/github.com/gorilla/handlers#CanonicalHost) for re-directing to the preferred host when handling multiple
|
||||
domains (i.e. multiple CNAME aliases).
|
||||
* [**RecoveryHandler**](https://godoc.org/github.com/gorilla/handlers#RecoveryHandler) for recovering from unexpected panics.
|
||||
|
||||
Other handlers are documented [on the Gorilla
|
||||
website](http://www.gorillatoolkit.org/pkg/handlers).
|
||||
|
||||
## Example
|
||||
|
||||
A simple example using `handlers.LoggingHandler` and `handlers.CompressHandler`:
|
||||
|
||||
```go
|
||||
import (
|
||||
"net/http"
|
||||
"github.com/gorilla/handlers"
|
||||
)
|
||||
|
||||
func main() {
|
||||
r := http.NewServeMux()
|
||||
|
||||
// Only log requests to our admin dashboard to stdout
|
||||
r.Handle("/admin", handlers.LoggingHandler(os.Stdout, http.HandlerFunc(ShowAdminDashboard)))
|
||||
r.HandleFunc("/", ShowIndex)
|
||||
|
||||
// Wrap our server with our gzip handler to gzip compress all responses.
|
||||
http.ListenAndServe(":8000", handlers.CompressHandler(r))
|
||||
}
|
||||
```
|
||||
|
||||
## License
|
||||
|
||||
BSD licensed. See the included LICENSE file for details.
|
||||
|
||||
+74
@@ -0,0 +1,74 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type canonical struct {
|
||||
h http.Handler
|
||||
domain string
|
||||
code int
|
||||
}
|
||||
|
||||
// CanonicalHost is HTTP middleware that re-directs requests to the canonical
|
||||
// domain. It accepts a domain and a status code (e.g. 301 or 302) and
|
||||
// re-directs clients to this domain. The existing request path is maintained.
|
||||
//
|
||||
// Note: If the provided domain is considered invalid by url.Parse or otherwise
|
||||
// returns an empty scheme or host, clients are not re-directed.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// r := mux.NewRouter()
|
||||
// canonical := handlers.CanonicalHost("http://www.gorillatoolkit.org", 302)
|
||||
// r.HandleFunc("/route", YourHandler)
|
||||
//
|
||||
// log.Fatal(http.ListenAndServe(":7000", canonical(r)))
|
||||
//
|
||||
func CanonicalHost(domain string, code int) func(h http.Handler) http.Handler {
|
||||
fn := func(h http.Handler) http.Handler {
|
||||
return canonical{h, domain, code}
|
||||
}
|
||||
|
||||
return fn
|
||||
}
|
||||
|
||||
func (c canonical) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
dest, err := url.Parse(c.domain)
|
||||
if err != nil {
|
||||
// Call the next handler if the provided domain fails to parse.
|
||||
c.h.ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
if dest.Scheme == "" || dest.Host == "" {
|
||||
// Call the next handler if the scheme or host are empty.
|
||||
// Note that url.Parse won't fail on in this case.
|
||||
c.h.ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
if !strings.EqualFold(cleanHost(r.Host), dest.Host) {
|
||||
// Re-build the destination URL
|
||||
dest := dest.Scheme + "://" + dest.Host + r.URL.Path
|
||||
if r.URL.RawQuery != "" {
|
||||
dest += "?" + r.URL.RawQuery
|
||||
}
|
||||
http.Redirect(w, r, dest, c.code)
|
||||
return
|
||||
}
|
||||
|
||||
c.h.ServeHTTP(w, r)
|
||||
}
|
||||
|
||||
// cleanHost cleans invalid Host headers by stripping anything after '/' or ' '.
|
||||
// This is backported from Go 1.5 (in response to issue #11206) and attempts to
|
||||
// mitigate malformed Host headers that do not match the format in RFC7230.
|
||||
func cleanHost(in string) string {
|
||||
if i := strings.IndexAny(in, " /"); i != -1 {
|
||||
return in[:i]
|
||||
}
|
||||
return in
|
||||
}
|
||||
+148
@@ -0,0 +1,148 @@
|
||||
// Copyright 2013 The Gorilla Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"compress/flate"
|
||||
"compress/gzip"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type compressResponseWriter struct {
|
||||
io.Writer
|
||||
http.ResponseWriter
|
||||
http.Hijacker
|
||||
http.Flusher
|
||||
http.CloseNotifier
|
||||
}
|
||||
|
||||
func (w *compressResponseWriter) WriteHeader(c int) {
|
||||
w.ResponseWriter.Header().Del("Content-Length")
|
||||
w.ResponseWriter.WriteHeader(c)
|
||||
}
|
||||
|
||||
func (w *compressResponseWriter) Header() http.Header {
|
||||
return w.ResponseWriter.Header()
|
||||
}
|
||||
|
||||
func (w *compressResponseWriter) Write(b []byte) (int, error) {
|
||||
h := w.ResponseWriter.Header()
|
||||
if h.Get("Content-Type") == "" {
|
||||
h.Set("Content-Type", http.DetectContentType(b))
|
||||
}
|
||||
h.Del("Content-Length")
|
||||
|
||||
return w.Writer.Write(b)
|
||||
}
|
||||
|
||||
type flusher interface {
|
||||
Flush() error
|
||||
}
|
||||
|
||||
func (w *compressResponseWriter) Flush() {
|
||||
// Flush compressed data if compressor supports it.
|
||||
if f, ok := w.Writer.(flusher); ok {
|
||||
f.Flush()
|
||||
}
|
||||
// Flush HTTP response.
|
||||
if w.Flusher != nil {
|
||||
w.Flusher.Flush()
|
||||
}
|
||||
}
|
||||
|
||||
// CompressHandler gzip compresses HTTP responses for clients that support it
|
||||
// via the 'Accept-Encoding' header.
|
||||
//
|
||||
// Compressing TLS traffic may leak the page contents to an attacker if the
|
||||
// page contains user input: http://security.stackexchange.com/a/102015/12208
|
||||
func CompressHandler(h http.Handler) http.Handler {
|
||||
return CompressHandlerLevel(h, gzip.DefaultCompression)
|
||||
}
|
||||
|
||||
// CompressHandlerLevel gzip compresses HTTP responses with specified compression level
|
||||
// for clients that support it via the 'Accept-Encoding' header.
|
||||
//
|
||||
// The compression level should be gzip.DefaultCompression, gzip.NoCompression,
|
||||
// or any integer value between gzip.BestSpeed and gzip.BestCompression inclusive.
|
||||
// gzip.DefaultCompression is used in case of invalid compression level.
|
||||
func CompressHandlerLevel(h http.Handler, level int) http.Handler {
|
||||
if level < gzip.DefaultCompression || level > gzip.BestCompression {
|
||||
level = gzip.DefaultCompression
|
||||
}
|
||||
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
L:
|
||||
for _, enc := range strings.Split(r.Header.Get("Accept-Encoding"), ",") {
|
||||
switch strings.TrimSpace(enc) {
|
||||
case "gzip":
|
||||
w.Header().Set("Content-Encoding", "gzip")
|
||||
w.Header().Add("Vary", "Accept-Encoding")
|
||||
|
||||
gw, _ := gzip.NewWriterLevel(w, level)
|
||||
defer gw.Close()
|
||||
|
||||
h, hok := w.(http.Hijacker)
|
||||
if !hok { /* w is not Hijacker... oh well... */
|
||||
h = nil
|
||||
}
|
||||
|
||||
f, fok := w.(http.Flusher)
|
||||
if !fok {
|
||||
f = nil
|
||||
}
|
||||
|
||||
cn, cnok := w.(http.CloseNotifier)
|
||||
if !cnok {
|
||||
cn = nil
|
||||
}
|
||||
|
||||
w = &compressResponseWriter{
|
||||
Writer: gw,
|
||||
ResponseWriter: w,
|
||||
Hijacker: h,
|
||||
Flusher: f,
|
||||
CloseNotifier: cn,
|
||||
}
|
||||
|
||||
break L
|
||||
case "deflate":
|
||||
w.Header().Set("Content-Encoding", "deflate")
|
||||
w.Header().Add("Vary", "Accept-Encoding")
|
||||
|
||||
fw, _ := flate.NewWriter(w, level)
|
||||
defer fw.Close()
|
||||
|
||||
h, hok := w.(http.Hijacker)
|
||||
if !hok { /* w is not Hijacker... oh well... */
|
||||
h = nil
|
||||
}
|
||||
|
||||
f, fok := w.(http.Flusher)
|
||||
if !fok {
|
||||
f = nil
|
||||
}
|
||||
|
||||
cn, cnok := w.(http.CloseNotifier)
|
||||
if !cnok {
|
||||
cn = nil
|
||||
}
|
||||
|
||||
w = &compressResponseWriter{
|
||||
Writer: fw,
|
||||
ResponseWriter: w,
|
||||
Hijacker: h,
|
||||
Flusher: f,
|
||||
CloseNotifier: cn,
|
||||
}
|
||||
|
||||
break L
|
||||
}
|
||||
}
|
||||
|
||||
h.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
+338
@@ -0,0 +1,338 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// CORSOption represents a functional option for configuring the CORS middleware.
|
||||
type CORSOption func(*cors) error
|
||||
|
||||
type cors struct {
|
||||
h http.Handler
|
||||
allowedHeaders []string
|
||||
allowedMethods []string
|
||||
allowedOrigins []string
|
||||
allowedOriginValidator OriginValidator
|
||||
exposedHeaders []string
|
||||
maxAge int
|
||||
ignoreOptions bool
|
||||
allowCredentials bool
|
||||
}
|
||||
|
||||
// OriginValidator takes an origin string and returns whether or not that origin is allowed.
|
||||
type OriginValidator func(string) bool
|
||||
|
||||
var (
|
||||
defaultCorsMethods = []string{"GET", "HEAD", "POST"}
|
||||
defaultCorsHeaders = []string{"Accept", "Accept-Language", "Content-Language", "Origin"}
|
||||
// (WebKit/Safari v9 sends the Origin header by default in AJAX requests)
|
||||
)
|
||||
|
||||
const (
|
||||
corsOptionMethod string = "OPTIONS"
|
||||
corsAllowOriginHeader string = "Access-Control-Allow-Origin"
|
||||
corsExposeHeadersHeader string = "Access-Control-Expose-Headers"
|
||||
corsMaxAgeHeader string = "Access-Control-Max-Age"
|
||||
corsAllowMethodsHeader string = "Access-Control-Allow-Methods"
|
||||
corsAllowHeadersHeader string = "Access-Control-Allow-Headers"
|
||||
corsAllowCredentialsHeader string = "Access-Control-Allow-Credentials"
|
||||
corsRequestMethodHeader string = "Access-Control-Request-Method"
|
||||
corsRequestHeadersHeader string = "Access-Control-Request-Headers"
|
||||
corsOriginHeader string = "Origin"
|
||||
corsVaryHeader string = "Vary"
|
||||
corsOriginMatchAll string = "*"
|
||||
)
|
||||
|
||||
func (ch *cors) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
origin := r.Header.Get(corsOriginHeader)
|
||||
if !ch.isOriginAllowed(origin) {
|
||||
if r.Method != corsOptionMethod || ch.ignoreOptions {
|
||||
ch.h.ServeHTTP(w, r)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if r.Method == corsOptionMethod {
|
||||
if ch.ignoreOptions {
|
||||
ch.h.ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
if _, ok := r.Header[corsRequestMethodHeader]; !ok {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
method := r.Header.Get(corsRequestMethodHeader)
|
||||
if !ch.isMatch(method, ch.allowedMethods) {
|
||||
w.WriteHeader(http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
requestHeaders := strings.Split(r.Header.Get(corsRequestHeadersHeader), ",")
|
||||
allowedHeaders := []string{}
|
||||
for _, v := range requestHeaders {
|
||||
canonicalHeader := http.CanonicalHeaderKey(strings.TrimSpace(v))
|
||||
if canonicalHeader == "" || ch.isMatch(canonicalHeader, defaultCorsHeaders) {
|
||||
continue
|
||||
}
|
||||
|
||||
if !ch.isMatch(canonicalHeader, ch.allowedHeaders) {
|
||||
w.WriteHeader(http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
|
||||
allowedHeaders = append(allowedHeaders, canonicalHeader)
|
||||
}
|
||||
|
||||
if len(allowedHeaders) > 0 {
|
||||
w.Header().Set(corsAllowHeadersHeader, strings.Join(allowedHeaders, ","))
|
||||
}
|
||||
|
||||
if ch.maxAge > 0 {
|
||||
w.Header().Set(corsMaxAgeHeader, strconv.Itoa(ch.maxAge))
|
||||
}
|
||||
|
||||
if !ch.isMatch(method, defaultCorsMethods) {
|
||||
w.Header().Set(corsAllowMethodsHeader, method)
|
||||
}
|
||||
} else {
|
||||
if len(ch.exposedHeaders) > 0 {
|
||||
w.Header().Set(corsExposeHeadersHeader, strings.Join(ch.exposedHeaders, ","))
|
||||
}
|
||||
}
|
||||
|
||||
if ch.allowCredentials {
|
||||
w.Header().Set(corsAllowCredentialsHeader, "true")
|
||||
}
|
||||
|
||||
if len(ch.allowedOrigins) > 1 {
|
||||
w.Header().Set(corsVaryHeader, corsOriginHeader)
|
||||
}
|
||||
|
||||
returnOrigin := origin
|
||||
if ch.allowedOriginValidator == nil && len(ch.allowedOrigins) == 0 {
|
||||
returnOrigin = "*"
|
||||
} else {
|
||||
for _, o := range ch.allowedOrigins {
|
||||
// A configuration of * is different than explicitly setting an allowed
|
||||
// origin. Returning arbitrary origin headers in an access control allow
|
||||
// origin header is unsafe and is not required by any use case.
|
||||
if o == corsOriginMatchAll {
|
||||
returnOrigin = "*"
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
w.Header().Set(corsAllowOriginHeader, returnOrigin)
|
||||
|
||||
if r.Method == corsOptionMethod {
|
||||
return
|
||||
}
|
||||
ch.h.ServeHTTP(w, r)
|
||||
}
|
||||
|
||||
// CORS provides Cross-Origin Resource Sharing middleware.
|
||||
// Example:
|
||||
//
|
||||
// import (
|
||||
// "net/http"
|
||||
//
|
||||
// "github.com/gorilla/handlers"
|
||||
// "github.com/gorilla/mux"
|
||||
// )
|
||||
//
|
||||
// func main() {
|
||||
// r := mux.NewRouter()
|
||||
// r.HandleFunc("/users", UserEndpoint)
|
||||
// r.HandleFunc("/projects", ProjectEndpoint)
|
||||
//
|
||||
// // Apply the CORS middleware to our top-level router, with the defaults.
|
||||
// http.ListenAndServe(":8000", handlers.CORS()(r))
|
||||
// }
|
||||
//
|
||||
func CORS(opts ...CORSOption) func(http.Handler) http.Handler {
|
||||
return func(h http.Handler) http.Handler {
|
||||
ch := parseCORSOptions(opts...)
|
||||
ch.h = h
|
||||
return ch
|
||||
}
|
||||
}
|
||||
|
||||
func parseCORSOptions(opts ...CORSOption) *cors {
|
||||
ch := &cors{
|
||||
allowedMethods: defaultCorsMethods,
|
||||
allowedHeaders: defaultCorsHeaders,
|
||||
allowedOrigins: []string{},
|
||||
}
|
||||
|
||||
for _, option := range opts {
|
||||
option(ch)
|
||||
}
|
||||
|
||||
return ch
|
||||
}
|
||||
|
||||
//
|
||||
// Functional options for configuring CORS.
|
||||
//
|
||||
|
||||
// AllowedHeaders adds the provided headers to the list of allowed headers in a
|
||||
// CORS request.
|
||||
// This is an append operation so the headers Accept, Accept-Language,
|
||||
// and Content-Language are always allowed.
|
||||
// Content-Type must be explicitly declared if accepting Content-Types other than
|
||||
// application/x-www-form-urlencoded, multipart/form-data, or text/plain.
|
||||
func AllowedHeaders(headers []string) CORSOption {
|
||||
return func(ch *cors) error {
|
||||
for _, v := range headers {
|
||||
normalizedHeader := http.CanonicalHeaderKey(strings.TrimSpace(v))
|
||||
if normalizedHeader == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
if !ch.isMatch(normalizedHeader, ch.allowedHeaders) {
|
||||
ch.allowedHeaders = append(ch.allowedHeaders, normalizedHeader)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// AllowedMethods can be used to explicitly allow methods in the
|
||||
// Access-Control-Allow-Methods header.
|
||||
// This is a replacement operation so you must also
|
||||
// pass GET, HEAD, and POST if you wish to support those methods.
|
||||
func AllowedMethods(methods []string) CORSOption {
|
||||
return func(ch *cors) error {
|
||||
ch.allowedMethods = []string{}
|
||||
for _, v := range methods {
|
||||
normalizedMethod := strings.ToUpper(strings.TrimSpace(v))
|
||||
if normalizedMethod == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
if !ch.isMatch(normalizedMethod, ch.allowedMethods) {
|
||||
ch.allowedMethods = append(ch.allowedMethods, normalizedMethod)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// AllowedOrigins sets the allowed origins for CORS requests, as used in the
|
||||
// 'Allow-Access-Control-Origin' HTTP header.
|
||||
// Note: Passing in a []string{"*"} will allow any domain.
|
||||
func AllowedOrigins(origins []string) CORSOption {
|
||||
return func(ch *cors) error {
|
||||
for _, v := range origins {
|
||||
if v == corsOriginMatchAll {
|
||||
ch.allowedOrigins = []string{corsOriginMatchAll}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
ch.allowedOrigins = origins
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// AllowedOriginValidator sets a function for evaluating allowed origins in CORS requests, represented by the
|
||||
// 'Allow-Access-Control-Origin' HTTP header.
|
||||
func AllowedOriginValidator(fn OriginValidator) CORSOption {
|
||||
return func(ch *cors) error {
|
||||
ch.allowedOriginValidator = fn
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// ExposeHeaders can be used to specify headers that are available
|
||||
// and will not be stripped out by the user-agent.
|
||||
func ExposedHeaders(headers []string) CORSOption {
|
||||
return func(ch *cors) error {
|
||||
ch.exposedHeaders = []string{}
|
||||
for _, v := range headers {
|
||||
normalizedHeader := http.CanonicalHeaderKey(strings.TrimSpace(v))
|
||||
if normalizedHeader == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
if !ch.isMatch(normalizedHeader, ch.exposedHeaders) {
|
||||
ch.exposedHeaders = append(ch.exposedHeaders, normalizedHeader)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// MaxAge determines the maximum age (in seconds) between preflight requests. A
|
||||
// maximum of 10 minutes is allowed. An age above this value will default to 10
|
||||
// minutes.
|
||||
func MaxAge(age int) CORSOption {
|
||||
return func(ch *cors) error {
|
||||
// Maximum of 10 minutes.
|
||||
if age > 600 {
|
||||
age = 600
|
||||
}
|
||||
|
||||
ch.maxAge = age
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// IgnoreOptions causes the CORS middleware to ignore OPTIONS requests, instead
|
||||
// passing them through to the next handler. This is useful when your application
|
||||
// or framework has a pre-existing mechanism for responding to OPTIONS requests.
|
||||
func IgnoreOptions() CORSOption {
|
||||
return func(ch *cors) error {
|
||||
ch.ignoreOptions = true
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// AllowCredentials can be used to specify that the user agent may pass
|
||||
// authentication details along with the request.
|
||||
func AllowCredentials() CORSOption {
|
||||
return func(ch *cors) error {
|
||||
ch.allowCredentials = true
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func (ch *cors) isOriginAllowed(origin string) bool {
|
||||
if origin == "" {
|
||||
return false
|
||||
}
|
||||
|
||||
if ch.allowedOriginValidator != nil {
|
||||
return ch.allowedOriginValidator(origin)
|
||||
}
|
||||
|
||||
if len(ch.allowedOrigins) == 0 {
|
||||
return true
|
||||
}
|
||||
|
||||
for _, allowedOrigin := range ch.allowedOrigins {
|
||||
if allowedOrigin == origin || allowedOrigin == corsOriginMatchAll {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func (ch *cors) isMatch(needle string, haystack []string) bool {
|
||||
for _, v := range haystack {
|
||||
if v == needle {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
/*
|
||||
Package handlers is a collection of handlers (aka "HTTP middleware") for use
|
||||
with Go's net/http package (or any framework supporting http.Handler).
|
||||
|
||||
The package includes handlers for logging in standardised formats, compressing
|
||||
HTTP responses, validating content types and other useful tools for manipulating
|
||||
requests and responses.
|
||||
*/
|
||||
package handlers
|
||||
+174
@@ -0,0 +1,174 @@
|
||||
// Copyright 2013 The Gorilla Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"sort"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// MethodHandler is an http.Handler that dispatches to a handler whose key in the
|
||||
// MethodHandler's map matches the name of the HTTP request's method, eg: GET
|
||||
//
|
||||
// If the request's method is OPTIONS and OPTIONS is not a key in the map then
|
||||
// the handler responds with a status of 200 and sets the Allow header to a
|
||||
// comma-separated list of available methods.
|
||||
//
|
||||
// If the request's method doesn't match any of its keys the handler responds
|
||||
// with a status of HTTP 405 "Method Not Allowed" and sets the Allow header to a
|
||||
// comma-separated list of available methods.
|
||||
type MethodHandler map[string]http.Handler
|
||||
|
||||
func (h MethodHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
|
||||
if handler, ok := h[req.Method]; ok {
|
||||
handler.ServeHTTP(w, req)
|
||||
} else {
|
||||
allow := []string{}
|
||||
for k := range h {
|
||||
allow = append(allow, k)
|
||||
}
|
||||
sort.Strings(allow)
|
||||
w.Header().Set("Allow", strings.Join(allow, ", "))
|
||||
if req.Method == "OPTIONS" {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
} else {
|
||||
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// responseLogger is wrapper of http.ResponseWriter that keeps track of its HTTP
|
||||
// status code and body size
|
||||
type responseLogger struct {
|
||||
w http.ResponseWriter
|
||||
status int
|
||||
size int
|
||||
}
|
||||
|
||||
func (l *responseLogger) Header() http.Header {
|
||||
return l.w.Header()
|
||||
}
|
||||
|
||||
func (l *responseLogger) Write(b []byte) (int, error) {
|
||||
size, err := l.w.Write(b)
|
||||
l.size += size
|
||||
return size, err
|
||||
}
|
||||
|
||||
func (l *responseLogger) WriteHeader(s int) {
|
||||
l.w.WriteHeader(s)
|
||||
l.status = s
|
||||
}
|
||||
|
||||
func (l *responseLogger) Status() int {
|
||||
return l.status
|
||||
}
|
||||
|
||||
func (l *responseLogger) Size() int {
|
||||
return l.size
|
||||
}
|
||||
|
||||
func (l *responseLogger) Flush() {
|
||||
f, ok := l.w.(http.Flusher)
|
||||
if ok {
|
||||
f.Flush()
|
||||
}
|
||||
}
|
||||
|
||||
type hijackLogger struct {
|
||||
responseLogger
|
||||
}
|
||||
|
||||
func (l *hijackLogger) Hijack() (net.Conn, *bufio.ReadWriter, error) {
|
||||
h := l.responseLogger.w.(http.Hijacker)
|
||||
conn, rw, err := h.Hijack()
|
||||
if err == nil && l.responseLogger.status == 0 {
|
||||
// The status will be StatusSwitchingProtocols if there was no error and
|
||||
// WriteHeader has not been called yet
|
||||
l.responseLogger.status = http.StatusSwitchingProtocols
|
||||
}
|
||||
return conn, rw, err
|
||||
}
|
||||
|
||||
type closeNotifyWriter struct {
|
||||
loggingResponseWriter
|
||||
http.CloseNotifier
|
||||
}
|
||||
|
||||
type hijackCloseNotifier struct {
|
||||
loggingResponseWriter
|
||||
http.Hijacker
|
||||
http.CloseNotifier
|
||||
}
|
||||
|
||||
// isContentType validates the Content-Type header matches the supplied
|
||||
// contentType. That is, its type and subtype match.
|
||||
func isContentType(h http.Header, contentType string) bool {
|
||||
ct := h.Get("Content-Type")
|
||||
if i := strings.IndexRune(ct, ';'); i != -1 {
|
||||
ct = ct[0:i]
|
||||
}
|
||||
return ct == contentType
|
||||
}
|
||||
|
||||
// ContentTypeHandler wraps and returns a http.Handler, validating the request
|
||||
// content type is compatible with the contentTypes list. It writes a HTTP 415
|
||||
// error if that fails.
|
||||
//
|
||||
// Only PUT, POST, and PATCH requests are considered.
|
||||
func ContentTypeHandler(h http.Handler, contentTypes ...string) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if !(r.Method == "PUT" || r.Method == "POST" || r.Method == "PATCH") {
|
||||
h.ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
for _, ct := range contentTypes {
|
||||
if isContentType(r.Header, ct) {
|
||||
h.ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
}
|
||||
http.Error(w, fmt.Sprintf("Unsupported content type %q; expected one of %q", r.Header.Get("Content-Type"), contentTypes), http.StatusUnsupportedMediaType)
|
||||
})
|
||||
}
|
||||
|
||||
const (
|
||||
// HTTPMethodOverrideHeader is a commonly used
|
||||
// http header to override a request method.
|
||||
HTTPMethodOverrideHeader = "X-HTTP-Method-Override"
|
||||
// HTTPMethodOverrideFormKey is a commonly used
|
||||
// HTML form key to override a request method.
|
||||
HTTPMethodOverrideFormKey = "_method"
|
||||
)
|
||||
|
||||
// HTTPMethodOverrideHandler wraps and returns a http.Handler which checks for
|
||||
// the X-HTTP-Method-Override header or the _method form key, and overrides (if
|
||||
// valid) request.Method with its value.
|
||||
//
|
||||
// This is especially useful for HTTP clients that don't support many http verbs.
|
||||
// It isn't secure to override e.g a GET to a POST, so only POST requests are
|
||||
// considered. Likewise, the override method can only be a "write" method: PUT,
|
||||
// PATCH or DELETE.
|
||||
//
|
||||
// Form method takes precedence over header method.
|
||||
func HTTPMethodOverrideHandler(h http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method == "POST" {
|
||||
om := r.FormValue(HTTPMethodOverrideFormKey)
|
||||
if om == "" {
|
||||
om = r.Header.Get(HTTPMethodOverrideHeader)
|
||||
}
|
||||
if om == "PUT" || om == "PATCH" || om == "DELETE" {
|
||||
r.Method = om
|
||||
}
|
||||
}
|
||||
h.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
// +build go1.8
|
||||
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
type loggingResponseWriter interface {
|
||||
commonLoggingResponseWriter
|
||||
http.Pusher
|
||||
}
|
||||
|
||||
func (l *responseLogger) Push(target string, opts *http.PushOptions) error {
|
||||
p, ok := l.w.(http.Pusher)
|
||||
if !ok {
|
||||
return fmt.Errorf("responseLogger does not implement http.Pusher")
|
||||
}
|
||||
return p.Push(target, opts)
|
||||
}
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
// +build !go1.8
|
||||
|
||||
package handlers
|
||||
|
||||
type loggingResponseWriter interface {
|
||||
commonLoggingResponseWriter
|
||||
}
|
||||
+252
@@ -0,0 +1,252 @@
|
||||
// Copyright 2013 The Gorilla Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"time"
|
||||
"unicode/utf8"
|
||||
)
|
||||
|
||||
// Logging
|
||||
|
||||
// FormatterParams is the structure any formatter will be handed when time to log comes
|
||||
type LogFormatterParams struct {
|
||||
Request *http.Request
|
||||
URL url.URL
|
||||
TimeStamp time.Time
|
||||
StatusCode int
|
||||
Size int
|
||||
}
|
||||
|
||||
// LogFormatter gives the signature of the formatter function passed to CustomLoggingHandler
|
||||
type LogFormatter func(writer io.Writer, params LogFormatterParams)
|
||||
|
||||
// loggingHandler is the http.Handler implementation for LoggingHandlerTo and its
|
||||
// friends
|
||||
|
||||
type loggingHandler struct {
|
||||
writer io.Writer
|
||||
handler http.Handler
|
||||
formatter LogFormatter
|
||||
}
|
||||
|
||||
func (h loggingHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
|
||||
t := time.Now()
|
||||
logger := makeLogger(w)
|
||||
url := *req.URL
|
||||
|
||||
h.handler.ServeHTTP(logger, req)
|
||||
|
||||
params := LogFormatterParams{
|
||||
Request: req,
|
||||
URL: url,
|
||||
TimeStamp: t,
|
||||
StatusCode: logger.Status(),
|
||||
Size: logger.Size(),
|
||||
}
|
||||
|
||||
h.formatter(h.writer, params)
|
||||
}
|
||||
|
||||
func makeLogger(w http.ResponseWriter) loggingResponseWriter {
|
||||
var logger loggingResponseWriter = &responseLogger{w: w, status: http.StatusOK}
|
||||
if _, ok := w.(http.Hijacker); ok {
|
||||
logger = &hijackLogger{responseLogger{w: w, status: http.StatusOK}}
|
||||
}
|
||||
h, ok1 := logger.(http.Hijacker)
|
||||
c, ok2 := w.(http.CloseNotifier)
|
||||
if ok1 && ok2 {
|
||||
return hijackCloseNotifier{logger, h, c}
|
||||
}
|
||||
if ok2 {
|
||||
return &closeNotifyWriter{logger, c}
|
||||
}
|
||||
return logger
|
||||
}
|
||||
|
||||
type commonLoggingResponseWriter interface {
|
||||
http.ResponseWriter
|
||||
http.Flusher
|
||||
Status() int
|
||||
Size() int
|
||||
}
|
||||
|
||||
const lowerhex = "0123456789abcdef"
|
||||
|
||||
func appendQuoted(buf []byte, s string) []byte {
|
||||
var runeTmp [utf8.UTFMax]byte
|
||||
for width := 0; len(s) > 0; s = s[width:] {
|
||||
r := rune(s[0])
|
||||
width = 1
|
||||
if r >= utf8.RuneSelf {
|
||||
r, width = utf8.DecodeRuneInString(s)
|
||||
}
|
||||
if width == 1 && r == utf8.RuneError {
|
||||
buf = append(buf, `\x`...)
|
||||
buf = append(buf, lowerhex[s[0]>>4])
|
||||
buf = append(buf, lowerhex[s[0]&0xF])
|
||||
continue
|
||||
}
|
||||
if r == rune('"') || r == '\\' { // always backslashed
|
||||
buf = append(buf, '\\')
|
||||
buf = append(buf, byte(r))
|
||||
continue
|
||||
}
|
||||
if strconv.IsPrint(r) {
|
||||
n := utf8.EncodeRune(runeTmp[:], r)
|
||||
buf = append(buf, runeTmp[:n]...)
|
||||
continue
|
||||
}
|
||||
switch r {
|
||||
case '\a':
|
||||
buf = append(buf, `\a`...)
|
||||
case '\b':
|
||||
buf = append(buf, `\b`...)
|
||||
case '\f':
|
||||
buf = append(buf, `\f`...)
|
||||
case '\n':
|
||||
buf = append(buf, `\n`...)
|
||||
case '\r':
|
||||
buf = append(buf, `\r`...)
|
||||
case '\t':
|
||||
buf = append(buf, `\t`...)
|
||||
case '\v':
|
||||
buf = append(buf, `\v`...)
|
||||
default:
|
||||
switch {
|
||||
case r < ' ':
|
||||
buf = append(buf, `\x`...)
|
||||
buf = append(buf, lowerhex[s[0]>>4])
|
||||
buf = append(buf, lowerhex[s[0]&0xF])
|
||||
case r > utf8.MaxRune:
|
||||
r = 0xFFFD
|
||||
fallthrough
|
||||
case r < 0x10000:
|
||||
buf = append(buf, `\u`...)
|
||||
for s := 12; s >= 0; s -= 4 {
|
||||
buf = append(buf, lowerhex[r>>uint(s)&0xF])
|
||||
}
|
||||
default:
|
||||
buf = append(buf, `\U`...)
|
||||
for s := 28; s >= 0; s -= 4 {
|
||||
buf = append(buf, lowerhex[r>>uint(s)&0xF])
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return buf
|
||||
|
||||
}
|
||||
|
||||
// buildCommonLogLine builds a log entry for req in Apache Common Log Format.
|
||||
// ts is the timestamp with which the entry should be logged.
|
||||
// status and size are used to provide the response HTTP status and size.
|
||||
func buildCommonLogLine(req *http.Request, url url.URL, ts time.Time, status int, size int) []byte {
|
||||
username := "-"
|
||||
if url.User != nil {
|
||||
if name := url.User.Username(); name != "" {
|
||||
username = name
|
||||
}
|
||||
}
|
||||
|
||||
host, _, err := net.SplitHostPort(req.RemoteAddr)
|
||||
|
||||
if err != nil {
|
||||
host = req.RemoteAddr
|
||||
}
|
||||
|
||||
uri := req.RequestURI
|
||||
|
||||
// Requests using the CONNECT method over HTTP/2.0 must use
|
||||
// the authority field (aka r.Host) to identify the target.
|
||||
// Refer: https://httpwg.github.io/specs/rfc7540.html#CONNECT
|
||||
if req.ProtoMajor == 2 && req.Method == "CONNECT" {
|
||||
uri = req.Host
|
||||
}
|
||||
if uri == "" {
|
||||
uri = url.RequestURI()
|
||||
}
|
||||
|
||||
buf := make([]byte, 0, 3*(len(host)+len(username)+len(req.Method)+len(uri)+len(req.Proto)+50)/2)
|
||||
buf = append(buf, host...)
|
||||
buf = append(buf, " - "...)
|
||||
buf = append(buf, username...)
|
||||
buf = append(buf, " ["...)
|
||||
buf = append(buf, ts.Format("02/Jan/2006:15:04:05 -0700")...)
|
||||
buf = append(buf, `] "`...)
|
||||
buf = append(buf, req.Method...)
|
||||
buf = append(buf, " "...)
|
||||
buf = appendQuoted(buf, uri)
|
||||
buf = append(buf, " "...)
|
||||
buf = append(buf, req.Proto...)
|
||||
buf = append(buf, `" `...)
|
||||
buf = append(buf, strconv.Itoa(status)...)
|
||||
buf = append(buf, " "...)
|
||||
buf = append(buf, strconv.Itoa(size)...)
|
||||
return buf
|
||||
}
|
||||
|
||||
// writeLog writes a log entry for req to w in Apache Common Log Format.
|
||||
// ts is the timestamp with which the entry should be logged.
|
||||
// status and size are used to provide the response HTTP status and size.
|
||||
func writeLog(writer io.Writer, params LogFormatterParams) {
|
||||
buf := buildCommonLogLine(params.Request, params.URL, params.TimeStamp, params.StatusCode, params.Size)
|
||||
buf = append(buf, '\n')
|
||||
writer.Write(buf)
|
||||
}
|
||||
|
||||
// writeCombinedLog writes a log entry for req to w in Apache Combined Log Format.
|
||||
// ts is the timestamp with which the entry should be logged.
|
||||
// status and size are used to provide the response HTTP status and size.
|
||||
func writeCombinedLog(writer io.Writer, params LogFormatterParams) {
|
||||
buf := buildCommonLogLine(params.Request, params.URL, params.TimeStamp, params.StatusCode, params.Size)
|
||||
buf = append(buf, ` "`...)
|
||||
buf = appendQuoted(buf, params.Request.Referer())
|
||||
buf = append(buf, `" "`...)
|
||||
buf = appendQuoted(buf, params.Request.UserAgent())
|
||||
buf = append(buf, '"', '\n')
|
||||
writer.Write(buf)
|
||||
}
|
||||
|
||||
// CombinedLoggingHandler return a http.Handler that wraps h and logs requests to out in
|
||||
// Apache Combined Log Format.
|
||||
//
|
||||
// See http://httpd.apache.org/docs/2.2/logs.html#combined for a description of this format.
|
||||
//
|
||||
// LoggingHandler always sets the ident field of the log to -
|
||||
func CombinedLoggingHandler(out io.Writer, h http.Handler) http.Handler {
|
||||
return loggingHandler{out, h, writeCombinedLog}
|
||||
}
|
||||
|
||||
// LoggingHandler return a http.Handler that wraps h and logs requests to out in
|
||||
// Apache Common Log Format (CLF).
|
||||
//
|
||||
// See http://httpd.apache.org/docs/2.2/logs.html#common for a description of this format.
|
||||
//
|
||||
// LoggingHandler always sets the ident field of the log to -
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// r := mux.NewRouter()
|
||||
// r.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
||||
// w.Write([]byte("This is a catch-all route"))
|
||||
// })
|
||||
// loggedRouter := handlers.LoggingHandler(os.Stdout, r)
|
||||
// http.ListenAndServe(":1123", loggedRouter)
|
||||
//
|
||||
func LoggingHandler(out io.Writer, h http.Handler) http.Handler {
|
||||
return loggingHandler{out, h, writeLog}
|
||||
}
|
||||
|
||||
// CustomLoggingHandler provides a way to supply a custom log formatter
|
||||
// while taking advantage of the mechanisms in this package
|
||||
func CustomLoggingHandler(out io.Writer, h http.Handler, f LogFormatter) http.Handler {
|
||||
return loggingHandler{out, h, f}
|
||||
}
|
||||
+120
@@ -0,0 +1,120 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var (
|
||||
// De-facto standard header keys.
|
||||
xForwardedFor = http.CanonicalHeaderKey("X-Forwarded-For")
|
||||
xForwardedHost = http.CanonicalHeaderKey("X-Forwarded-Host")
|
||||
xForwardedProto = http.CanonicalHeaderKey("X-Forwarded-Proto")
|
||||
xForwardedScheme = http.CanonicalHeaderKey("X-Forwarded-Scheme")
|
||||
xRealIP = http.CanonicalHeaderKey("X-Real-IP")
|
||||
)
|
||||
|
||||
var (
|
||||
// RFC7239 defines a new "Forwarded: " header designed to replace the
|
||||
// existing use of X-Forwarded-* headers.
|
||||
// e.g. Forwarded: for=192.0.2.60;proto=https;by=203.0.113.43
|
||||
forwarded = http.CanonicalHeaderKey("Forwarded")
|
||||
// Allows for a sub-match of the first value after 'for=' to the next
|
||||
// comma, semi-colon or space. The match is case-insensitive.
|
||||
forRegex = regexp.MustCompile(`(?i)(?:for=)([^(;|,| )]+)`)
|
||||
// Allows for a sub-match for the first instance of scheme (http|https)
|
||||
// prefixed by 'proto='. The match is case-insensitive.
|
||||
protoRegex = regexp.MustCompile(`(?i)(?:proto=)(https|http)`)
|
||||
)
|
||||
|
||||
// ProxyHeaders inspects common reverse proxy headers and sets the corresponding
|
||||
// fields in the HTTP request struct. These are X-Forwarded-For and X-Real-IP
|
||||
// for the remote (client) IP address, X-Forwarded-Proto or X-Forwarded-Scheme
|
||||
// for the scheme (http|https) and the RFC7239 Forwarded header, which may
|
||||
// include both client IPs and schemes.
|
||||
//
|
||||
// NOTE: This middleware should only be used when behind a reverse
|
||||
// proxy like nginx, HAProxy or Apache. Reverse proxies that don't (or are
|
||||
// configured not to) strip these headers from client requests, or where these
|
||||
// headers are accepted "as is" from a remote client (e.g. when Go is not behind
|
||||
// a proxy), can manifest as a vulnerability if your application uses these
|
||||
// headers for validating the 'trustworthiness' of a request.
|
||||
func ProxyHeaders(h http.Handler) http.Handler {
|
||||
fn := func(w http.ResponseWriter, r *http.Request) {
|
||||
// Set the remote IP with the value passed from the proxy.
|
||||
if fwd := getIP(r); fwd != "" {
|
||||
r.RemoteAddr = fwd
|
||||
}
|
||||
|
||||
// Set the scheme (proto) with the value passed from the proxy.
|
||||
if scheme := getScheme(r); scheme != "" {
|
||||
r.URL.Scheme = scheme
|
||||
}
|
||||
// Set the host with the value passed by the proxy
|
||||
if r.Header.Get(xForwardedHost) != "" {
|
||||
r.Host = r.Header.Get(xForwardedHost)
|
||||
}
|
||||
// Call the next handler in the chain.
|
||||
h.ServeHTTP(w, r)
|
||||
}
|
||||
|
||||
return http.HandlerFunc(fn)
|
||||
}
|
||||
|
||||
// getIP retrieves the IP from the X-Forwarded-For, X-Real-IP and RFC7239
|
||||
// Forwarded headers (in that order).
|
||||
func getIP(r *http.Request) string {
|
||||
var addr string
|
||||
|
||||
if fwd := r.Header.Get(xForwardedFor); fwd != "" {
|
||||
// Only grab the first (client) address. Note that '192.168.0.1,
|
||||
// 10.1.1.1' is a valid key for X-Forwarded-For where addresses after
|
||||
// the first may represent forwarding proxies earlier in the chain.
|
||||
s := strings.Index(fwd, ", ")
|
||||
if s == -1 {
|
||||
s = len(fwd)
|
||||
}
|
||||
addr = fwd[:s]
|
||||
} else if fwd := r.Header.Get(xRealIP); fwd != "" {
|
||||
// X-Real-IP should only contain one IP address (the client making the
|
||||
// request).
|
||||
addr = fwd
|
||||
} else if fwd := r.Header.Get(forwarded); fwd != "" {
|
||||
// match should contain at least two elements if the protocol was
|
||||
// specified in the Forwarded header. The first element will always be
|
||||
// the 'for=' capture, which we ignore. In the case of multiple IP
|
||||
// addresses (for=8.8.8.8, 8.8.4.4,172.16.1.20 is valid) we only
|
||||
// extract the first, which should be the client IP.
|
||||
if match := forRegex.FindStringSubmatch(fwd); len(match) > 1 {
|
||||
// IPv6 addresses in Forwarded headers are quoted-strings. We strip
|
||||
// these quotes.
|
||||
addr = strings.Trim(match[1], `"`)
|
||||
}
|
||||
}
|
||||
|
||||
return addr
|
||||
}
|
||||
|
||||
// getScheme retrieves the scheme from the X-Forwarded-Proto and RFC7239
|
||||
// Forwarded headers (in that order).
|
||||
func getScheme(r *http.Request) string {
|
||||
var scheme string
|
||||
|
||||
// Retrieve the scheme from X-Forwarded-Proto.
|
||||
if proto := r.Header.Get(xForwardedProto); proto != "" {
|
||||
scheme = strings.ToLower(proto)
|
||||
} else if proto = r.Header.Get(xForwardedScheme); proto != "" {
|
||||
scheme = strings.ToLower(proto)
|
||||
} else if proto = r.Header.Get(forwarded); proto != "" {
|
||||
// match should contain at least two elements if the protocol was
|
||||
// specified in the Forwarded header. The first element will always be
|
||||
// the 'proto=' capture, which we ignore. In the case of multiple proto
|
||||
// parameters (invalid) we only extract the first.
|
||||
if match := protoRegex.FindStringSubmatch(proto); len(match) > 1 {
|
||||
scheme = strings.ToLower(match[1])
|
||||
}
|
||||
}
|
||||
|
||||
return scheme
|
||||
}
|
||||
+91
@@ -0,0 +1,91 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"log"
|
||||
"net/http"
|
||||
"runtime/debug"
|
||||
)
|
||||
|
||||
// RecoveryHandlerLogger is an interface used by the recovering handler to print logs.
|
||||
type RecoveryHandlerLogger interface {
|
||||
Println(...interface{})
|
||||
}
|
||||
|
||||
type recoveryHandler struct {
|
||||
handler http.Handler
|
||||
logger RecoveryHandlerLogger
|
||||
printStack bool
|
||||
}
|
||||
|
||||
// RecoveryOption provides a functional approach to define
|
||||
// configuration for a handler; such as setting the logging
|
||||
// whether or not to print strack traces on panic.
|
||||
type RecoveryOption func(http.Handler)
|
||||
|
||||
func parseRecoveryOptions(h http.Handler, opts ...RecoveryOption) http.Handler {
|
||||
for _, option := range opts {
|
||||
option(h)
|
||||
}
|
||||
|
||||
return h
|
||||
}
|
||||
|
||||
// RecoveryHandler is HTTP middleware that recovers from a panic,
|
||||
// logs the panic, writes http.StatusInternalServerError, and
|
||||
// continues to the next handler.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// r := mux.NewRouter()
|
||||
// r.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
||||
// panic("Unexpected error!")
|
||||
// })
|
||||
//
|
||||
// http.ListenAndServe(":1123", handlers.RecoveryHandler()(r))
|
||||
func RecoveryHandler(opts ...RecoveryOption) func(h http.Handler) http.Handler {
|
||||
return func(h http.Handler) http.Handler {
|
||||
r := &recoveryHandler{handler: h}
|
||||
return parseRecoveryOptions(r, opts...)
|
||||
}
|
||||
}
|
||||
|
||||
// RecoveryLogger is a functional option to override
|
||||
// the default logger
|
||||
func RecoveryLogger(logger RecoveryHandlerLogger) RecoveryOption {
|
||||
return func(h http.Handler) {
|
||||
r := h.(*recoveryHandler)
|
||||
r.logger = logger
|
||||
}
|
||||
}
|
||||
|
||||
// PrintRecoveryStack is a functional option to enable
|
||||
// or disable printing stack traces on panic.
|
||||
func PrintRecoveryStack(print bool) RecoveryOption {
|
||||
return func(h http.Handler) {
|
||||
r := h.(*recoveryHandler)
|
||||
r.printStack = print
|
||||
}
|
||||
}
|
||||
|
||||
func (h recoveryHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
|
||||
defer func() {
|
||||
if err := recover(); err != nil {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
h.log(err)
|
||||
}
|
||||
}()
|
||||
|
||||
h.handler.ServeHTTP(w, req)
|
||||
}
|
||||
|
||||
func (h recoveryHandler) log(v ...interface{}) {
|
||||
if h.logger != nil {
|
||||
h.logger.Println(v...)
|
||||
} else {
|
||||
log.Println(v...)
|
||||
}
|
||||
|
||||
if h.printStack {
|
||||
debug.PrintStack()
|
||||
}
|
||||
}
|
||||
-1
Submodule vendor/github.com/gorilla/mux deleted from f3ff42f93a
+23
@@ -0,0 +1,23 @@
|
||||
language: go
|
||||
sudo: false
|
||||
|
||||
matrix:
|
||||
include:
|
||||
- go: 1.5.x
|
||||
- go: 1.6.x
|
||||
- go: 1.7.x
|
||||
- go: 1.8.x
|
||||
- go: 1.9.x
|
||||
- go: 1.10.x
|
||||
- go: tip
|
||||
allow_failures:
|
||||
- go: tip
|
||||
|
||||
install:
|
||||
- # Skip
|
||||
|
||||
script:
|
||||
- go get -t -v ./...
|
||||
- diff -u <(echo -n) <(gofmt -d .)
|
||||
- go tool vet .
|
||||
- go test -v -race ./...
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
**What version of Go are you running?** (Paste the output of `go version`)
|
||||
|
||||
|
||||
**What version of gorilla/mux are you at?** (Paste the output of `git rev-parse HEAD` inside `$GOPATH/src/github.com/gorilla/mux`)
|
||||
|
||||
|
||||
**Describe your problem** (and what you have tried so far)
|
||||
|
||||
|
||||
**Paste a minimal, runnable, reproduction of your issue below** (use backticks to format it)
|
||||
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
Copyright (c) 2012 Rodrigo Moraes. 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.
|
||||
* Neither the name of Google Inc. nor the names of its
|
||||
contributors may 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
|
||||
OWNER 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.
|
||||
+649
@@ -0,0 +1,649 @@
|
||||
# gorilla/mux
|
||||
|
||||
[](https://godoc.org/github.com/gorilla/mux)
|
||||
[](https://travis-ci.org/gorilla/mux)
|
||||
[](https://sourcegraph.com/github.com/gorilla/mux?badge)
|
||||
|
||||

|
||||
|
||||
http://www.gorillatoolkit.org/pkg/mux
|
||||
|
||||
Package `gorilla/mux` implements a request router and dispatcher for matching incoming requests to
|
||||
their respective handler.
|
||||
|
||||
The name mux stands for "HTTP request multiplexer". Like the standard `http.ServeMux`, `mux.Router` matches incoming requests against a list of registered routes and calls a handler for the route that matches the URL or other conditions. The main features are:
|
||||
|
||||
* It implements the `http.Handler` interface so it is compatible with the standard `http.ServeMux`.
|
||||
* Requests can be matched based on URL host, path, path prefix, schemes, header and query values, HTTP methods or using custom matchers.
|
||||
* URL hosts, paths and query values can have variables with an optional regular expression.
|
||||
* Registered URLs can be built, or "reversed", which helps maintaining references to resources.
|
||||
* Routes can be used as subrouters: nested routes are only tested if the parent route matches. This is useful to define groups of routes that share common conditions like a host, a path prefix or other repeated attributes. As a bonus, this optimizes request matching.
|
||||
|
||||
---
|
||||
|
||||
* [Install](#install)
|
||||
* [Examples](#examples)
|
||||
* [Matching Routes](#matching-routes)
|
||||
* [Static Files](#static-files)
|
||||
* [Registered URLs](#registered-urls)
|
||||
* [Walking Routes](#walking-routes)
|
||||
* [Graceful Shutdown](#graceful-shutdown)
|
||||
* [Middleware](#middleware)
|
||||
* [Testing Handlers](#testing-handlers)
|
||||
* [Full Example](#full-example)
|
||||
|
||||
---
|
||||
|
||||
## Install
|
||||
|
||||
With a [correctly configured](https://golang.org/doc/install#testing) Go toolchain:
|
||||
|
||||
```sh
|
||||
go get -u github.com/gorilla/mux
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
Let's start registering a couple of URL paths and handlers:
|
||||
|
||||
```go
|
||||
func main() {
|
||||
r := mux.NewRouter()
|
||||
r.HandleFunc("/", HomeHandler)
|
||||
r.HandleFunc("/products", ProductsHandler)
|
||||
r.HandleFunc("/articles", ArticlesHandler)
|
||||
http.Handle("/", r)
|
||||
}
|
||||
```
|
||||
|
||||
Here we register three routes mapping URL paths to handlers. This is equivalent to how `http.HandleFunc()` works: if an incoming request URL matches one of the paths, the corresponding handler is called passing (`http.ResponseWriter`, `*http.Request`) as parameters.
|
||||
|
||||
Paths can have variables. They are defined using the format `{name}` or `{name:pattern}`. If a regular expression pattern is not defined, the matched variable will be anything until the next slash. For example:
|
||||
|
||||
```go
|
||||
r := mux.NewRouter()
|
||||
r.HandleFunc("/products/{key}", ProductHandler)
|
||||
r.HandleFunc("/articles/{category}/", ArticlesCategoryHandler)
|
||||
r.HandleFunc("/articles/{category}/{id:[0-9]+}", ArticleHandler)
|
||||
```
|
||||
|
||||
The names are used to create a map of route variables which can be retrieved calling `mux.Vars()`:
|
||||
|
||||
```go
|
||||
func ArticlesCategoryHandler(w http.ResponseWriter, r *http.Request) {
|
||||
vars := mux.Vars(r)
|
||||
w.WriteHeader(http.StatusOK)
|
||||
fmt.Fprintf(w, "Category: %v\n", vars["category"])
|
||||
}
|
||||
```
|
||||
|
||||
And this is all you need to know about the basic usage. More advanced options are explained below.
|
||||
|
||||
### Matching Routes
|
||||
|
||||
Routes can also be restricted to a domain or subdomain. Just define a host pattern to be matched. They can also have variables:
|
||||
|
||||
```go
|
||||
r := mux.NewRouter()
|
||||
// Only matches if domain is "www.example.com".
|
||||
r.Host("www.example.com")
|
||||
// Matches a dynamic subdomain.
|
||||
r.Host("{subdomain:[a-z]+}.domain.com")
|
||||
```
|
||||
|
||||
There are several other matchers that can be added. To match path prefixes:
|
||||
|
||||
```go
|
||||
r.PathPrefix("/products/")
|
||||
```
|
||||
|
||||
...or HTTP methods:
|
||||
|
||||
```go
|
||||
r.Methods("GET", "POST")
|
||||
```
|
||||
|
||||
...or URL schemes:
|
||||
|
||||
```go
|
||||
r.Schemes("https")
|
||||
```
|
||||
|
||||
...or header values:
|
||||
|
||||
```go
|
||||
r.Headers("X-Requested-With", "XMLHttpRequest")
|
||||
```
|
||||
|
||||
...or query values:
|
||||
|
||||
```go
|
||||
r.Queries("key", "value")
|
||||
```
|
||||
|
||||
...or to use a custom matcher function:
|
||||
|
||||
```go
|
||||
r.MatcherFunc(func(r *http.Request, rm *RouteMatch) bool {
|
||||
return r.ProtoMajor == 0
|
||||
})
|
||||
```
|
||||
|
||||
...and finally, it is possible to combine several matchers in a single route:
|
||||
|
||||
```go
|
||||
r.HandleFunc("/products", ProductsHandler).
|
||||
Host("www.example.com").
|
||||
Methods("GET").
|
||||
Schemes("http")
|
||||
```
|
||||
|
||||
Routes are tested in the order they were added to the router. If two routes match, the first one wins:
|
||||
|
||||
```go
|
||||
r := mux.NewRouter()
|
||||
r.HandleFunc("/specific", specificHandler)
|
||||
r.PathPrefix("/").Handler(catchAllHandler)
|
||||
```
|
||||
|
||||
Setting the same matching conditions again and again can be boring, so we have a way to group several routes that share the same requirements. We call it "subrouting".
|
||||
|
||||
For example, let's say we have several URLs that should only match when the host is `www.example.com`. Create a route for that host and get a "subrouter" from it:
|
||||
|
||||
```go
|
||||
r := mux.NewRouter()
|
||||
s := r.Host("www.example.com").Subrouter()
|
||||
```
|
||||
|
||||
Then register routes in the subrouter:
|
||||
|
||||
```go
|
||||
s.HandleFunc("/products/", ProductsHandler)
|
||||
s.HandleFunc("/products/{key}", ProductHandler)
|
||||
s.HandleFunc("/articles/{category}/{id:[0-9]+}", ArticleHandler)
|
||||
```
|
||||
|
||||
The three URL paths we registered above will only be tested if the domain is `www.example.com`, because the subrouter is tested first. This is not only convenient, but also optimizes request matching. You can create subrouters combining any attribute matchers accepted by a route.
|
||||
|
||||
Subrouters can be used to create domain or path "namespaces": you define subrouters in a central place and then parts of the app can register its paths relatively to a given subrouter.
|
||||
|
||||
There's one more thing about subroutes. When a subrouter has a path prefix, the inner routes use it as base for their paths:
|
||||
|
||||
```go
|
||||
r := mux.NewRouter()
|
||||
s := r.PathPrefix("/products").Subrouter()
|
||||
// "/products/"
|
||||
s.HandleFunc("/", ProductsHandler)
|
||||
// "/products/{key}/"
|
||||
s.HandleFunc("/{key}/", ProductHandler)
|
||||
// "/products/{key}/details"
|
||||
s.HandleFunc("/{key}/details", ProductDetailsHandler)
|
||||
```
|
||||
|
||||
|
||||
### Static Files
|
||||
|
||||
Note that the path provided to `PathPrefix()` represents a "wildcard": calling
|
||||
`PathPrefix("/static/").Handler(...)` means that the handler will be passed any
|
||||
request that matches "/static/\*". This makes it easy to serve static files with mux:
|
||||
|
||||
```go
|
||||
func main() {
|
||||
var dir string
|
||||
|
||||
flag.StringVar(&dir, "dir", ".", "the directory to serve files from. Defaults to the current dir")
|
||||
flag.Parse()
|
||||
r := mux.NewRouter()
|
||||
|
||||
// This will serve files under http://localhost:8000/static/<filename>
|
||||
r.PathPrefix("/static/").Handler(http.StripPrefix("/static/", http.FileServer(http.Dir(dir))))
|
||||
|
||||
srv := &http.Server{
|
||||
Handler: r,
|
||||
Addr: "127.0.0.1:8000",
|
||||
// Good practice: enforce timeouts for servers you create!
|
||||
WriteTimeout: 15 * time.Second,
|
||||
ReadTimeout: 15 * time.Second,
|
||||
}
|
||||
|
||||
log.Fatal(srv.ListenAndServe())
|
||||
}
|
||||
```
|
||||
|
||||
### Registered URLs
|
||||
|
||||
Now let's see how to build registered URLs.
|
||||
|
||||
Routes can be named. All routes that define a name can have their URLs built, or "reversed". We define a name calling `Name()` on a route. For example:
|
||||
|
||||
```go
|
||||
r := mux.NewRouter()
|
||||
r.HandleFunc("/articles/{category}/{id:[0-9]+}", ArticleHandler).
|
||||
Name("article")
|
||||
```
|
||||
|
||||
To build a URL, get the route and call the `URL()` method, passing a sequence of key/value pairs for the route variables. For the previous route, we would do:
|
||||
|
||||
```go
|
||||
url, err := r.Get("article").URL("category", "technology", "id", "42")
|
||||
```
|
||||
|
||||
...and the result will be a `url.URL` with the following path:
|
||||
|
||||
```
|
||||
"/articles/technology/42"
|
||||
```
|
||||
|
||||
This also works for host and query value variables:
|
||||
|
||||
```go
|
||||
r := mux.NewRouter()
|
||||
r.Host("{subdomain}.domain.com").
|
||||
Path("/articles/{category}/{id:[0-9]+}").
|
||||
Queries("filter", "{filter}").
|
||||
HandlerFunc(ArticleHandler).
|
||||
Name("article")
|
||||
|
||||
// url.String() will be "http://news.domain.com/articles/technology/42?filter=gorilla"
|
||||
url, err := r.Get("article").URL("subdomain", "news",
|
||||
"category", "technology",
|
||||
"id", "42",
|
||||
"filter", "gorilla")
|
||||
```
|
||||
|
||||
All variables defined in the route are required, and their values must conform to the corresponding patterns. These requirements guarantee that a generated URL will always match a registered route -- the only exception is for explicitly defined "build-only" routes which never match.
|
||||
|
||||
Regex support also exists for matching Headers within a route. For example, we could do:
|
||||
|
||||
```go
|
||||
r.HeadersRegexp("Content-Type", "application/(text|json)")
|
||||
```
|
||||
|
||||
...and the route will match both requests with a Content-Type of `application/json` as well as `application/text`
|
||||
|
||||
There's also a way to build only the URL host or path for a route: use the methods `URLHost()` or `URLPath()` instead. For the previous route, we would do:
|
||||
|
||||
```go
|
||||
// "http://news.domain.com/"
|
||||
host, err := r.Get("article").URLHost("subdomain", "news")
|
||||
|
||||
// "/articles/technology/42"
|
||||
path, err := r.Get("article").URLPath("category", "technology", "id", "42")
|
||||
```
|
||||
|
||||
And if you use subrouters, host and path defined separately can be built as well:
|
||||
|
||||
```go
|
||||
r := mux.NewRouter()
|
||||
s := r.Host("{subdomain}.domain.com").Subrouter()
|
||||
s.Path("/articles/{category}/{id:[0-9]+}").
|
||||
HandlerFunc(ArticleHandler).
|
||||
Name("article")
|
||||
|
||||
// "http://news.domain.com/articles/technology/42"
|
||||
url, err := r.Get("article").URL("subdomain", "news",
|
||||
"category", "technology",
|
||||
"id", "42")
|
||||
```
|
||||
|
||||
### Walking Routes
|
||||
|
||||
The `Walk` function on `mux.Router` can be used to visit all of the routes that are registered on a router. For example,
|
||||
the following prints all of the registered routes:
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
)
|
||||
|
||||
func handler(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
func main() {
|
||||
r := mux.NewRouter()
|
||||
r.HandleFunc("/", handler)
|
||||
r.HandleFunc("/products", handler).Methods("POST")
|
||||
r.HandleFunc("/articles", handler).Methods("GET")
|
||||
r.HandleFunc("/articles/{id}", handler).Methods("GET", "PUT")
|
||||
r.HandleFunc("/authors", handler).Queries("surname", "{surname}")
|
||||
err := r.Walk(func(route *mux.Route, router *mux.Router, ancestors []*mux.Route) error {
|
||||
pathTemplate, err := route.GetPathTemplate()
|
||||
if err == nil {
|
||||
fmt.Println("ROUTE:", pathTemplate)
|
||||
}
|
||||
pathRegexp, err := route.GetPathRegexp()
|
||||
if err == nil {
|
||||
fmt.Println("Path regexp:", pathRegexp)
|
||||
}
|
||||
queriesTemplates, err := route.GetQueriesTemplates()
|
||||
if err == nil {
|
||||
fmt.Println("Queries templates:", strings.Join(queriesTemplates, ","))
|
||||
}
|
||||
queriesRegexps, err := route.GetQueriesRegexp()
|
||||
if err == nil {
|
||||
fmt.Println("Queries regexps:", strings.Join(queriesRegexps, ","))
|
||||
}
|
||||
methods, err := route.GetMethods()
|
||||
if err == nil {
|
||||
fmt.Println("Methods:", strings.Join(methods, ","))
|
||||
}
|
||||
fmt.Println()
|
||||
return nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
}
|
||||
|
||||
http.Handle("/", r)
|
||||
}
|
||||
```
|
||||
|
||||
### Graceful Shutdown
|
||||
|
||||
Go 1.8 introduced the ability to [gracefully shutdown](https://golang.org/doc/go1.8#http_shutdown) a `*http.Server`. Here's how to do that alongside `mux`:
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"flag"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/signal"
|
||||
"time"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
)
|
||||
|
||||
func main() {
|
||||
var wait time.Duration
|
||||
flag.DurationVar(&wait, "graceful-timeout", time.Second * 15, "the duration for which the server gracefully wait for existing connections to finish - e.g. 15s or 1m")
|
||||
flag.Parse()
|
||||
|
||||
r := mux.NewRouter()
|
||||
// Add your routes as needed
|
||||
|
||||
srv := &http.Server{
|
||||
Addr: "0.0.0.0:8080",
|
||||
// Good practice to set timeouts to avoid Slowloris attacks.
|
||||
WriteTimeout: time.Second * 15,
|
||||
ReadTimeout: time.Second * 15,
|
||||
IdleTimeout: time.Second * 60,
|
||||
Handler: r, // Pass our instance of gorilla/mux in.
|
||||
}
|
||||
|
||||
// Run our server in a goroutine so that it doesn't block.
|
||||
go func() {
|
||||
if err := srv.ListenAndServe(); err != nil {
|
||||
log.Println(err)
|
||||
}
|
||||
}()
|
||||
|
||||
c := make(chan os.Signal, 1)
|
||||
// We'll accept graceful shutdowns when quit via SIGINT (Ctrl+C)
|
||||
// SIGKILL, SIGQUIT or SIGTERM (Ctrl+/) will not be caught.
|
||||
signal.Notify(c, os.Interrupt)
|
||||
|
||||
// Block until we receive our signal.
|
||||
<-c
|
||||
|
||||
// Create a deadline to wait for.
|
||||
ctx, cancel := context.WithTimeout(context.Background(), wait)
|
||||
defer cancel()
|
||||
// Doesn't block if no connections, but will otherwise wait
|
||||
// until the timeout deadline.
|
||||
srv.Shutdown(ctx)
|
||||
// Optionally, you could run srv.Shutdown in a goroutine and block on
|
||||
// <-ctx.Done() if your application should wait for other services
|
||||
// to finalize based on context cancellation.
|
||||
log.Println("shutting down")
|
||||
os.Exit(0)
|
||||
}
|
||||
```
|
||||
|
||||
### Middleware
|
||||
|
||||
Mux supports the addition of middlewares to a [Router](https://godoc.org/github.com/gorilla/mux#Router), which are executed in the order they are added if a match is found, including its subrouters.
|
||||
Middlewares are (typically) small pieces of code which take one request, do something with it, and pass it down to another middleware or the final handler. Some common use cases for middleware are request logging, header manipulation, or `ResponseWriter` hijacking.
|
||||
|
||||
Mux middlewares are defined using the de facto standard type:
|
||||
|
||||
```go
|
||||
type MiddlewareFunc func(http.Handler) http.Handler
|
||||
```
|
||||
|
||||
Typically, the returned handler is a closure which does something with the http.ResponseWriter and http.Request passed to it, and then calls the handler passed as parameter to the MiddlewareFunc. This takes advantage of closures being able access variables from the context where they are created, while retaining the signature enforced by the receivers.
|
||||
|
||||
A very basic middleware which logs the URI of the request being handled could be written as:
|
||||
|
||||
```go
|
||||
func loggingMiddleware(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
// Do stuff here
|
||||
log.Println(r.RequestURI)
|
||||
// Call the next handler, which can be another middleware in the chain, or the final handler.
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
Middlewares can be added to a router using `Router.Use()`:
|
||||
|
||||
```go
|
||||
r := mux.NewRouter()
|
||||
r.HandleFunc("/", handler)
|
||||
r.Use(loggingMiddleware)
|
||||
```
|
||||
|
||||
A more complex authentication middleware, which maps session token to users, could be written as:
|
||||
|
||||
```go
|
||||
// Define our struct
|
||||
type authenticationMiddleware struct {
|
||||
tokenUsers map[string]string
|
||||
}
|
||||
|
||||
// Initialize it somewhere
|
||||
func (amw *authenticationMiddleware) Populate() {
|
||||
amw.tokenUsers["00000000"] = "user0"
|
||||
amw.tokenUsers["aaaaaaaa"] = "userA"
|
||||
amw.tokenUsers["05f717e5"] = "randomUser"
|
||||
amw.tokenUsers["deadbeef"] = "user0"
|
||||
}
|
||||
|
||||
// Middleware function, which will be called for each request
|
||||
func (amw *authenticationMiddleware) Middleware(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
token := r.Header.Get("X-Session-Token")
|
||||
|
||||
if user, found := amw.tokenUsers[token]; found {
|
||||
// We found the token in our map
|
||||
log.Printf("Authenticated user %s\n", user)
|
||||
// Pass down the request to the next middleware (or final handler)
|
||||
next.ServeHTTP(w, r)
|
||||
} else {
|
||||
// Write an error and stop the handler chain
|
||||
http.Error(w, "Forbidden", http.StatusForbidden)
|
||||
}
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
```go
|
||||
r := mux.NewRouter()
|
||||
r.HandleFunc("/", handler)
|
||||
|
||||
amw := authenticationMiddleware{}
|
||||
amw.Populate()
|
||||
|
||||
r.Use(amw.Middleware)
|
||||
```
|
||||
|
||||
Note: The handler chain will be stopped if your middleware doesn't call `next.ServeHTTP()` with the corresponding parameters. This can be used to abort a request if the middleware writer wants to. Middlewares _should_ write to `ResponseWriter` if they _are_ going to terminate the request, and they _should not_ write to `ResponseWriter` if they _are not_ going to terminate it.
|
||||
|
||||
### Testing Handlers
|
||||
|
||||
Testing handlers in a Go web application is straightforward, and _mux_ doesn't complicate this any further. Given two files: `endpoints.go` and `endpoints_test.go`, here's how we'd test an application using _mux_.
|
||||
|
||||
First, our simple HTTP handler:
|
||||
|
||||
```go
|
||||
// endpoints.go
|
||||
package main
|
||||
|
||||
func HealthCheckHandler(w http.ResponseWriter, r *http.Request) {
|
||||
// A very simple health check.
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
|
||||
// In the future we could report back on the status of our DB, or our cache
|
||||
// (e.g. Redis) by performing a simple PING, and include them in the response.
|
||||
io.WriteString(w, `{"alive": true}`)
|
||||
}
|
||||
|
||||
func main() {
|
||||
r := mux.NewRouter()
|
||||
r.HandleFunc("/health", HealthCheckHandler)
|
||||
|
||||
log.Fatal(http.ListenAndServe("localhost:8080", r))
|
||||
}
|
||||
```
|
||||
|
||||
Our test code:
|
||||
|
||||
```go
|
||||
// endpoints_test.go
|
||||
package main
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestHealthCheckHandler(t *testing.T) {
|
||||
// Create a request to pass to our handler. We don't have any query parameters for now, so we'll
|
||||
// pass 'nil' as the third parameter.
|
||||
req, err := http.NewRequest("GET", "/health", nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// We create a ResponseRecorder (which satisfies http.ResponseWriter) to record the response.
|
||||
rr := httptest.NewRecorder()
|
||||
handler := http.HandlerFunc(HealthCheckHandler)
|
||||
|
||||
// Our handlers satisfy http.Handler, so we can call their ServeHTTP method
|
||||
// directly and pass in our Request and ResponseRecorder.
|
||||
handler.ServeHTTP(rr, req)
|
||||
|
||||
// Check the status code is what we expect.
|
||||
if status := rr.Code; status != http.StatusOK {
|
||||
t.Errorf("handler returned wrong status code: got %v want %v",
|
||||
status, http.StatusOK)
|
||||
}
|
||||
|
||||
// Check the response body is what we expect.
|
||||
expected := `{"alive": true}`
|
||||
if rr.Body.String() != expected {
|
||||
t.Errorf("handler returned unexpected body: got %v want %v",
|
||||
rr.Body.String(), expected)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
In the case that our routes have [variables](#examples), we can pass those in the request. We could write
|
||||
[table-driven tests](https://dave.cheney.net/2013/06/09/writing-table-driven-tests-in-go) to test multiple
|
||||
possible route variables as needed.
|
||||
|
||||
```go
|
||||
// endpoints.go
|
||||
func main() {
|
||||
r := mux.NewRouter()
|
||||
// A route with a route variable:
|
||||
r.HandleFunc("/metrics/{type}", MetricsHandler)
|
||||
|
||||
log.Fatal(http.ListenAndServe("localhost:8080", r))
|
||||
}
|
||||
```
|
||||
|
||||
Our test file, with a table-driven test of `routeVariables`:
|
||||
|
||||
```go
|
||||
// endpoints_test.go
|
||||
func TestMetricsHandler(t *testing.T) {
|
||||
tt := []struct{
|
||||
routeVariable string
|
||||
shouldPass bool
|
||||
}{
|
||||
{"goroutines", true},
|
||||
{"heap", true},
|
||||
{"counters", true},
|
||||
{"queries", true},
|
||||
{"adhadaeqm3k", false},
|
||||
}
|
||||
|
||||
for _, tc := range tt {
|
||||
path := fmt.Sprintf("/metrics/%s", tc.routeVariable)
|
||||
req, err := http.NewRequest("GET", path, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
rr := httptest.NewRecorder()
|
||||
|
||||
// Need to create a router that we can pass the request through so that the vars will be added to the context
|
||||
router := mux.NewRouter()
|
||||
router.HandleFunc("/metrics/{type}", MetricsHandler)
|
||||
router.ServeHTTP(rr, req)
|
||||
|
||||
// In this case, our MetricsHandler returns a non-200 response
|
||||
// for a route variable it doesn't know about.
|
||||
if rr.Code == http.StatusOK && !tc.shouldPass {
|
||||
t.Errorf("handler should have failed on routeVariable %s: got %v want %v",
|
||||
tc.routeVariable, rr.Code, http.StatusOK)
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Full Example
|
||||
|
||||
Here's a complete, runnable example of a small `mux` based server:
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"log"
|
||||
"github.com/gorilla/mux"
|
||||
)
|
||||
|
||||
func YourHandler(w http.ResponseWriter, r *http.Request) {
|
||||
w.Write([]byte("Gorilla!\n"))
|
||||
}
|
||||
|
||||
func main() {
|
||||
r := mux.NewRouter()
|
||||
// Routes consist of a path and a handler function.
|
||||
r.HandleFunc("/", YourHandler)
|
||||
|
||||
// Bind to a port and pass our router in
|
||||
log.Fatal(http.ListenAndServe(":8000", r))
|
||||
}
|
||||
```
|
||||
|
||||
## License
|
||||
|
||||
BSD licensed. See the LICENSE file for details.
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
// +build !go1.7
|
||||
|
||||
package mux
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gorilla/context"
|
||||
)
|
||||
|
||||
func contextGet(r *http.Request, key interface{}) interface{} {
|
||||
return context.Get(r, key)
|
||||
}
|
||||
|
||||
func contextSet(r *http.Request, key, val interface{}) *http.Request {
|
||||
if val == nil {
|
||||
return r
|
||||
}
|
||||
|
||||
context.Set(r, key, val)
|
||||
return r
|
||||
}
|
||||
|
||||
func contextClear(r *http.Request) {
|
||||
context.Clear(r)
|
||||
}
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
// +build go1.7
|
||||
|
||||
package mux
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
func contextGet(r *http.Request, key interface{}) interface{} {
|
||||
return r.Context().Value(key)
|
||||
}
|
||||
|
||||
func contextSet(r *http.Request, key, val interface{}) *http.Request {
|
||||
if val == nil {
|
||||
return r
|
||||
}
|
||||
|
||||
return r.WithContext(context.WithValue(r.Context(), key, val))
|
||||
}
|
||||
|
||||
func contextClear(r *http.Request) {
|
||||
return
|
||||
}
|
||||
+306
@@ -0,0 +1,306 @@
|
||||
// Copyright 2012 The Gorilla Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
/*
|
||||
Package mux implements a request router and dispatcher.
|
||||
|
||||
The name mux stands for "HTTP request multiplexer". Like the standard
|
||||
http.ServeMux, mux.Router matches incoming requests against a list of
|
||||
registered routes and calls a handler for the route that matches the URL
|
||||
or other conditions. The main features are:
|
||||
|
||||
* Requests can be matched based on URL host, path, path prefix, schemes,
|
||||
header and query values, HTTP methods or using custom matchers.
|
||||
* URL hosts, paths and query values can have variables with an optional
|
||||
regular expression.
|
||||
* Registered URLs can be built, or "reversed", which helps maintaining
|
||||
references to resources.
|
||||
* Routes can be used as subrouters: nested routes are only tested if the
|
||||
parent route matches. This is useful to define groups of routes that
|
||||
share common conditions like a host, a path prefix or other repeated
|
||||
attributes. As a bonus, this optimizes request matching.
|
||||
* It implements the http.Handler interface so it is compatible with the
|
||||
standard http.ServeMux.
|
||||
|
||||
Let's start registering a couple of URL paths and handlers:
|
||||
|
||||
func main() {
|
||||
r := mux.NewRouter()
|
||||
r.HandleFunc("/", HomeHandler)
|
||||
r.HandleFunc("/products", ProductsHandler)
|
||||
r.HandleFunc("/articles", ArticlesHandler)
|
||||
http.Handle("/", r)
|
||||
}
|
||||
|
||||
Here we register three routes mapping URL paths to handlers. This is
|
||||
equivalent to how http.HandleFunc() works: if an incoming request URL matches
|
||||
one of the paths, the corresponding handler is called passing
|
||||
(http.ResponseWriter, *http.Request) as parameters.
|
||||
|
||||
Paths can have variables. They are defined using the format {name} or
|
||||
{name:pattern}. If a regular expression pattern is not defined, the matched
|
||||
variable will be anything until the next slash. For example:
|
||||
|
||||
r := mux.NewRouter()
|
||||
r.HandleFunc("/products/{key}", ProductHandler)
|
||||
r.HandleFunc("/articles/{category}/", ArticlesCategoryHandler)
|
||||
r.HandleFunc("/articles/{category}/{id:[0-9]+}", ArticleHandler)
|
||||
|
||||
Groups can be used inside patterns, as long as they are non-capturing (?:re). For example:
|
||||
|
||||
r.HandleFunc("/articles/{category}/{sort:(?:asc|desc|new)}", ArticlesCategoryHandler)
|
||||
|
||||
The names are used to create a map of route variables which can be retrieved
|
||||
calling mux.Vars():
|
||||
|
||||
vars := mux.Vars(request)
|
||||
category := vars["category"]
|
||||
|
||||
Note that if any capturing groups are present, mux will panic() during parsing. To prevent
|
||||
this, convert any capturing groups to non-capturing, e.g. change "/{sort:(asc|desc)}" to
|
||||
"/{sort:(?:asc|desc)}". This is a change from prior versions which behaved unpredictably
|
||||
when capturing groups were present.
|
||||
|
||||
And this is all you need to know about the basic usage. More advanced options
|
||||
are explained below.
|
||||
|
||||
Routes can also be restricted to a domain or subdomain. Just define a host
|
||||
pattern to be matched. They can also have variables:
|
||||
|
||||
r := mux.NewRouter()
|
||||
// Only matches if domain is "www.example.com".
|
||||
r.Host("www.example.com")
|
||||
// Matches a dynamic subdomain.
|
||||
r.Host("{subdomain:[a-z]+}.domain.com")
|
||||
|
||||
There are several other matchers that can be added. To match path prefixes:
|
||||
|
||||
r.PathPrefix("/products/")
|
||||
|
||||
...or HTTP methods:
|
||||
|
||||
r.Methods("GET", "POST")
|
||||
|
||||
...or URL schemes:
|
||||
|
||||
r.Schemes("https")
|
||||
|
||||
...or header values:
|
||||
|
||||
r.Headers("X-Requested-With", "XMLHttpRequest")
|
||||
|
||||
...or query values:
|
||||
|
||||
r.Queries("key", "value")
|
||||
|
||||
...or to use a custom matcher function:
|
||||
|
||||
r.MatcherFunc(func(r *http.Request, rm *RouteMatch) bool {
|
||||
return r.ProtoMajor == 0
|
||||
})
|
||||
|
||||
...and finally, it is possible to combine several matchers in a single route:
|
||||
|
||||
r.HandleFunc("/products", ProductsHandler).
|
||||
Host("www.example.com").
|
||||
Methods("GET").
|
||||
Schemes("http")
|
||||
|
||||
Setting the same matching conditions again and again can be boring, so we have
|
||||
a way to group several routes that share the same requirements.
|
||||
We call it "subrouting".
|
||||
|
||||
For example, let's say we have several URLs that should only match when the
|
||||
host is "www.example.com". Create a route for that host and get a "subrouter"
|
||||
from it:
|
||||
|
||||
r := mux.NewRouter()
|
||||
s := r.Host("www.example.com").Subrouter()
|
||||
|
||||
Then register routes in the subrouter:
|
||||
|
||||
s.HandleFunc("/products/", ProductsHandler)
|
||||
s.HandleFunc("/products/{key}", ProductHandler)
|
||||
s.HandleFunc("/articles/{category}/{id:[0-9]+}"), ArticleHandler)
|
||||
|
||||
The three URL paths we registered above will only be tested if the domain is
|
||||
"www.example.com", because the subrouter is tested first. This is not
|
||||
only convenient, but also optimizes request matching. You can create
|
||||
subrouters combining any attribute matchers accepted by a route.
|
||||
|
||||
Subrouters can be used to create domain or path "namespaces": you define
|
||||
subrouters in a central place and then parts of the app can register its
|
||||
paths relatively to a given subrouter.
|
||||
|
||||
There's one more thing about subroutes. When a subrouter has a path prefix,
|
||||
the inner routes use it as base for their paths:
|
||||
|
||||
r := mux.NewRouter()
|
||||
s := r.PathPrefix("/products").Subrouter()
|
||||
// "/products/"
|
||||
s.HandleFunc("/", ProductsHandler)
|
||||
// "/products/{key}/"
|
||||
s.HandleFunc("/{key}/", ProductHandler)
|
||||
// "/products/{key}/details"
|
||||
s.HandleFunc("/{key}/details", ProductDetailsHandler)
|
||||
|
||||
Note that the path provided to PathPrefix() represents a "wildcard": calling
|
||||
PathPrefix("/static/").Handler(...) means that the handler will be passed any
|
||||
request that matches "/static/*". This makes it easy to serve static files with mux:
|
||||
|
||||
func main() {
|
||||
var dir string
|
||||
|
||||
flag.StringVar(&dir, "dir", ".", "the directory to serve files from. Defaults to the current dir")
|
||||
flag.Parse()
|
||||
r := mux.NewRouter()
|
||||
|
||||
// This will serve files under http://localhost:8000/static/<filename>
|
||||
r.PathPrefix("/static/").Handler(http.StripPrefix("/static/", http.FileServer(http.Dir(dir))))
|
||||
|
||||
srv := &http.Server{
|
||||
Handler: r,
|
||||
Addr: "127.0.0.1:8000",
|
||||
// Good practice: enforce timeouts for servers you create!
|
||||
WriteTimeout: 15 * time.Second,
|
||||
ReadTimeout: 15 * time.Second,
|
||||
}
|
||||
|
||||
log.Fatal(srv.ListenAndServe())
|
||||
}
|
||||
|
||||
Now let's see how to build registered URLs.
|
||||
|
||||
Routes can be named. All routes that define a name can have their URLs built,
|
||||
or "reversed". We define a name calling Name() on a route. For example:
|
||||
|
||||
r := mux.NewRouter()
|
||||
r.HandleFunc("/articles/{category}/{id:[0-9]+}", ArticleHandler).
|
||||
Name("article")
|
||||
|
||||
To build a URL, get the route and call the URL() method, passing a sequence of
|
||||
key/value pairs for the route variables. For the previous route, we would do:
|
||||
|
||||
url, err := r.Get("article").URL("category", "technology", "id", "42")
|
||||
|
||||
...and the result will be a url.URL with the following path:
|
||||
|
||||
"/articles/technology/42"
|
||||
|
||||
This also works for host and query value variables:
|
||||
|
||||
r := mux.NewRouter()
|
||||
r.Host("{subdomain}.domain.com").
|
||||
Path("/articles/{category}/{id:[0-9]+}").
|
||||
Queries("filter", "{filter}").
|
||||
HandlerFunc(ArticleHandler).
|
||||
Name("article")
|
||||
|
||||
// url.String() will be "http://news.domain.com/articles/technology/42?filter=gorilla"
|
||||
url, err := r.Get("article").URL("subdomain", "news",
|
||||
"category", "technology",
|
||||
"id", "42",
|
||||
"filter", "gorilla")
|
||||
|
||||
All variables defined in the route are required, and their values must
|
||||
conform to the corresponding patterns. These requirements guarantee that a
|
||||
generated URL will always match a registered route -- the only exception is
|
||||
for explicitly defined "build-only" routes which never match.
|
||||
|
||||
Regex support also exists for matching Headers within a route. For example, we could do:
|
||||
|
||||
r.HeadersRegexp("Content-Type", "application/(text|json)")
|
||||
|
||||
...and the route will match both requests with a Content-Type of `application/json` as well as
|
||||
`application/text`
|
||||
|
||||
There's also a way to build only the URL host or path for a route:
|
||||
use the methods URLHost() or URLPath() instead. For the previous route,
|
||||
we would do:
|
||||
|
||||
// "http://news.domain.com/"
|
||||
host, err := r.Get("article").URLHost("subdomain", "news")
|
||||
|
||||
// "/articles/technology/42"
|
||||
path, err := r.Get("article").URLPath("category", "technology", "id", "42")
|
||||
|
||||
And if you use subrouters, host and path defined separately can be built
|
||||
as well:
|
||||
|
||||
r := mux.NewRouter()
|
||||
s := r.Host("{subdomain}.domain.com").Subrouter()
|
||||
s.Path("/articles/{category}/{id:[0-9]+}").
|
||||
HandlerFunc(ArticleHandler).
|
||||
Name("article")
|
||||
|
||||
// "http://news.domain.com/articles/technology/42"
|
||||
url, err := r.Get("article").URL("subdomain", "news",
|
||||
"category", "technology",
|
||||
"id", "42")
|
||||
|
||||
Mux supports the addition of middlewares to a Router, which are executed in the order they are added if a match is found, including its subrouters. Middlewares are (typically) small pieces of code which take one request, do something with it, and pass it down to another middleware or the final handler. Some common use cases for middleware are request logging, header manipulation, or ResponseWriter hijacking.
|
||||
|
||||
type MiddlewareFunc func(http.Handler) http.Handler
|
||||
|
||||
Typically, the returned handler is a closure which does something with the http.ResponseWriter and http.Request passed to it, and then calls the handler passed as parameter to the MiddlewareFunc (closures can access variables from the context where they are created).
|
||||
|
||||
A very basic middleware which logs the URI of the request being handled could be written as:
|
||||
|
||||
func simpleMw(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
// Do stuff here
|
||||
log.Println(r.RequestURI)
|
||||
// Call the next handler, which can be another middleware in the chain, or the final handler.
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
|
||||
Middlewares can be added to a router using `Router.Use()`:
|
||||
|
||||
r := mux.NewRouter()
|
||||
r.HandleFunc("/", handler)
|
||||
r.Use(simpleMw)
|
||||
|
||||
A more complex authentication middleware, which maps session token to users, could be written as:
|
||||
|
||||
// Define our struct
|
||||
type authenticationMiddleware struct {
|
||||
tokenUsers map[string]string
|
||||
}
|
||||
|
||||
// Initialize it somewhere
|
||||
func (amw *authenticationMiddleware) Populate() {
|
||||
amw.tokenUsers["00000000"] = "user0"
|
||||
amw.tokenUsers["aaaaaaaa"] = "userA"
|
||||
amw.tokenUsers["05f717e5"] = "randomUser"
|
||||
amw.tokenUsers["deadbeef"] = "user0"
|
||||
}
|
||||
|
||||
// Middleware function, which will be called for each request
|
||||
func (amw *authenticationMiddleware) Middleware(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
token := r.Header.Get("X-Session-Token")
|
||||
|
||||
if user, found := amw.tokenUsers[token]; found {
|
||||
// We found the token in our map
|
||||
log.Printf("Authenticated user %s\n", user)
|
||||
next.ServeHTTP(w, r)
|
||||
} else {
|
||||
http.Error(w, "Forbidden", http.StatusForbidden)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
r := mux.NewRouter()
|
||||
r.HandleFunc("/", handler)
|
||||
|
||||
amw := authenticationMiddleware{}
|
||||
amw.Populate()
|
||||
|
||||
r.Use(amw.Middleware)
|
||||
|
||||
Note: The handler chain will be stopped if your middleware doesn't call `next.ServeHTTP()` with the corresponding parameters. This can be used to abort a request if the middleware writer wants to.
|
||||
|
||||
*/
|
||||
package mux
|
||||
+72
@@ -0,0 +1,72 @@
|
||||
package mux
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// MiddlewareFunc is a function which receives an http.Handler and returns another http.Handler.
|
||||
// Typically, the returned handler is a closure which does something with the http.ResponseWriter and http.Request passed
|
||||
// to it, and then calls the handler passed as parameter to the MiddlewareFunc.
|
||||
type MiddlewareFunc func(http.Handler) http.Handler
|
||||
|
||||
// middleware interface is anything which implements a MiddlewareFunc named Middleware.
|
||||
type middleware interface {
|
||||
Middleware(handler http.Handler) http.Handler
|
||||
}
|
||||
|
||||
// Middleware allows MiddlewareFunc to implement the middleware interface.
|
||||
func (mw MiddlewareFunc) Middleware(handler http.Handler) http.Handler {
|
||||
return mw(handler)
|
||||
}
|
||||
|
||||
// Use appends a MiddlewareFunc to the chain. Middleware can be used to intercept or otherwise modify requests and/or responses, and are executed in the order that they are applied to the Router.
|
||||
func (r *Router) Use(mwf ...MiddlewareFunc) {
|
||||
for _, fn := range mwf {
|
||||
r.middlewares = append(r.middlewares, fn)
|
||||
}
|
||||
}
|
||||
|
||||
// useInterface appends a middleware to the chain. Middleware can be used to intercept or otherwise modify requests and/or responses, and are executed in the order that they are applied to the Router.
|
||||
func (r *Router) useInterface(mw middleware) {
|
||||
r.middlewares = append(r.middlewares, mw)
|
||||
}
|
||||
|
||||
// CORSMethodMiddleware sets the Access-Control-Allow-Methods response header
|
||||
// on a request, by matching routes based only on paths. It also handles
|
||||
// OPTIONS requests, by settings Access-Control-Allow-Methods, and then
|
||||
// returning without calling the next http handler.
|
||||
func CORSMethodMiddleware(r *Router) MiddlewareFunc {
|
||||
return func(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
|
||||
var allMethods []string
|
||||
|
||||
err := r.Walk(func(route *Route, _ *Router, _ []*Route) error {
|
||||
for _, m := range route.matchers {
|
||||
if _, ok := m.(*routeRegexp); ok {
|
||||
if m.Match(req, &RouteMatch{}) {
|
||||
methods, err := route.GetMethods()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
allMethods = append(allMethods, methods...)
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
if err == nil {
|
||||
w.Header().Set("Access-Control-Allow-Methods", strings.Join(append(allMethods, "OPTIONS"), ","))
|
||||
|
||||
if req.Method == "OPTIONS" {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
next.ServeHTTP(w, req)
|
||||
})
|
||||
}
|
||||
}
|
||||
+588
@@ -0,0 +1,588 @@
|
||||
// Copyright 2012 The Gorilla Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package mux
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"path"
|
||||
"regexp"
|
||||
)
|
||||
|
||||
var (
|
||||
// ErrMethodMismatch is returned when the method in the request does not match
|
||||
// the method defined against the route.
|
||||
ErrMethodMismatch = errors.New("method is not allowed")
|
||||
// ErrNotFound is returned when no route match is found.
|
||||
ErrNotFound = errors.New("no matching route was found")
|
||||
)
|
||||
|
||||
// NewRouter returns a new router instance.
|
||||
func NewRouter() *Router {
|
||||
return &Router{namedRoutes: make(map[string]*Route), KeepContext: false}
|
||||
}
|
||||
|
||||
// Router registers routes to be matched and dispatches a handler.
|
||||
//
|
||||
// It implements the http.Handler interface, so it can be registered to serve
|
||||
// requests:
|
||||
//
|
||||
// var router = mux.NewRouter()
|
||||
//
|
||||
// func main() {
|
||||
// http.Handle("/", router)
|
||||
// }
|
||||
//
|
||||
// Or, for Google App Engine, register it in a init() function:
|
||||
//
|
||||
// func init() {
|
||||
// http.Handle("/", router)
|
||||
// }
|
||||
//
|
||||
// This will send all incoming requests to the router.
|
||||
type Router struct {
|
||||
// Configurable Handler to be used when no route matches.
|
||||
NotFoundHandler http.Handler
|
||||
|
||||
// Configurable Handler to be used when the request method does not match the route.
|
||||
MethodNotAllowedHandler http.Handler
|
||||
|
||||
// Parent route, if this is a subrouter.
|
||||
parent parentRoute
|
||||
// Routes to be matched, in order.
|
||||
routes []*Route
|
||||
// Routes by name for URL building.
|
||||
namedRoutes map[string]*Route
|
||||
// See Router.StrictSlash(). This defines the flag for new routes.
|
||||
strictSlash bool
|
||||
// See Router.SkipClean(). This defines the flag for new routes.
|
||||
skipClean bool
|
||||
// If true, do not clear the request context after handling the request.
|
||||
// This has no effect when go1.7+ is used, since the context is stored
|
||||
// on the request itself.
|
||||
KeepContext bool
|
||||
// see Router.UseEncodedPath(). This defines a flag for all routes.
|
||||
useEncodedPath bool
|
||||
// Slice of middlewares to be called after a match is found
|
||||
middlewares []middleware
|
||||
}
|
||||
|
||||
// Match attempts to match the given request against the router's registered routes.
|
||||
//
|
||||
// If the request matches a route of this router or one of its subrouters the Route,
|
||||
// Handler, and Vars fields of the the match argument are filled and this function
|
||||
// returns true.
|
||||
//
|
||||
// If the request does not match any of this router's or its subrouters' routes
|
||||
// then this function returns false. If available, a reason for the match failure
|
||||
// will be filled in the match argument's MatchErr field. If the match failure type
|
||||
// (eg: not found) has a registered handler, the handler is assigned to the Handler
|
||||
// field of the match argument.
|
||||
func (r *Router) Match(req *http.Request, match *RouteMatch) bool {
|
||||
for _, route := range r.routes {
|
||||
if route.Match(req, match) {
|
||||
// Build middleware chain if no error was found
|
||||
if match.MatchErr == nil {
|
||||
for i := len(r.middlewares) - 1; i >= 0; i-- {
|
||||
match.Handler = r.middlewares[i].Middleware(match.Handler)
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
if match.MatchErr == ErrMethodMismatch {
|
||||
if r.MethodNotAllowedHandler != nil {
|
||||
match.Handler = r.MethodNotAllowedHandler
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// Closest match for a router (includes sub-routers)
|
||||
if r.NotFoundHandler != nil {
|
||||
match.Handler = r.NotFoundHandler
|
||||
match.MatchErr = ErrNotFound
|
||||
return true
|
||||
}
|
||||
|
||||
match.MatchErr = ErrNotFound
|
||||
return false
|
||||
}
|
||||
|
||||
// ServeHTTP dispatches the handler registered in the matched route.
|
||||
//
|
||||
// When there is a match, the route variables can be retrieved calling
|
||||
// mux.Vars(request).
|
||||
func (r *Router) ServeHTTP(w http.ResponseWriter, req *http.Request) {
|
||||
if !r.skipClean {
|
||||
path := req.URL.Path
|
||||
if r.useEncodedPath {
|
||||
path = req.URL.EscapedPath()
|
||||
}
|
||||
// Clean path to canonical form and redirect.
|
||||
if p := cleanPath(path); p != path {
|
||||
|
||||
// Added 3 lines (Philip Schlump) - It was dropping the query string and #whatever from query.
|
||||
// This matches with fix in go 1.2 r.c. 4 for same problem. Go Issue:
|
||||
// http://code.google.com/p/go/issues/detail?id=5252
|
||||
url := *req.URL
|
||||
url.Path = p
|
||||
p = url.String()
|
||||
|
||||
w.Header().Set("Location", p)
|
||||
w.WriteHeader(http.StatusMovedPermanently)
|
||||
return
|
||||
}
|
||||
}
|
||||
var match RouteMatch
|
||||
var handler http.Handler
|
||||
if r.Match(req, &match) {
|
||||
handler = match.Handler
|
||||
req = setVars(req, match.Vars)
|
||||
req = setCurrentRoute(req, match.Route)
|
||||
}
|
||||
|
||||
if handler == nil && match.MatchErr == ErrMethodMismatch {
|
||||
handler = methodNotAllowedHandler()
|
||||
}
|
||||
|
||||
if handler == nil {
|
||||
handler = http.NotFoundHandler()
|
||||
}
|
||||
|
||||
if !r.KeepContext {
|
||||
defer contextClear(req)
|
||||
}
|
||||
|
||||
handler.ServeHTTP(w, req)
|
||||
}
|
||||
|
||||
// Get returns a route registered with the given name.
|
||||
func (r *Router) Get(name string) *Route {
|
||||
return r.getNamedRoutes()[name]
|
||||
}
|
||||
|
||||
// GetRoute returns a route registered with the given name. This method
|
||||
// was renamed to Get() and remains here for backwards compatibility.
|
||||
func (r *Router) GetRoute(name string) *Route {
|
||||
return r.getNamedRoutes()[name]
|
||||
}
|
||||
|
||||
// StrictSlash defines the trailing slash behavior for new routes. The initial
|
||||
// value is false.
|
||||
//
|
||||
// When true, if the route path is "/path/", accessing "/path" will perform a redirect
|
||||
// to the former and vice versa. In other words, your application will always
|
||||
// see the path as specified in the route.
|
||||
//
|
||||
// When false, if the route path is "/path", accessing "/path/" will not match
|
||||
// this route and vice versa.
|
||||
//
|
||||
// The re-direct is a HTTP 301 (Moved Permanently). Note that when this is set for
|
||||
// routes with a non-idempotent method (e.g. POST, PUT), the subsequent re-directed
|
||||
// request will be made as a GET by most clients. Use middleware or client settings
|
||||
// to modify this behaviour as needed.
|
||||
//
|
||||
// Special case: when a route sets a path prefix using the PathPrefix() method,
|
||||
// strict slash is ignored for that route because the redirect behavior can't
|
||||
// be determined from a prefix alone. However, any subrouters created from that
|
||||
// route inherit the original StrictSlash setting.
|
||||
func (r *Router) StrictSlash(value bool) *Router {
|
||||
r.strictSlash = value
|
||||
return r
|
||||
}
|
||||
|
||||
// SkipClean defines the path cleaning behaviour for new routes. The initial
|
||||
// value is false. Users should be careful about which routes are not cleaned
|
||||
//
|
||||
// When true, if the route path is "/path//to", it will remain with the double
|
||||
// slash. This is helpful if you have a route like: /fetch/http://xkcd.com/534/
|
||||
//
|
||||
// When false, the path will be cleaned, so /fetch/http://xkcd.com/534/ will
|
||||
// become /fetch/http/xkcd.com/534
|
||||
func (r *Router) SkipClean(value bool) *Router {
|
||||
r.skipClean = value
|
||||
return r
|
||||
}
|
||||
|
||||
// UseEncodedPath tells the router to match the encoded original path
|
||||
// to the routes.
|
||||
// For eg. "/path/foo%2Fbar/to" will match the path "/path/{var}/to".
|
||||
//
|
||||
// If not called, the router will match the unencoded path to the routes.
|
||||
// For eg. "/path/foo%2Fbar/to" will match the path "/path/foo/bar/to"
|
||||
func (r *Router) UseEncodedPath() *Router {
|
||||
r.useEncodedPath = true
|
||||
return r
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// parentRoute
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
func (r *Router) getBuildScheme() string {
|
||||
if r.parent != nil {
|
||||
return r.parent.getBuildScheme()
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// getNamedRoutes returns the map where named routes are registered.
|
||||
func (r *Router) getNamedRoutes() map[string]*Route {
|
||||
if r.namedRoutes == nil {
|
||||
if r.parent != nil {
|
||||
r.namedRoutes = r.parent.getNamedRoutes()
|
||||
} else {
|
||||
r.namedRoutes = make(map[string]*Route)
|
||||
}
|
||||
}
|
||||
return r.namedRoutes
|
||||
}
|
||||
|
||||
// getRegexpGroup returns regexp definitions from the parent route, if any.
|
||||
func (r *Router) getRegexpGroup() *routeRegexpGroup {
|
||||
if r.parent != nil {
|
||||
return r.parent.getRegexpGroup()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *Router) buildVars(m map[string]string) map[string]string {
|
||||
if r.parent != nil {
|
||||
m = r.parent.buildVars(m)
|
||||
}
|
||||
return m
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// Route factories
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
// NewRoute registers an empty route.
|
||||
func (r *Router) NewRoute() *Route {
|
||||
route := &Route{parent: r, strictSlash: r.strictSlash, skipClean: r.skipClean, useEncodedPath: r.useEncodedPath}
|
||||
r.routes = append(r.routes, route)
|
||||
return route
|
||||
}
|
||||
|
||||
// Handle registers a new route with a matcher for the URL path.
|
||||
// See Route.Path() and Route.Handler().
|
||||
func (r *Router) Handle(path string, handler http.Handler) *Route {
|
||||
return r.NewRoute().Path(path).Handler(handler)
|
||||
}
|
||||
|
||||
// HandleFunc registers a new route with a matcher for the URL path.
|
||||
// See Route.Path() and Route.HandlerFunc().
|
||||
func (r *Router) HandleFunc(path string, f func(http.ResponseWriter,
|
||||
*http.Request)) *Route {
|
||||
return r.NewRoute().Path(path).HandlerFunc(f)
|
||||
}
|
||||
|
||||
// Headers registers a new route with a matcher for request header values.
|
||||
// See Route.Headers().
|
||||
func (r *Router) Headers(pairs ...string) *Route {
|
||||
return r.NewRoute().Headers(pairs...)
|
||||
}
|
||||
|
||||
// Host registers a new route with a matcher for the URL host.
|
||||
// See Route.Host().
|
||||
func (r *Router) Host(tpl string) *Route {
|
||||
return r.NewRoute().Host(tpl)
|
||||
}
|
||||
|
||||
// MatcherFunc registers a new route with a custom matcher function.
|
||||
// See Route.MatcherFunc().
|
||||
func (r *Router) MatcherFunc(f MatcherFunc) *Route {
|
||||
return r.NewRoute().MatcherFunc(f)
|
||||
}
|
||||
|
||||
// Methods registers a new route with a matcher for HTTP methods.
|
||||
// See Route.Methods().
|
||||
func (r *Router) Methods(methods ...string) *Route {
|
||||
return r.NewRoute().Methods(methods...)
|
||||
}
|
||||
|
||||
// Path registers a new route with a matcher for the URL path.
|
||||
// See Route.Path().
|
||||
func (r *Router) Path(tpl string) *Route {
|
||||
return r.NewRoute().Path(tpl)
|
||||
}
|
||||
|
||||
// PathPrefix registers a new route with a matcher for the URL path prefix.
|
||||
// See Route.PathPrefix().
|
||||
func (r *Router) PathPrefix(tpl string) *Route {
|
||||
return r.NewRoute().PathPrefix(tpl)
|
||||
}
|
||||
|
||||
// Queries registers a new route with a matcher for URL query values.
|
||||
// See Route.Queries().
|
||||
func (r *Router) Queries(pairs ...string) *Route {
|
||||
return r.NewRoute().Queries(pairs...)
|
||||
}
|
||||
|
||||
// Schemes registers a new route with a matcher for URL schemes.
|
||||
// See Route.Schemes().
|
||||
func (r *Router) Schemes(schemes ...string) *Route {
|
||||
return r.NewRoute().Schemes(schemes...)
|
||||
}
|
||||
|
||||
// BuildVarsFunc registers a new route with a custom function for modifying
|
||||
// route variables before building a URL.
|
||||
func (r *Router) BuildVarsFunc(f BuildVarsFunc) *Route {
|
||||
return r.NewRoute().BuildVarsFunc(f)
|
||||
}
|
||||
|
||||
// Walk walks the router and all its sub-routers, calling walkFn for each route
|
||||
// in the tree. The routes are walked in the order they were added. Sub-routers
|
||||
// are explored depth-first.
|
||||
func (r *Router) Walk(walkFn WalkFunc) error {
|
||||
return r.walk(walkFn, []*Route{})
|
||||
}
|
||||
|
||||
// SkipRouter is used as a return value from WalkFuncs to indicate that the
|
||||
// router that walk is about to descend down to should be skipped.
|
||||
var SkipRouter = errors.New("skip this router")
|
||||
|
||||
// WalkFunc is the type of the function called for each route visited by Walk.
|
||||
// At every invocation, it is given the current route, and the current router,
|
||||
// and a list of ancestor routes that lead to the current route.
|
||||
type WalkFunc func(route *Route, router *Router, ancestors []*Route) error
|
||||
|
||||
func (r *Router) walk(walkFn WalkFunc, ancestors []*Route) error {
|
||||
for _, t := range r.routes {
|
||||
err := walkFn(t, r, ancestors)
|
||||
if err == SkipRouter {
|
||||
continue
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, sr := range t.matchers {
|
||||
if h, ok := sr.(*Router); ok {
|
||||
ancestors = append(ancestors, t)
|
||||
err := h.walk(walkFn, ancestors)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
ancestors = ancestors[:len(ancestors)-1]
|
||||
}
|
||||
}
|
||||
if h, ok := t.handler.(*Router); ok {
|
||||
ancestors = append(ancestors, t)
|
||||
err := h.walk(walkFn, ancestors)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
ancestors = ancestors[:len(ancestors)-1]
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// Context
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
// RouteMatch stores information about a matched route.
|
||||
type RouteMatch struct {
|
||||
Route *Route
|
||||
Handler http.Handler
|
||||
Vars map[string]string
|
||||
|
||||
// MatchErr is set to appropriate matching error
|
||||
// It is set to ErrMethodMismatch if there is a mismatch in
|
||||
// the request method and route method
|
||||
MatchErr error
|
||||
}
|
||||
|
||||
type contextKey int
|
||||
|
||||
const (
|
||||
varsKey contextKey = iota
|
||||
routeKey
|
||||
)
|
||||
|
||||
// Vars returns the route variables for the current request, if any.
|
||||
func Vars(r *http.Request) map[string]string {
|
||||
if rv := contextGet(r, varsKey); rv != nil {
|
||||
return rv.(map[string]string)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// CurrentRoute returns the matched route for the current request, if any.
|
||||
// This only works when called inside the handler of the matched route
|
||||
// because the matched route is stored in the request context which is cleared
|
||||
// after the handler returns, unless the KeepContext option is set on the
|
||||
// Router.
|
||||
func CurrentRoute(r *http.Request) *Route {
|
||||
if rv := contextGet(r, routeKey); rv != nil {
|
||||
return rv.(*Route)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func setVars(r *http.Request, val interface{}) *http.Request {
|
||||
return contextSet(r, varsKey, val)
|
||||
}
|
||||
|
||||
func setCurrentRoute(r *http.Request, val interface{}) *http.Request {
|
||||
return contextSet(r, routeKey, val)
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// Helpers
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
// cleanPath returns the canonical path for p, eliminating . and .. elements.
|
||||
// Borrowed from the net/http package.
|
||||
func cleanPath(p string) string {
|
||||
if p == "" {
|
||||
return "/"
|
||||
}
|
||||
if p[0] != '/' {
|
||||
p = "/" + p
|
||||
}
|
||||
np := path.Clean(p)
|
||||
// path.Clean removes trailing slash except for root;
|
||||
// put the trailing slash back if necessary.
|
||||
if p[len(p)-1] == '/' && np != "/" {
|
||||
np += "/"
|
||||
}
|
||||
|
||||
return np
|
||||
}
|
||||
|
||||
// uniqueVars returns an error if two slices contain duplicated strings.
|
||||
func uniqueVars(s1, s2 []string) error {
|
||||
for _, v1 := range s1 {
|
||||
for _, v2 := range s2 {
|
||||
if v1 == v2 {
|
||||
return fmt.Errorf("mux: duplicated route variable %q", v2)
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// checkPairs returns the count of strings passed in, and an error if
|
||||
// the count is not an even number.
|
||||
func checkPairs(pairs ...string) (int, error) {
|
||||
length := len(pairs)
|
||||
if length%2 != 0 {
|
||||
return length, fmt.Errorf(
|
||||
"mux: number of parameters must be multiple of 2, got %v", pairs)
|
||||
}
|
||||
return length, nil
|
||||
}
|
||||
|
||||
// mapFromPairsToString converts variadic string parameters to a
|
||||
// string to string map.
|
||||
func mapFromPairsToString(pairs ...string) (map[string]string, error) {
|
||||
length, err := checkPairs(pairs...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
m := make(map[string]string, length/2)
|
||||
for i := 0; i < length; i += 2 {
|
||||
m[pairs[i]] = pairs[i+1]
|
||||
}
|
||||
return m, nil
|
||||
}
|
||||
|
||||
// mapFromPairsToRegex converts variadic string parameters to a
|
||||
// string to regex map.
|
||||
func mapFromPairsToRegex(pairs ...string) (map[string]*regexp.Regexp, error) {
|
||||
length, err := checkPairs(pairs...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
m := make(map[string]*regexp.Regexp, length/2)
|
||||
for i := 0; i < length; i += 2 {
|
||||
regex, err := regexp.Compile(pairs[i+1])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
m[pairs[i]] = regex
|
||||
}
|
||||
return m, nil
|
||||
}
|
||||
|
||||
// matchInArray returns true if the given string value is in the array.
|
||||
func matchInArray(arr []string, value string) bool {
|
||||
for _, v := range arr {
|
||||
if v == value {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// matchMapWithString returns true if the given key/value pairs exist in a given map.
|
||||
func matchMapWithString(toCheck map[string]string, toMatch map[string][]string, canonicalKey bool) bool {
|
||||
for k, v := range toCheck {
|
||||
// Check if key exists.
|
||||
if canonicalKey {
|
||||
k = http.CanonicalHeaderKey(k)
|
||||
}
|
||||
if values := toMatch[k]; values == nil {
|
||||
return false
|
||||
} else if v != "" {
|
||||
// If value was defined as an empty string we only check that the
|
||||
// key exists. Otherwise we also check for equality.
|
||||
valueExists := false
|
||||
for _, value := range values {
|
||||
if v == value {
|
||||
valueExists = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !valueExists {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// matchMapWithRegex returns true if the given key/value pairs exist in a given map compiled against
|
||||
// the given regex
|
||||
func matchMapWithRegex(toCheck map[string]*regexp.Regexp, toMatch map[string][]string, canonicalKey bool) bool {
|
||||
for k, v := range toCheck {
|
||||
// Check if key exists.
|
||||
if canonicalKey {
|
||||
k = http.CanonicalHeaderKey(k)
|
||||
}
|
||||
if values := toMatch[k]; values == nil {
|
||||
return false
|
||||
} else if v != nil {
|
||||
// If value was defined as an empty string we only check that the
|
||||
// key exists. Otherwise we also check for equality.
|
||||
valueExists := false
|
||||
for _, value := range values {
|
||||
if v.MatchString(value) {
|
||||
valueExists = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !valueExists {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// methodNotAllowed replies to the request with an HTTP status code 405.
|
||||
func methodNotAllowed(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusMethodNotAllowed)
|
||||
}
|
||||
|
||||
// methodNotAllowedHandler returns a simple request handler
|
||||
// that replies to each request with a status code 405.
|
||||
func methodNotAllowedHandler() http.Handler { return http.HandlerFunc(methodNotAllowed) }
|
||||
+332
@@ -0,0 +1,332 @@
|
||||
// Copyright 2012 The Gorilla Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package mux
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type routeRegexpOptions struct {
|
||||
strictSlash bool
|
||||
useEncodedPath bool
|
||||
}
|
||||
|
||||
type regexpType int
|
||||
|
||||
const (
|
||||
regexpTypePath regexpType = 0
|
||||
regexpTypeHost regexpType = 1
|
||||
regexpTypePrefix regexpType = 2
|
||||
regexpTypeQuery regexpType = 3
|
||||
)
|
||||
|
||||
// newRouteRegexp parses a route template and returns a routeRegexp,
|
||||
// used to match a host, a path or a query string.
|
||||
//
|
||||
// It will extract named variables, assemble a regexp to be matched, create
|
||||
// a "reverse" template to build URLs and compile regexps to validate variable
|
||||
// values used in URL building.
|
||||
//
|
||||
// Previously we accepted only Python-like identifiers for variable
|
||||
// names ([a-zA-Z_][a-zA-Z0-9_]*), but currently the only restriction is that
|
||||
// name and pattern can't be empty, and names can't contain a colon.
|
||||
func newRouteRegexp(tpl string, typ regexpType, options routeRegexpOptions) (*routeRegexp, error) {
|
||||
// Check if it is well-formed.
|
||||
idxs, errBraces := braceIndices(tpl)
|
||||
if errBraces != nil {
|
||||
return nil, errBraces
|
||||
}
|
||||
// Backup the original.
|
||||
template := tpl
|
||||
// Now let's parse it.
|
||||
defaultPattern := "[^/]+"
|
||||
if typ == regexpTypeQuery {
|
||||
defaultPattern = ".*"
|
||||
} else if typ == regexpTypeHost {
|
||||
defaultPattern = "[^.]+"
|
||||
}
|
||||
// Only match strict slash if not matching
|
||||
if typ != regexpTypePath {
|
||||
options.strictSlash = false
|
||||
}
|
||||
// Set a flag for strictSlash.
|
||||
endSlash := false
|
||||
if options.strictSlash && strings.HasSuffix(tpl, "/") {
|
||||
tpl = tpl[:len(tpl)-1]
|
||||
endSlash = true
|
||||
}
|
||||
varsN := make([]string, len(idxs)/2)
|
||||
varsR := make([]*regexp.Regexp, len(idxs)/2)
|
||||
pattern := bytes.NewBufferString("")
|
||||
pattern.WriteByte('^')
|
||||
reverse := bytes.NewBufferString("")
|
||||
var end int
|
||||
var err error
|
||||
for i := 0; i < len(idxs); i += 2 {
|
||||
// Set all values we are interested in.
|
||||
raw := tpl[end:idxs[i]]
|
||||
end = idxs[i+1]
|
||||
parts := strings.SplitN(tpl[idxs[i]+1:end-1], ":", 2)
|
||||
name := parts[0]
|
||||
patt := defaultPattern
|
||||
if len(parts) == 2 {
|
||||
patt = parts[1]
|
||||
}
|
||||
// Name or pattern can't be empty.
|
||||
if name == "" || patt == "" {
|
||||
return nil, fmt.Errorf("mux: missing name or pattern in %q",
|
||||
tpl[idxs[i]:end])
|
||||
}
|
||||
// Build the regexp pattern.
|
||||
fmt.Fprintf(pattern, "%s(?P<%s>%s)", regexp.QuoteMeta(raw), varGroupName(i/2), patt)
|
||||
|
||||
// Build the reverse template.
|
||||
fmt.Fprintf(reverse, "%s%%s", raw)
|
||||
|
||||
// Append variable name and compiled pattern.
|
||||
varsN[i/2] = name
|
||||
varsR[i/2], err = regexp.Compile(fmt.Sprintf("^%s$", patt))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
// Add the remaining.
|
||||
raw := tpl[end:]
|
||||
pattern.WriteString(regexp.QuoteMeta(raw))
|
||||
if options.strictSlash {
|
||||
pattern.WriteString("[/]?")
|
||||
}
|
||||
if typ == regexpTypeQuery {
|
||||
// Add the default pattern if the query value is empty
|
||||
if queryVal := strings.SplitN(template, "=", 2)[1]; queryVal == "" {
|
||||
pattern.WriteString(defaultPattern)
|
||||
}
|
||||
}
|
||||
if typ != regexpTypePrefix {
|
||||
pattern.WriteByte('$')
|
||||
}
|
||||
reverse.WriteString(raw)
|
||||
if endSlash {
|
||||
reverse.WriteByte('/')
|
||||
}
|
||||
// Compile full regexp.
|
||||
reg, errCompile := regexp.Compile(pattern.String())
|
||||
if errCompile != nil {
|
||||
return nil, errCompile
|
||||
}
|
||||
|
||||
// Check for capturing groups which used to work in older versions
|
||||
if reg.NumSubexp() != len(idxs)/2 {
|
||||
panic(fmt.Sprintf("route %s contains capture groups in its regexp. ", template) +
|
||||
"Only non-capturing groups are accepted: e.g. (?:pattern) instead of (pattern)")
|
||||
}
|
||||
|
||||
// Done!
|
||||
return &routeRegexp{
|
||||
template: template,
|
||||
regexpType: typ,
|
||||
options: options,
|
||||
regexp: reg,
|
||||
reverse: reverse.String(),
|
||||
varsN: varsN,
|
||||
varsR: varsR,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// routeRegexp stores a regexp to match a host or path and information to
|
||||
// collect and validate route variables.
|
||||
type routeRegexp struct {
|
||||
// The unmodified template.
|
||||
template string
|
||||
// The type of match
|
||||
regexpType regexpType
|
||||
// Options for matching
|
||||
options routeRegexpOptions
|
||||
// Expanded regexp.
|
||||
regexp *regexp.Regexp
|
||||
// Reverse template.
|
||||
reverse string
|
||||
// Variable names.
|
||||
varsN []string
|
||||
// Variable regexps (validators).
|
||||
varsR []*regexp.Regexp
|
||||
}
|
||||
|
||||
// Match matches the regexp against the URL host or path.
|
||||
func (r *routeRegexp) Match(req *http.Request, match *RouteMatch) bool {
|
||||
if r.regexpType != regexpTypeHost {
|
||||
if r.regexpType == regexpTypeQuery {
|
||||
return r.matchQueryString(req)
|
||||
}
|
||||
path := req.URL.Path
|
||||
if r.options.useEncodedPath {
|
||||
path = req.URL.EscapedPath()
|
||||
}
|
||||
return r.regexp.MatchString(path)
|
||||
}
|
||||
|
||||
return r.regexp.MatchString(getHost(req))
|
||||
}
|
||||
|
||||
// url builds a URL part using the given values.
|
||||
func (r *routeRegexp) url(values map[string]string) (string, error) {
|
||||
urlValues := make([]interface{}, len(r.varsN))
|
||||
for k, v := range r.varsN {
|
||||
value, ok := values[v]
|
||||
if !ok {
|
||||
return "", fmt.Errorf("mux: missing route variable %q", v)
|
||||
}
|
||||
if r.regexpType == regexpTypeQuery {
|
||||
value = url.QueryEscape(value)
|
||||
}
|
||||
urlValues[k] = value
|
||||
}
|
||||
rv := fmt.Sprintf(r.reverse, urlValues...)
|
||||
if !r.regexp.MatchString(rv) {
|
||||
// The URL is checked against the full regexp, instead of checking
|
||||
// individual variables. This is faster but to provide a good error
|
||||
// message, we check individual regexps if the URL doesn't match.
|
||||
for k, v := range r.varsN {
|
||||
if !r.varsR[k].MatchString(values[v]) {
|
||||
return "", fmt.Errorf(
|
||||
"mux: variable %q doesn't match, expected %q", values[v],
|
||||
r.varsR[k].String())
|
||||
}
|
||||
}
|
||||
}
|
||||
return rv, nil
|
||||
}
|
||||
|
||||
// getURLQuery returns a single query parameter from a request URL.
|
||||
// For a URL with foo=bar&baz=ding, we return only the relevant key
|
||||
// value pair for the routeRegexp.
|
||||
func (r *routeRegexp) getURLQuery(req *http.Request) string {
|
||||
if r.regexpType != regexpTypeQuery {
|
||||
return ""
|
||||
}
|
||||
templateKey := strings.SplitN(r.template, "=", 2)[0]
|
||||
for key, vals := range req.URL.Query() {
|
||||
if key == templateKey && len(vals) > 0 {
|
||||
return key + "=" + vals[0]
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (r *routeRegexp) matchQueryString(req *http.Request) bool {
|
||||
return r.regexp.MatchString(r.getURLQuery(req))
|
||||
}
|
||||
|
||||
// braceIndices returns the first level curly brace indices from a string.
|
||||
// It returns an error in case of unbalanced braces.
|
||||
func braceIndices(s string) ([]int, error) {
|
||||
var level, idx int
|
||||
var idxs []int
|
||||
for i := 0; i < len(s); i++ {
|
||||
switch s[i] {
|
||||
case '{':
|
||||
if level++; level == 1 {
|
||||
idx = i
|
||||
}
|
||||
case '}':
|
||||
if level--; level == 0 {
|
||||
idxs = append(idxs, idx, i+1)
|
||||
} else if level < 0 {
|
||||
return nil, fmt.Errorf("mux: unbalanced braces in %q", s)
|
||||
}
|
||||
}
|
||||
}
|
||||
if level != 0 {
|
||||
return nil, fmt.Errorf("mux: unbalanced braces in %q", s)
|
||||
}
|
||||
return idxs, nil
|
||||
}
|
||||
|
||||
// varGroupName builds a capturing group name for the indexed variable.
|
||||
func varGroupName(idx int) string {
|
||||
return "v" + strconv.Itoa(idx)
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// routeRegexpGroup
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
// routeRegexpGroup groups the route matchers that carry variables.
|
||||
type routeRegexpGroup struct {
|
||||
host *routeRegexp
|
||||
path *routeRegexp
|
||||
queries []*routeRegexp
|
||||
}
|
||||
|
||||
// setMatch extracts the variables from the URL once a route matches.
|
||||
func (v *routeRegexpGroup) setMatch(req *http.Request, m *RouteMatch, r *Route) {
|
||||
// Store host variables.
|
||||
if v.host != nil {
|
||||
host := getHost(req)
|
||||
matches := v.host.regexp.FindStringSubmatchIndex(host)
|
||||
if len(matches) > 0 {
|
||||
extractVars(host, matches, v.host.varsN, m.Vars)
|
||||
}
|
||||
}
|
||||
path := req.URL.Path
|
||||
if r.useEncodedPath {
|
||||
path = req.URL.EscapedPath()
|
||||
}
|
||||
// Store path variables.
|
||||
if v.path != nil {
|
||||
matches := v.path.regexp.FindStringSubmatchIndex(path)
|
||||
if len(matches) > 0 {
|
||||
extractVars(path, matches, v.path.varsN, m.Vars)
|
||||
// Check if we should redirect.
|
||||
if v.path.options.strictSlash {
|
||||
p1 := strings.HasSuffix(path, "/")
|
||||
p2 := strings.HasSuffix(v.path.template, "/")
|
||||
if p1 != p2 {
|
||||
u, _ := url.Parse(req.URL.String())
|
||||
if p1 {
|
||||
u.Path = u.Path[:len(u.Path)-1]
|
||||
} else {
|
||||
u.Path += "/"
|
||||
}
|
||||
m.Handler = http.RedirectHandler(u.String(), 301)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// Store query string variables.
|
||||
for _, q := range v.queries {
|
||||
queryURL := q.getURLQuery(req)
|
||||
matches := q.regexp.FindStringSubmatchIndex(queryURL)
|
||||
if len(matches) > 0 {
|
||||
extractVars(queryURL, matches, q.varsN, m.Vars)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// getHost tries its best to return the request host.
|
||||
func getHost(r *http.Request) string {
|
||||
if r.URL.IsAbs() {
|
||||
return r.URL.Host
|
||||
}
|
||||
host := r.Host
|
||||
// Slice off any port information.
|
||||
if i := strings.Index(host, ":"); i != -1 {
|
||||
host = host[:i]
|
||||
}
|
||||
return host
|
||||
|
||||
}
|
||||
|
||||
func extractVars(input string, matches []int, names []string, output map[string]string) {
|
||||
for i, name := range names {
|
||||
output[name] = input[matches[2*i+2]:matches[2*i+3]]
|
||||
}
|
||||
}
|
||||
+763
@@ -0,0 +1,763 @@
|
||||
// Copyright 2012 The Gorilla Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package mux
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Route stores information to match a request and build URLs.
|
||||
type Route struct {
|
||||
// Parent where the route was registered (a Router).
|
||||
parent parentRoute
|
||||
// Request handler for the route.
|
||||
handler http.Handler
|
||||
// List of matchers.
|
||||
matchers []matcher
|
||||
// Manager for the variables from host and path.
|
||||
regexp *routeRegexpGroup
|
||||
// If true, when the path pattern is "/path/", accessing "/path" will
|
||||
// redirect to the former and vice versa.
|
||||
strictSlash bool
|
||||
// If true, when the path pattern is "/path//to", accessing "/path//to"
|
||||
// will not redirect
|
||||
skipClean bool
|
||||
// If true, "/path/foo%2Fbar/to" will match the path "/path/{var}/to"
|
||||
useEncodedPath bool
|
||||
// The scheme used when building URLs.
|
||||
buildScheme string
|
||||
// If true, this route never matches: it is only used to build URLs.
|
||||
buildOnly bool
|
||||
// The name used to build URLs.
|
||||
name string
|
||||
// Error resulted from building a route.
|
||||
err error
|
||||
|
||||
buildVarsFunc BuildVarsFunc
|
||||
}
|
||||
|
||||
// SkipClean reports whether path cleaning is enabled for this route via
|
||||
// Router.SkipClean.
|
||||
func (r *Route) SkipClean() bool {
|
||||
return r.skipClean
|
||||
}
|
||||
|
||||
// Match matches the route against the request.
|
||||
func (r *Route) Match(req *http.Request, match *RouteMatch) bool {
|
||||
if r.buildOnly || r.err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
var matchErr error
|
||||
|
||||
// Match everything.
|
||||
for _, m := range r.matchers {
|
||||
if matched := m.Match(req, match); !matched {
|
||||
if _, ok := m.(methodMatcher); ok {
|
||||
matchErr = ErrMethodMismatch
|
||||
continue
|
||||
}
|
||||
matchErr = nil
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
if matchErr != nil {
|
||||
match.MatchErr = matchErr
|
||||
return false
|
||||
}
|
||||
|
||||
if match.MatchErr == ErrMethodMismatch {
|
||||
// We found a route which matches request method, clear MatchErr
|
||||
match.MatchErr = nil
|
||||
// Then override the mis-matched handler
|
||||
match.Handler = r.handler
|
||||
}
|
||||
|
||||
// Yay, we have a match. Let's collect some info about it.
|
||||
if match.Route == nil {
|
||||
match.Route = r
|
||||
}
|
||||
if match.Handler == nil {
|
||||
match.Handler = r.handler
|
||||
}
|
||||
if match.Vars == nil {
|
||||
match.Vars = make(map[string]string)
|
||||
}
|
||||
|
||||
// Set variables.
|
||||
if r.regexp != nil {
|
||||
r.regexp.setMatch(req, match, r)
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// Route attributes
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
// GetError returns an error resulted from building the route, if any.
|
||||
func (r *Route) GetError() error {
|
||||
return r.err
|
||||
}
|
||||
|
||||
// BuildOnly sets the route to never match: it is only used to build URLs.
|
||||
func (r *Route) BuildOnly() *Route {
|
||||
r.buildOnly = true
|
||||
return r
|
||||
}
|
||||
|
||||
// Handler --------------------------------------------------------------------
|
||||
|
||||
// Handler sets a handler for the route.
|
||||
func (r *Route) Handler(handler http.Handler) *Route {
|
||||
if r.err == nil {
|
||||
r.handler = handler
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
||||
// HandlerFunc sets a handler function for the route.
|
||||
func (r *Route) HandlerFunc(f func(http.ResponseWriter, *http.Request)) *Route {
|
||||
return r.Handler(http.HandlerFunc(f))
|
||||
}
|
||||
|
||||
// GetHandler returns the handler for the route, if any.
|
||||
func (r *Route) GetHandler() http.Handler {
|
||||
return r.handler
|
||||
}
|
||||
|
||||
// Name -----------------------------------------------------------------------
|
||||
|
||||
// Name sets the name for the route, used to build URLs.
|
||||
// If the name was registered already it will be overwritten.
|
||||
func (r *Route) Name(name string) *Route {
|
||||
if r.name != "" {
|
||||
r.err = fmt.Errorf("mux: route already has name %q, can't set %q",
|
||||
r.name, name)
|
||||
}
|
||||
if r.err == nil {
|
||||
r.name = name
|
||||
r.getNamedRoutes()[name] = r
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
||||
// GetName returns the name for the route, if any.
|
||||
func (r *Route) GetName() string {
|
||||
return r.name
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// Matchers
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
// matcher types try to match a request.
|
||||
type matcher interface {
|
||||
Match(*http.Request, *RouteMatch) bool
|
||||
}
|
||||
|
||||
// addMatcher adds a matcher to the route.
|
||||
func (r *Route) addMatcher(m matcher) *Route {
|
||||
if r.err == nil {
|
||||
r.matchers = append(r.matchers, m)
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
||||
// addRegexpMatcher adds a host or path matcher and builder to a route.
|
||||
func (r *Route) addRegexpMatcher(tpl string, typ regexpType) error {
|
||||
if r.err != nil {
|
||||
return r.err
|
||||
}
|
||||
r.regexp = r.getRegexpGroup()
|
||||
if typ == regexpTypePath || typ == regexpTypePrefix {
|
||||
if len(tpl) > 0 && tpl[0] != '/' {
|
||||
return fmt.Errorf("mux: path must start with a slash, got %q", tpl)
|
||||
}
|
||||
if r.regexp.path != nil {
|
||||
tpl = strings.TrimRight(r.regexp.path.template, "/") + tpl
|
||||
}
|
||||
}
|
||||
rr, err := newRouteRegexp(tpl, typ, routeRegexpOptions{
|
||||
strictSlash: r.strictSlash,
|
||||
useEncodedPath: r.useEncodedPath,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, q := range r.regexp.queries {
|
||||
if err = uniqueVars(rr.varsN, q.varsN); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if typ == regexpTypeHost {
|
||||
if r.regexp.path != nil {
|
||||
if err = uniqueVars(rr.varsN, r.regexp.path.varsN); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
r.regexp.host = rr
|
||||
} else {
|
||||
if r.regexp.host != nil {
|
||||
if err = uniqueVars(rr.varsN, r.regexp.host.varsN); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if typ == regexpTypeQuery {
|
||||
r.regexp.queries = append(r.regexp.queries, rr)
|
||||
} else {
|
||||
r.regexp.path = rr
|
||||
}
|
||||
}
|
||||
r.addMatcher(rr)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Headers --------------------------------------------------------------------
|
||||
|
||||
// headerMatcher matches the request against header values.
|
||||
type headerMatcher map[string]string
|
||||
|
||||
func (m headerMatcher) Match(r *http.Request, match *RouteMatch) bool {
|
||||
return matchMapWithString(m, r.Header, true)
|
||||
}
|
||||
|
||||
// Headers adds a matcher for request header values.
|
||||
// It accepts a sequence of key/value pairs to be matched. For example:
|
||||
//
|
||||
// r := mux.NewRouter()
|
||||
// r.Headers("Content-Type", "application/json",
|
||||
// "X-Requested-With", "XMLHttpRequest")
|
||||
//
|
||||
// The above route will only match if both request header values match.
|
||||
// If the value is an empty string, it will match any value if the key is set.
|
||||
func (r *Route) Headers(pairs ...string) *Route {
|
||||
if r.err == nil {
|
||||
var headers map[string]string
|
||||
headers, r.err = mapFromPairsToString(pairs...)
|
||||
return r.addMatcher(headerMatcher(headers))
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
||||
// headerRegexMatcher matches the request against the route given a regex for the header
|
||||
type headerRegexMatcher map[string]*regexp.Regexp
|
||||
|
||||
func (m headerRegexMatcher) Match(r *http.Request, match *RouteMatch) bool {
|
||||
return matchMapWithRegex(m, r.Header, true)
|
||||
}
|
||||
|
||||
// HeadersRegexp accepts a sequence of key/value pairs, where the value has regex
|
||||
// support. For example:
|
||||
//
|
||||
// r := mux.NewRouter()
|
||||
// r.HeadersRegexp("Content-Type", "application/(text|json)",
|
||||
// "X-Requested-With", "XMLHttpRequest")
|
||||
//
|
||||
// The above route will only match if both the request header matches both regular expressions.
|
||||
// If the value is an empty string, it will match any value if the key is set.
|
||||
// Use the start and end of string anchors (^ and $) to match an exact value.
|
||||
func (r *Route) HeadersRegexp(pairs ...string) *Route {
|
||||
if r.err == nil {
|
||||
var headers map[string]*regexp.Regexp
|
||||
headers, r.err = mapFromPairsToRegex(pairs...)
|
||||
return r.addMatcher(headerRegexMatcher(headers))
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
||||
// Host -----------------------------------------------------------------------
|
||||
|
||||
// Host adds a matcher for the URL host.
|
||||
// It accepts a template with zero or more URL variables enclosed by {}.
|
||||
// Variables can define an optional regexp pattern to be matched:
|
||||
//
|
||||
// - {name} matches anything until the next dot.
|
||||
//
|
||||
// - {name:pattern} matches the given regexp pattern.
|
||||
//
|
||||
// For example:
|
||||
//
|
||||
// r := mux.NewRouter()
|
||||
// r.Host("www.example.com")
|
||||
// r.Host("{subdomain}.domain.com")
|
||||
// r.Host("{subdomain:[a-z]+}.domain.com")
|
||||
//
|
||||
// Variable names must be unique in a given route. They can be retrieved
|
||||
// calling mux.Vars(request).
|
||||
func (r *Route) Host(tpl string) *Route {
|
||||
r.err = r.addRegexpMatcher(tpl, regexpTypeHost)
|
||||
return r
|
||||
}
|
||||
|
||||
// MatcherFunc ----------------------------------------------------------------
|
||||
|
||||
// MatcherFunc is the function signature used by custom matchers.
|
||||
type MatcherFunc func(*http.Request, *RouteMatch) bool
|
||||
|
||||
// Match returns the match for a given request.
|
||||
func (m MatcherFunc) Match(r *http.Request, match *RouteMatch) bool {
|
||||
return m(r, match)
|
||||
}
|
||||
|
||||
// MatcherFunc adds a custom function to be used as request matcher.
|
||||
func (r *Route) MatcherFunc(f MatcherFunc) *Route {
|
||||
return r.addMatcher(f)
|
||||
}
|
||||
|
||||
// Methods --------------------------------------------------------------------
|
||||
|
||||
// methodMatcher matches the request against HTTP methods.
|
||||
type methodMatcher []string
|
||||
|
||||
func (m methodMatcher) Match(r *http.Request, match *RouteMatch) bool {
|
||||
return matchInArray(m, r.Method)
|
||||
}
|
||||
|
||||
// Methods adds a matcher for HTTP methods.
|
||||
// It accepts a sequence of one or more methods to be matched, e.g.:
|
||||
// "GET", "POST", "PUT".
|
||||
func (r *Route) Methods(methods ...string) *Route {
|
||||
for k, v := range methods {
|
||||
methods[k] = strings.ToUpper(v)
|
||||
}
|
||||
return r.addMatcher(methodMatcher(methods))
|
||||
}
|
||||
|
||||
// Path -----------------------------------------------------------------------
|
||||
|
||||
// Path adds a matcher for the URL path.
|
||||
// It accepts a template with zero or more URL variables enclosed by {}. The
|
||||
// template must start with a "/".
|
||||
// Variables can define an optional regexp pattern to be matched:
|
||||
//
|
||||
// - {name} matches anything until the next slash.
|
||||
//
|
||||
// - {name:pattern} matches the given regexp pattern.
|
||||
//
|
||||
// For example:
|
||||
//
|
||||
// r := mux.NewRouter()
|
||||
// r.Path("/products/").Handler(ProductsHandler)
|
||||
// r.Path("/products/{key}").Handler(ProductsHandler)
|
||||
// r.Path("/articles/{category}/{id:[0-9]+}").
|
||||
// Handler(ArticleHandler)
|
||||
//
|
||||
// Variable names must be unique in a given route. They can be retrieved
|
||||
// calling mux.Vars(request).
|
||||
func (r *Route) Path(tpl string) *Route {
|
||||
r.err = r.addRegexpMatcher(tpl, regexpTypePath)
|
||||
return r
|
||||
}
|
||||
|
||||
// PathPrefix -----------------------------------------------------------------
|
||||
|
||||
// PathPrefix adds a matcher for the URL path prefix. This matches if the given
|
||||
// template is a prefix of the full URL path. See Route.Path() for details on
|
||||
// the tpl argument.
|
||||
//
|
||||
// Note that it does not treat slashes specially ("/foobar/" will be matched by
|
||||
// the prefix "/foo") so you may want to use a trailing slash here.
|
||||
//
|
||||
// Also note that the setting of Router.StrictSlash() has no effect on routes
|
||||
// with a PathPrefix matcher.
|
||||
func (r *Route) PathPrefix(tpl string) *Route {
|
||||
r.err = r.addRegexpMatcher(tpl, regexpTypePrefix)
|
||||
return r
|
||||
}
|
||||
|
||||
// Query ----------------------------------------------------------------------
|
||||
|
||||
// Queries adds a matcher for URL query values.
|
||||
// It accepts a sequence of key/value pairs. Values may define variables.
|
||||
// For example:
|
||||
//
|
||||
// r := mux.NewRouter()
|
||||
// r.Queries("foo", "bar", "id", "{id:[0-9]+}")
|
||||
//
|
||||
// The above route will only match if the URL contains the defined queries
|
||||
// values, e.g.: ?foo=bar&id=42.
|
||||
//
|
||||
// It the value is an empty string, it will match any value if the key is set.
|
||||
//
|
||||
// Variables can define an optional regexp pattern to be matched:
|
||||
//
|
||||
// - {name} matches anything until the next slash.
|
||||
//
|
||||
// - {name:pattern} matches the given regexp pattern.
|
||||
func (r *Route) Queries(pairs ...string) *Route {
|
||||
length := len(pairs)
|
||||
if length%2 != 0 {
|
||||
r.err = fmt.Errorf(
|
||||
"mux: number of parameters must be multiple of 2, got %v", pairs)
|
||||
return nil
|
||||
}
|
||||
for i := 0; i < length; i += 2 {
|
||||
if r.err = r.addRegexpMatcher(pairs[i]+"="+pairs[i+1], regexpTypeQuery); r.err != nil {
|
||||
return r
|
||||
}
|
||||
}
|
||||
|
||||
return r
|
||||
}
|
||||
|
||||
// Schemes --------------------------------------------------------------------
|
||||
|
||||
// schemeMatcher matches the request against URL schemes.
|
||||
type schemeMatcher []string
|
||||
|
||||
func (m schemeMatcher) Match(r *http.Request, match *RouteMatch) bool {
|
||||
return matchInArray(m, r.URL.Scheme)
|
||||
}
|
||||
|
||||
// Schemes adds a matcher for URL schemes.
|
||||
// It accepts a sequence of schemes to be matched, e.g.: "http", "https".
|
||||
func (r *Route) Schemes(schemes ...string) *Route {
|
||||
for k, v := range schemes {
|
||||
schemes[k] = strings.ToLower(v)
|
||||
}
|
||||
if r.buildScheme == "" && len(schemes) > 0 {
|
||||
r.buildScheme = schemes[0]
|
||||
}
|
||||
return r.addMatcher(schemeMatcher(schemes))
|
||||
}
|
||||
|
||||
// BuildVarsFunc --------------------------------------------------------------
|
||||
|
||||
// BuildVarsFunc is the function signature used by custom build variable
|
||||
// functions (which can modify route variables before a route's URL is built).
|
||||
type BuildVarsFunc func(map[string]string) map[string]string
|
||||
|
||||
// BuildVarsFunc adds a custom function to be used to modify build variables
|
||||
// before a route's URL is built.
|
||||
func (r *Route) BuildVarsFunc(f BuildVarsFunc) *Route {
|
||||
r.buildVarsFunc = f
|
||||
return r
|
||||
}
|
||||
|
||||
// Subrouter ------------------------------------------------------------------
|
||||
|
||||
// Subrouter creates a subrouter for the route.
|
||||
//
|
||||
// It will test the inner routes only if the parent route matched. For example:
|
||||
//
|
||||
// r := mux.NewRouter()
|
||||
// s := r.Host("www.example.com").Subrouter()
|
||||
// s.HandleFunc("/products/", ProductsHandler)
|
||||
// s.HandleFunc("/products/{key}", ProductHandler)
|
||||
// s.HandleFunc("/articles/{category}/{id:[0-9]+}"), ArticleHandler)
|
||||
//
|
||||
// Here, the routes registered in the subrouter won't be tested if the host
|
||||
// doesn't match.
|
||||
func (r *Route) Subrouter() *Router {
|
||||
router := &Router{parent: r, strictSlash: r.strictSlash}
|
||||
r.addMatcher(router)
|
||||
return router
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// URL building
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
// URL builds a URL for the route.
|
||||
//
|
||||
// It accepts a sequence of key/value pairs for the route variables. For
|
||||
// example, given this route:
|
||||
//
|
||||
// r := mux.NewRouter()
|
||||
// r.HandleFunc("/articles/{category}/{id:[0-9]+}", ArticleHandler).
|
||||
// Name("article")
|
||||
//
|
||||
// ...a URL for it can be built using:
|
||||
//
|
||||
// url, err := r.Get("article").URL("category", "technology", "id", "42")
|
||||
//
|
||||
// ...which will return an url.URL with the following path:
|
||||
//
|
||||
// "/articles/technology/42"
|
||||
//
|
||||
// This also works for host variables:
|
||||
//
|
||||
// r := mux.NewRouter()
|
||||
// r.Host("{subdomain}.domain.com").
|
||||
// HandleFunc("/articles/{category}/{id:[0-9]+}", ArticleHandler).
|
||||
// Name("article")
|
||||
//
|
||||
// // url.String() will be "http://news.domain.com/articles/technology/42"
|
||||
// url, err := r.Get("article").URL("subdomain", "news",
|
||||
// "category", "technology",
|
||||
// "id", "42")
|
||||
//
|
||||
// All variables defined in the route are required, and their values must
|
||||
// conform to the corresponding patterns.
|
||||
func (r *Route) URL(pairs ...string) (*url.URL, error) {
|
||||
if r.err != nil {
|
||||
return nil, r.err
|
||||
}
|
||||
if r.regexp == nil {
|
||||
return nil, errors.New("mux: route doesn't have a host or path")
|
||||
}
|
||||
values, err := r.prepareVars(pairs...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var scheme, host, path string
|
||||
queries := make([]string, 0, len(r.regexp.queries))
|
||||
if r.regexp.host != nil {
|
||||
if host, err = r.regexp.host.url(values); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
scheme = "http"
|
||||
if s := r.getBuildScheme(); s != "" {
|
||||
scheme = s
|
||||
}
|
||||
}
|
||||
if r.regexp.path != nil {
|
||||
if path, err = r.regexp.path.url(values); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
for _, q := range r.regexp.queries {
|
||||
var query string
|
||||
if query, err = q.url(values); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
queries = append(queries, query)
|
||||
}
|
||||
return &url.URL{
|
||||
Scheme: scheme,
|
||||
Host: host,
|
||||
Path: path,
|
||||
RawQuery: strings.Join(queries, "&"),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// URLHost builds the host part of the URL for a route. See Route.URL().
|
||||
//
|
||||
// The route must have a host defined.
|
||||
func (r *Route) URLHost(pairs ...string) (*url.URL, error) {
|
||||
if r.err != nil {
|
||||
return nil, r.err
|
||||
}
|
||||
if r.regexp == nil || r.regexp.host == nil {
|
||||
return nil, errors.New("mux: route doesn't have a host")
|
||||
}
|
||||
values, err := r.prepareVars(pairs...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
host, err := r.regexp.host.url(values)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
u := &url.URL{
|
||||
Scheme: "http",
|
||||
Host: host,
|
||||
}
|
||||
if s := r.getBuildScheme(); s != "" {
|
||||
u.Scheme = s
|
||||
}
|
||||
return u, nil
|
||||
}
|
||||
|
||||
// URLPath builds the path part of the URL for a route. See Route.URL().
|
||||
//
|
||||
// The route must have a path defined.
|
||||
func (r *Route) URLPath(pairs ...string) (*url.URL, error) {
|
||||
if r.err != nil {
|
||||
return nil, r.err
|
||||
}
|
||||
if r.regexp == nil || r.regexp.path == nil {
|
||||
return nil, errors.New("mux: route doesn't have a path")
|
||||
}
|
||||
values, err := r.prepareVars(pairs...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
path, err := r.regexp.path.url(values)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &url.URL{
|
||||
Path: path,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// GetPathTemplate returns the template used to build the
|
||||
// route match.
|
||||
// This is useful for building simple REST API documentation and for instrumentation
|
||||
// against third-party services.
|
||||
// An error will be returned if the route does not define a path.
|
||||
func (r *Route) GetPathTemplate() (string, error) {
|
||||
if r.err != nil {
|
||||
return "", r.err
|
||||
}
|
||||
if r.regexp == nil || r.regexp.path == nil {
|
||||
return "", errors.New("mux: route doesn't have a path")
|
||||
}
|
||||
return r.regexp.path.template, nil
|
||||
}
|
||||
|
||||
// GetPathRegexp returns the expanded regular expression used to match route path.
|
||||
// This is useful for building simple REST API documentation and for instrumentation
|
||||
// against third-party services.
|
||||
// An error will be returned if the route does not define a path.
|
||||
func (r *Route) GetPathRegexp() (string, error) {
|
||||
if r.err != nil {
|
||||
return "", r.err
|
||||
}
|
||||
if r.regexp == nil || r.regexp.path == nil {
|
||||
return "", errors.New("mux: route does not have a path")
|
||||
}
|
||||
return r.regexp.path.regexp.String(), nil
|
||||
}
|
||||
|
||||
// GetQueriesRegexp returns the expanded regular expressions used to match the
|
||||
// route queries.
|
||||
// This is useful for building simple REST API documentation and for instrumentation
|
||||
// against third-party services.
|
||||
// An error will be returned if the route does not have queries.
|
||||
func (r *Route) GetQueriesRegexp() ([]string, error) {
|
||||
if r.err != nil {
|
||||
return nil, r.err
|
||||
}
|
||||
if r.regexp == nil || r.regexp.queries == nil {
|
||||
return nil, errors.New("mux: route doesn't have queries")
|
||||
}
|
||||
var queries []string
|
||||
for _, query := range r.regexp.queries {
|
||||
queries = append(queries, query.regexp.String())
|
||||
}
|
||||
return queries, nil
|
||||
}
|
||||
|
||||
// GetQueriesTemplates returns the templates used to build the
|
||||
// query matching.
|
||||
// This is useful for building simple REST API documentation and for instrumentation
|
||||
// against third-party services.
|
||||
// An error will be returned if the route does not define queries.
|
||||
func (r *Route) GetQueriesTemplates() ([]string, error) {
|
||||
if r.err != nil {
|
||||
return nil, r.err
|
||||
}
|
||||
if r.regexp == nil || r.regexp.queries == nil {
|
||||
return nil, errors.New("mux: route doesn't have queries")
|
||||
}
|
||||
var queries []string
|
||||
for _, query := range r.regexp.queries {
|
||||
queries = append(queries, query.template)
|
||||
}
|
||||
return queries, nil
|
||||
}
|
||||
|
||||
// GetMethods returns the methods the route matches against
|
||||
// This is useful for building simple REST API documentation and for instrumentation
|
||||
// against third-party services.
|
||||
// An error will be returned if route does not have methods.
|
||||
func (r *Route) GetMethods() ([]string, error) {
|
||||
if r.err != nil {
|
||||
return nil, r.err
|
||||
}
|
||||
for _, m := range r.matchers {
|
||||
if methods, ok := m.(methodMatcher); ok {
|
||||
return []string(methods), nil
|
||||
}
|
||||
}
|
||||
return nil, errors.New("mux: route doesn't have methods")
|
||||
}
|
||||
|
||||
// GetHostTemplate returns the template used to build the
|
||||
// route match.
|
||||
// This is useful for building simple REST API documentation and for instrumentation
|
||||
// against third-party services.
|
||||
// An error will be returned if the route does not define a host.
|
||||
func (r *Route) GetHostTemplate() (string, error) {
|
||||
if r.err != nil {
|
||||
return "", r.err
|
||||
}
|
||||
if r.regexp == nil || r.regexp.host == nil {
|
||||
return "", errors.New("mux: route doesn't have a host")
|
||||
}
|
||||
return r.regexp.host.template, nil
|
||||
}
|
||||
|
||||
// prepareVars converts the route variable pairs into a map. If the route has a
|
||||
// BuildVarsFunc, it is invoked.
|
||||
func (r *Route) prepareVars(pairs ...string) (map[string]string, error) {
|
||||
m, err := mapFromPairsToString(pairs...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return r.buildVars(m), nil
|
||||
}
|
||||
|
||||
func (r *Route) buildVars(m map[string]string) map[string]string {
|
||||
if r.parent != nil {
|
||||
m = r.parent.buildVars(m)
|
||||
}
|
||||
if r.buildVarsFunc != nil {
|
||||
m = r.buildVarsFunc(m)
|
||||
}
|
||||
return m
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// parentRoute
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
// parentRoute allows routes to know about parent host and path definitions.
|
||||
type parentRoute interface {
|
||||
getBuildScheme() string
|
||||
getNamedRoutes() map[string]*Route
|
||||
getRegexpGroup() *routeRegexpGroup
|
||||
buildVars(map[string]string) map[string]string
|
||||
}
|
||||
|
||||
func (r *Route) getBuildScheme() string {
|
||||
if r.buildScheme != "" {
|
||||
return r.buildScheme
|
||||
}
|
||||
if r.parent != nil {
|
||||
return r.parent.getBuildScheme()
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// getNamedRoutes returns the map where named routes are registered.
|
||||
func (r *Route) getNamedRoutes() map[string]*Route {
|
||||
if r.parent == nil {
|
||||
// During tests router is not always set.
|
||||
r.parent = NewRouter()
|
||||
}
|
||||
return r.parent.getNamedRoutes()
|
||||
}
|
||||
|
||||
// getRegexpGroup returns regexp definitions from this route.
|
||||
func (r *Route) getRegexpGroup() *routeRegexpGroup {
|
||||
if r.regexp == nil {
|
||||
if r.parent == nil {
|
||||
// During tests router is not always set.
|
||||
r.parent = NewRouter()
|
||||
}
|
||||
regexp := r.parent.getRegexpGroup()
|
||||
if regexp == nil {
|
||||
r.regexp = new(routeRegexpGroup)
|
||||
} else {
|
||||
// Copy.
|
||||
r.regexp = &routeRegexpGroup{
|
||||
host: regexp.host,
|
||||
path: regexp.path,
|
||||
queries: regexp.queries,
|
||||
}
|
||||
}
|
||||
}
|
||||
return r.regexp
|
||||
}
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
// Copyright 2012 The Gorilla Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package mux
|
||||
|
||||
import "net/http"
|
||||
|
||||
// SetURLVars sets the URL variables for the given request, to be accessed via
|
||||
// mux.Vars for testing route behaviour. Arguments are not modified, a shallow
|
||||
// copy is returned.
|
||||
//
|
||||
// This API should only be used for testing purposes; it provides a way to
|
||||
// inject variables into the request context. Alternatively, URL variables
|
||||
// can be set by making a route that captures the required variables,
|
||||
// starting a server and sending the request to that server.
|
||||
func SetURLVars(r *http.Request, val map[string]string) *http.Request {
|
||||
return setVars(r, val)
|
||||
}
|
||||
-1
Submodule vendor/github.com/gorilla/websocket deleted from 95ba29eb98
+25
@@ -0,0 +1,25 @@
|
||||
# Compiled Object files, Static and Dynamic libs (Shared Objects)
|
||||
*.o
|
||||
*.a
|
||||
*.so
|
||||
|
||||
# Folders
|
||||
_obj
|
||||
_test
|
||||
|
||||
# Architecture specific extensions/prefixes
|
||||
*.[568vq]
|
||||
[568vq].out
|
||||
|
||||
*.cgo1.go
|
||||
*.cgo2.c
|
||||
_cgo_defun.c
|
||||
_cgo_gotypes.go
|
||||
_cgo_export.*
|
||||
|
||||
_testmain.go
|
||||
|
||||
*.exe
|
||||
|
||||
.idea/
|
||||
*.iml
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
language: go
|
||||
sudo: false
|
||||
|
||||
matrix:
|
||||
include:
|
||||
- go: 1.7.x
|
||||
- go: 1.8.x
|
||||
- go: 1.9.x
|
||||
- go: 1.10.x
|
||||
- go: 1.11.x
|
||||
- go: tip
|
||||
allow_failures:
|
||||
- go: tip
|
||||
|
||||
script:
|
||||
- go get -t -v ./...
|
||||
- diff -u <(echo -n) <(gofmt -d .)
|
||||
- go vet $(go list ./... | grep -v /vendor/)
|
||||
- go test -v -race ./...
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
# This is the official list of Gorilla WebSocket authors for copyright
|
||||
# purposes.
|
||||
#
|
||||
# Please keep the list sorted.
|
||||
|
||||
Gary Burd <gary@beagledreams.com>
|
||||
Google LLC (https://opensource.google.com/)
|
||||
Joachim Bauch <mail@joachim-bauch.de>
|
||||
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
Copyright (c) 2013 The Gorilla WebSocket Authors. 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.
|
||||
+64
@@ -0,0 +1,64 @@
|
||||
# Gorilla WebSocket
|
||||
|
||||
Gorilla WebSocket is a [Go](http://golang.org/) implementation of the
|
||||
[WebSocket](http://www.rfc-editor.org/rfc/rfc6455.txt) protocol.
|
||||
|
||||
[](https://travis-ci.org/gorilla/websocket)
|
||||
[](https://godoc.org/github.com/gorilla/websocket)
|
||||
|
||||
### Documentation
|
||||
|
||||
* [API Reference](http://godoc.org/github.com/gorilla/websocket)
|
||||
* [Chat example](https://github.com/gorilla/websocket/tree/master/examples/chat)
|
||||
* [Command example](https://github.com/gorilla/websocket/tree/master/examples/command)
|
||||
* [Client and server example](https://github.com/gorilla/websocket/tree/master/examples/echo)
|
||||
* [File watch example](https://github.com/gorilla/websocket/tree/master/examples/filewatch)
|
||||
|
||||
### Status
|
||||
|
||||
The Gorilla WebSocket package provides a complete and tested implementation of
|
||||
the [WebSocket](http://www.rfc-editor.org/rfc/rfc6455.txt) protocol. The
|
||||
package API is stable.
|
||||
|
||||
### Installation
|
||||
|
||||
go get github.com/gorilla/websocket
|
||||
|
||||
### Protocol Compliance
|
||||
|
||||
The Gorilla WebSocket package passes the server tests in the [Autobahn Test
|
||||
Suite](http://autobahn.ws/testsuite) using the application in the [examples/autobahn
|
||||
subdirectory](https://github.com/gorilla/websocket/tree/master/examples/autobahn).
|
||||
|
||||
### Gorilla WebSocket compared with other packages
|
||||
|
||||
<table>
|
||||
<tr>
|
||||
<th></th>
|
||||
<th><a href="http://godoc.org/github.com/gorilla/websocket">github.com/gorilla</a></th>
|
||||
<th><a href="http://godoc.org/golang.org/x/net/websocket">golang.org/x/net</a></th>
|
||||
</tr>
|
||||
<tr>
|
||||
<tr><td colspan="3"><a href="http://tools.ietf.org/html/rfc6455">RFC 6455</a> Features</td></tr>
|
||||
<tr><td>Passes <a href="http://autobahn.ws/testsuite/">Autobahn Test Suite</a></td><td><a href="https://github.com/gorilla/websocket/tree/master/examples/autobahn">Yes</a></td><td>No</td></tr>
|
||||
<tr><td>Receive <a href="https://tools.ietf.org/html/rfc6455#section-5.4">fragmented</a> message<td>Yes</td><td><a href="https://code.google.com/p/go/issues/detail?id=7632">No</a>, see note 1</td></tr>
|
||||
<tr><td>Send <a href="https://tools.ietf.org/html/rfc6455#section-5.5.1">close</a> message</td><td><a href="http://godoc.org/github.com/gorilla/websocket#hdr-Control_Messages">Yes</a></td><td><a href="https://code.google.com/p/go/issues/detail?id=4588">No</a></td></tr>
|
||||
<tr><td>Send <a href="https://tools.ietf.org/html/rfc6455#section-5.5.2">pings</a> and receive <a href="https://tools.ietf.org/html/rfc6455#section-5.5.3">pongs</a></td><td><a href="http://godoc.org/github.com/gorilla/websocket#hdr-Control_Messages">Yes</a></td><td>No</td></tr>
|
||||
<tr><td>Get the <a href="https://tools.ietf.org/html/rfc6455#section-5.6">type</a> of a received data message</td><td>Yes</td><td>Yes, see note 2</td></tr>
|
||||
<tr><td colspan="3">Other Features</tr></td>
|
||||
<tr><td><a href="https://tools.ietf.org/html/rfc7692">Compression Extensions</a></td><td>Experimental</td><td>No</td></tr>
|
||||
<tr><td>Read message using io.Reader</td><td><a href="http://godoc.org/github.com/gorilla/websocket#Conn.NextReader">Yes</a></td><td>No, see note 3</td></tr>
|
||||
<tr><td>Write message using io.WriteCloser</td><td><a href="http://godoc.org/github.com/gorilla/websocket#Conn.NextWriter">Yes</a></td><td>No, see note 3</td></tr>
|
||||
</table>
|
||||
|
||||
Notes:
|
||||
|
||||
1. Large messages are fragmented in [Chrome's new WebSocket implementation](http://www.ietf.org/mail-archive/web/hybi/current/msg10503.html).
|
||||
2. The application can get the type of a received data message by implementing
|
||||
a [Codec marshal](http://godoc.org/golang.org/x/net/websocket#Codec.Marshal)
|
||||
function.
|
||||
3. The go.net io.Reader and io.Writer operate across WebSocket frame boundaries.
|
||||
Read returns when the input buffer is full or a frame boundary is
|
||||
encountered. Each call to Write sends a single frame message. The Gorilla
|
||||
io.Reader and io.WriteCloser operate on a single WebSocket message.
|
||||
|
||||
+395
@@ -0,0 +1,395 @@
|
||||
// Copyright 2013 The Gorilla WebSocket Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package websocket
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"errors"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/httptrace"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// ErrBadHandshake is returned when the server response to opening handshake is
|
||||
// invalid.
|
||||
var ErrBadHandshake = errors.New("websocket: bad handshake")
|
||||
|
||||
var errInvalidCompression = errors.New("websocket: invalid compression negotiation")
|
||||
|
||||
// NewClient creates a new client connection using the given net connection.
|
||||
// The URL u specifies the host and request URI. Use requestHeader to specify
|
||||
// the origin (Origin), subprotocols (Sec-WebSocket-Protocol) and cookies
|
||||
// (Cookie). Use the response.Header to get the selected subprotocol
|
||||
// (Sec-WebSocket-Protocol) and cookies (Set-Cookie).
|
||||
//
|
||||
// If the WebSocket handshake fails, ErrBadHandshake is returned along with a
|
||||
// non-nil *http.Response so that callers can handle redirects, authentication,
|
||||
// etc.
|
||||
//
|
||||
// Deprecated: Use Dialer instead.
|
||||
func NewClient(netConn net.Conn, u *url.URL, requestHeader http.Header, readBufSize, writeBufSize int) (c *Conn, response *http.Response, err error) {
|
||||
d := Dialer{
|
||||
ReadBufferSize: readBufSize,
|
||||
WriteBufferSize: writeBufSize,
|
||||
NetDial: func(net, addr string) (net.Conn, error) {
|
||||
return netConn, nil
|
||||
},
|
||||
}
|
||||
return d.Dial(u.String(), requestHeader)
|
||||
}
|
||||
|
||||
// A Dialer contains options for connecting to WebSocket server.
|
||||
type Dialer struct {
|
||||
// NetDial specifies the dial function for creating TCP connections. If
|
||||
// NetDial is nil, net.Dial is used.
|
||||
NetDial func(network, addr string) (net.Conn, error)
|
||||
|
||||
// NetDialContext specifies the dial function for creating TCP connections. If
|
||||
// NetDialContext is nil, net.DialContext is used.
|
||||
NetDialContext func(ctx context.Context, network, addr string) (net.Conn, error)
|
||||
|
||||
// Proxy specifies a function to return a proxy for a given
|
||||
// Request. If the function returns a non-nil error, the
|
||||
// request is aborted with the provided error.
|
||||
// If Proxy is nil or returns a nil *URL, no proxy is used.
|
||||
Proxy func(*http.Request) (*url.URL, error)
|
||||
|
||||
// TLSClientConfig specifies the TLS configuration to use with tls.Client.
|
||||
// If nil, the default configuration is used.
|
||||
TLSClientConfig *tls.Config
|
||||
|
||||
// HandshakeTimeout specifies the duration for the handshake to complete.
|
||||
HandshakeTimeout time.Duration
|
||||
|
||||
// ReadBufferSize and WriteBufferSize specify I/O buffer sizes. If a buffer
|
||||
// size is zero, then a useful default size is used. The I/O buffer sizes
|
||||
// do not limit the size of the messages that can be sent or received.
|
||||
ReadBufferSize, WriteBufferSize int
|
||||
|
||||
// WriteBufferPool is a pool of buffers for write operations. If the value
|
||||
// is not set, then write buffers are allocated to the connection for the
|
||||
// lifetime of the connection.
|
||||
//
|
||||
// A pool is most useful when the application has a modest volume of writes
|
||||
// across a large number of connections.
|
||||
//
|
||||
// Applications should use a single pool for each unique value of
|
||||
// WriteBufferSize.
|
||||
WriteBufferPool BufferPool
|
||||
|
||||
// Subprotocols specifies the client's requested subprotocols.
|
||||
Subprotocols []string
|
||||
|
||||
// EnableCompression specifies if the client should attempt to negotiate
|
||||
// per message compression (RFC 7692). Setting this value to true does not
|
||||
// guarantee that compression will be supported. Currently only "no context
|
||||
// takeover" modes are supported.
|
||||
EnableCompression bool
|
||||
|
||||
// Jar specifies the cookie jar.
|
||||
// If Jar is nil, cookies are not sent in requests and ignored
|
||||
// in responses.
|
||||
Jar http.CookieJar
|
||||
}
|
||||
|
||||
// Dial creates a new client connection by calling DialContext with a background context.
|
||||
func (d *Dialer) Dial(urlStr string, requestHeader http.Header) (*Conn, *http.Response, error) {
|
||||
return d.DialContext(context.Background(), urlStr, requestHeader)
|
||||
}
|
||||
|
||||
var errMalformedURL = errors.New("malformed ws or wss URL")
|
||||
|
||||
func hostPortNoPort(u *url.URL) (hostPort, hostNoPort string) {
|
||||
hostPort = u.Host
|
||||
hostNoPort = u.Host
|
||||
if i := strings.LastIndex(u.Host, ":"); i > strings.LastIndex(u.Host, "]") {
|
||||
hostNoPort = hostNoPort[:i]
|
||||
} else {
|
||||
switch u.Scheme {
|
||||
case "wss":
|
||||
hostPort += ":443"
|
||||
case "https":
|
||||
hostPort += ":443"
|
||||
default:
|
||||
hostPort += ":80"
|
||||
}
|
||||
}
|
||||
return hostPort, hostNoPort
|
||||
}
|
||||
|
||||
// DefaultDialer is a dialer with all fields set to the default values.
|
||||
var DefaultDialer = &Dialer{
|
||||
Proxy: http.ProxyFromEnvironment,
|
||||
HandshakeTimeout: 45 * time.Second,
|
||||
}
|
||||
|
||||
// nilDialer is dialer to use when receiver is nil.
|
||||
var nilDialer = *DefaultDialer
|
||||
|
||||
// DialContext creates a new client connection. Use requestHeader to specify the
|
||||
// origin (Origin), subprotocols (Sec-WebSocket-Protocol) and cookies (Cookie).
|
||||
// Use the response.Header to get the selected subprotocol
|
||||
// (Sec-WebSocket-Protocol) and cookies (Set-Cookie).
|
||||
//
|
||||
// The context will be used in the request and in the Dialer
|
||||
//
|
||||
// If the WebSocket handshake fails, ErrBadHandshake is returned along with a
|
||||
// non-nil *http.Response so that callers can handle redirects, authentication,
|
||||
// etcetera. The response body may not contain the entire response and does not
|
||||
// need to be closed by the application.
|
||||
func (d *Dialer) DialContext(ctx context.Context, urlStr string, requestHeader http.Header) (*Conn, *http.Response, error) {
|
||||
if d == nil {
|
||||
d = &nilDialer
|
||||
}
|
||||
|
||||
challengeKey, err := generateChallengeKey()
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
u, err := url.Parse(urlStr)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
switch u.Scheme {
|
||||
case "ws":
|
||||
u.Scheme = "http"
|
||||
case "wss":
|
||||
u.Scheme = "https"
|
||||
default:
|
||||
return nil, nil, errMalformedURL
|
||||
}
|
||||
|
||||
if u.User != nil {
|
||||
// User name and password are not allowed in websocket URIs.
|
||||
return nil, nil, errMalformedURL
|
||||
}
|
||||
|
||||
req := &http.Request{
|
||||
Method: "GET",
|
||||
URL: u,
|
||||
Proto: "HTTP/1.1",
|
||||
ProtoMajor: 1,
|
||||
ProtoMinor: 1,
|
||||
Header: make(http.Header),
|
||||
Host: u.Host,
|
||||
}
|
||||
req = req.WithContext(ctx)
|
||||
|
||||
// Set the cookies present in the cookie jar of the dialer
|
||||
if d.Jar != nil {
|
||||
for _, cookie := range d.Jar.Cookies(u) {
|
||||
req.AddCookie(cookie)
|
||||
}
|
||||
}
|
||||
|
||||
// Set the request headers using the capitalization for names and values in
|
||||
// RFC examples. Although the capitalization shouldn't matter, there are
|
||||
// servers that depend on it. The Header.Set method is not used because the
|
||||
// method canonicalizes the header names.
|
||||
req.Header["Upgrade"] = []string{"websocket"}
|
||||
req.Header["Connection"] = []string{"Upgrade"}
|
||||
req.Header["Sec-WebSocket-Key"] = []string{challengeKey}
|
||||
req.Header["Sec-WebSocket-Version"] = []string{"13"}
|
||||
if len(d.Subprotocols) > 0 {
|
||||
req.Header["Sec-WebSocket-Protocol"] = []string{strings.Join(d.Subprotocols, ", ")}
|
||||
}
|
||||
for k, vs := range requestHeader {
|
||||
switch {
|
||||
case k == "Host":
|
||||
if len(vs) > 0 {
|
||||
req.Host = vs[0]
|
||||
}
|
||||
case k == "Upgrade" ||
|
||||
k == "Connection" ||
|
||||
k == "Sec-Websocket-Key" ||
|
||||
k == "Sec-Websocket-Version" ||
|
||||
k == "Sec-Websocket-Extensions" ||
|
||||
(k == "Sec-Websocket-Protocol" && len(d.Subprotocols) > 0):
|
||||
return nil, nil, errors.New("websocket: duplicate header not allowed: " + k)
|
||||
case k == "Sec-Websocket-Protocol":
|
||||
req.Header["Sec-WebSocket-Protocol"] = vs
|
||||
default:
|
||||
req.Header[k] = vs
|
||||
}
|
||||
}
|
||||
|
||||
if d.EnableCompression {
|
||||
req.Header["Sec-WebSocket-Extensions"] = []string{"permessage-deflate; server_no_context_takeover; client_no_context_takeover"}
|
||||
}
|
||||
|
||||
if d.HandshakeTimeout != 0 {
|
||||
var cancel func()
|
||||
ctx, cancel = context.WithTimeout(ctx, d.HandshakeTimeout)
|
||||
defer cancel()
|
||||
}
|
||||
|
||||
// Get network dial function.
|
||||
var netDial func(network, add string) (net.Conn, error)
|
||||
|
||||
if d.NetDialContext != nil {
|
||||
netDial = func(network, addr string) (net.Conn, error) {
|
||||
return d.NetDialContext(ctx, network, addr)
|
||||
}
|
||||
} else if d.NetDial != nil {
|
||||
netDial = d.NetDial
|
||||
} else {
|
||||
netDialer := &net.Dialer{}
|
||||
netDial = func(network, addr string) (net.Conn, error) {
|
||||
return netDialer.DialContext(ctx, network, addr)
|
||||
}
|
||||
}
|
||||
|
||||
// If needed, wrap the dial function to set the connection deadline.
|
||||
if deadline, ok := ctx.Deadline(); ok {
|
||||
forwardDial := netDial
|
||||
netDial = func(network, addr string) (net.Conn, error) {
|
||||
c, err := forwardDial(network, addr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err = c.SetDeadline(deadline)
|
||||
if err != nil {
|
||||
c.Close()
|
||||
return nil, err
|
||||
}
|
||||
return c, nil
|
||||
}
|
||||
}
|
||||
|
||||
// If needed, wrap the dial function to connect through a proxy.
|
||||
if d.Proxy != nil {
|
||||
proxyURL, err := d.Proxy(req)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
if proxyURL != nil {
|
||||
dialer, err := proxy_FromURL(proxyURL, netDialerFunc(netDial))
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
netDial = dialer.Dial
|
||||
}
|
||||
}
|
||||
|
||||
hostPort, hostNoPort := hostPortNoPort(u)
|
||||
trace := httptrace.ContextClientTrace(ctx)
|
||||
if trace != nil && trace.GetConn != nil {
|
||||
trace.GetConn(hostPort)
|
||||
}
|
||||
|
||||
netConn, err := netDial("tcp", hostPort)
|
||||
if trace != nil && trace.GotConn != nil {
|
||||
trace.GotConn(httptrace.GotConnInfo{
|
||||
Conn: netConn,
|
||||
})
|
||||
}
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
defer func() {
|
||||
if netConn != nil {
|
||||
netConn.Close()
|
||||
}
|
||||
}()
|
||||
|
||||
if u.Scheme == "https" {
|
||||
cfg := cloneTLSConfig(d.TLSClientConfig)
|
||||
if cfg.ServerName == "" {
|
||||
cfg.ServerName = hostNoPort
|
||||
}
|
||||
tlsConn := tls.Client(netConn, cfg)
|
||||
netConn = tlsConn
|
||||
|
||||
var err error
|
||||
if trace != nil {
|
||||
err = doHandshakeWithTrace(trace, tlsConn, cfg)
|
||||
} else {
|
||||
err = doHandshake(tlsConn, cfg)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
}
|
||||
|
||||
conn := newConn(netConn, false, d.ReadBufferSize, d.WriteBufferSize, d.WriteBufferPool, nil, nil)
|
||||
|
||||
if err := req.Write(netConn); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
if trace != nil && trace.GotFirstResponseByte != nil {
|
||||
if peek, err := conn.br.Peek(1); err == nil && len(peek) == 1 {
|
||||
trace.GotFirstResponseByte()
|
||||
}
|
||||
}
|
||||
|
||||
resp, err := http.ReadResponse(conn.br, req)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
if d.Jar != nil {
|
||||
if rc := resp.Cookies(); len(rc) > 0 {
|
||||
d.Jar.SetCookies(u, rc)
|
||||
}
|
||||
}
|
||||
|
||||
if resp.StatusCode != 101 ||
|
||||
!strings.EqualFold(resp.Header.Get("Upgrade"), "websocket") ||
|
||||
!strings.EqualFold(resp.Header.Get("Connection"), "upgrade") ||
|
||||
resp.Header.Get("Sec-Websocket-Accept") != computeAcceptKey(challengeKey) {
|
||||
// Before closing the network connection on return from this
|
||||
// function, slurp up some of the response to aid application
|
||||
// debugging.
|
||||
buf := make([]byte, 1024)
|
||||
n, _ := io.ReadFull(resp.Body, buf)
|
||||
resp.Body = ioutil.NopCloser(bytes.NewReader(buf[:n]))
|
||||
return nil, resp, ErrBadHandshake
|
||||
}
|
||||
|
||||
for _, ext := range parseExtensions(resp.Header) {
|
||||
if ext[""] != "permessage-deflate" {
|
||||
continue
|
||||
}
|
||||
_, snct := ext["server_no_context_takeover"]
|
||||
_, cnct := ext["client_no_context_takeover"]
|
||||
if !snct || !cnct {
|
||||
return nil, resp, errInvalidCompression
|
||||
}
|
||||
conn.newCompressionWriter = compressNoContextTakeover
|
||||
conn.newDecompressionReader = decompressNoContextTakeover
|
||||
break
|
||||
}
|
||||
|
||||
resp.Body = ioutil.NopCloser(bytes.NewReader([]byte{}))
|
||||
conn.subprotocol = resp.Header.Get("Sec-Websocket-Protocol")
|
||||
|
||||
netConn.SetDeadline(time.Time{})
|
||||
netConn = nil // to avoid close in defer.
|
||||
return conn, resp, nil
|
||||
}
|
||||
|
||||
func doHandshake(tlsConn *tls.Conn, cfg *tls.Config) error {
|
||||
if err := tlsConn.Handshake(); err != nil {
|
||||
return err
|
||||
}
|
||||
if !cfg.InsecureSkipVerify {
|
||||
if err := tlsConn.VerifyHostname(cfg.ServerName); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
// Copyright 2013 The Gorilla WebSocket Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// +build go1.8
|
||||
|
||||
package websocket
|
||||
|
||||
import "crypto/tls"
|
||||
|
||||
func cloneTLSConfig(cfg *tls.Config) *tls.Config {
|
||||
if cfg == nil {
|
||||
return &tls.Config{}
|
||||
}
|
||||
return cfg.Clone()
|
||||
}
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
// Copyright 2013 The Gorilla WebSocket Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// +build !go1.8
|
||||
|
||||
package websocket
|
||||
|
||||
import "crypto/tls"
|
||||
|
||||
// cloneTLSConfig clones all public fields except the fields
|
||||
// SessionTicketsDisabled and SessionTicketKey. This avoids copying the
|
||||
// sync.Mutex in the sync.Once and makes it safe to call cloneTLSConfig on a
|
||||
// config in active use.
|
||||
func cloneTLSConfig(cfg *tls.Config) *tls.Config {
|
||||
if cfg == nil {
|
||||
return &tls.Config{}
|
||||
}
|
||||
return &tls.Config{
|
||||
Rand: cfg.Rand,
|
||||
Time: cfg.Time,
|
||||
Certificates: cfg.Certificates,
|
||||
NameToCertificate: cfg.NameToCertificate,
|
||||
GetCertificate: cfg.GetCertificate,
|
||||
RootCAs: cfg.RootCAs,
|
||||
NextProtos: cfg.NextProtos,
|
||||
ServerName: cfg.ServerName,
|
||||
ClientAuth: cfg.ClientAuth,
|
||||
ClientCAs: cfg.ClientCAs,
|
||||
InsecureSkipVerify: cfg.InsecureSkipVerify,
|
||||
CipherSuites: cfg.CipherSuites,
|
||||
PreferServerCipherSuites: cfg.PreferServerCipherSuites,
|
||||
ClientSessionCache: cfg.ClientSessionCache,
|
||||
MinVersion: cfg.MinVersion,
|
||||
MaxVersion: cfg.MaxVersion,
|
||||
CurvePreferences: cfg.CurvePreferences,
|
||||
}
|
||||
}
|
||||
+148
@@ -0,0 +1,148 @@
|
||||
// Copyright 2017 The Gorilla WebSocket Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package websocket
|
||||
|
||||
import (
|
||||
"compress/flate"
|
||||
"errors"
|
||||
"io"
|
||||
"strings"
|
||||
"sync"
|
||||
)
|
||||
|
||||
const (
|
||||
minCompressionLevel = -2 // flate.HuffmanOnly not defined in Go < 1.6
|
||||
maxCompressionLevel = flate.BestCompression
|
||||
defaultCompressionLevel = 1
|
||||
)
|
||||
|
||||
var (
|
||||
flateWriterPools [maxCompressionLevel - minCompressionLevel + 1]sync.Pool
|
||||
flateReaderPool = sync.Pool{New: func() interface{} {
|
||||
return flate.NewReader(nil)
|
||||
}}
|
||||
)
|
||||
|
||||
func decompressNoContextTakeover(r io.Reader) io.ReadCloser {
|
||||
const tail =
|
||||
// Add four bytes as specified in RFC
|
||||
"\x00\x00\xff\xff" +
|
||||
// Add final block to squelch unexpected EOF error from flate reader.
|
||||
"\x01\x00\x00\xff\xff"
|
||||
|
||||
fr, _ := flateReaderPool.Get().(io.ReadCloser)
|
||||
fr.(flate.Resetter).Reset(io.MultiReader(r, strings.NewReader(tail)), nil)
|
||||
return &flateReadWrapper{fr}
|
||||
}
|
||||
|
||||
func isValidCompressionLevel(level int) bool {
|
||||
return minCompressionLevel <= level && level <= maxCompressionLevel
|
||||
}
|
||||
|
||||
func compressNoContextTakeover(w io.WriteCloser, level int) io.WriteCloser {
|
||||
p := &flateWriterPools[level-minCompressionLevel]
|
||||
tw := &truncWriter{w: w}
|
||||
fw, _ := p.Get().(*flate.Writer)
|
||||
if fw == nil {
|
||||
fw, _ = flate.NewWriter(tw, level)
|
||||
} else {
|
||||
fw.Reset(tw)
|
||||
}
|
||||
return &flateWriteWrapper{fw: fw, tw: tw, p: p}
|
||||
}
|
||||
|
||||
// truncWriter is an io.Writer that writes all but the last four bytes of the
|
||||
// stream to another io.Writer.
|
||||
type truncWriter struct {
|
||||
w io.WriteCloser
|
||||
n int
|
||||
p [4]byte
|
||||
}
|
||||
|
||||
func (w *truncWriter) Write(p []byte) (int, error) {
|
||||
n := 0
|
||||
|
||||
// fill buffer first for simplicity.
|
||||
if w.n < len(w.p) {
|
||||
n = copy(w.p[w.n:], p)
|
||||
p = p[n:]
|
||||
w.n += n
|
||||
if len(p) == 0 {
|
||||
return n, nil
|
||||
}
|
||||
}
|
||||
|
||||
m := len(p)
|
||||
if m > len(w.p) {
|
||||
m = len(w.p)
|
||||
}
|
||||
|
||||
if nn, err := w.w.Write(w.p[:m]); err != nil {
|
||||
return n + nn, err
|
||||
}
|
||||
|
||||
copy(w.p[:], w.p[m:])
|
||||
copy(w.p[len(w.p)-m:], p[len(p)-m:])
|
||||
nn, err := w.w.Write(p[:len(p)-m])
|
||||
return n + nn, err
|
||||
}
|
||||
|
||||
type flateWriteWrapper struct {
|
||||
fw *flate.Writer
|
||||
tw *truncWriter
|
||||
p *sync.Pool
|
||||
}
|
||||
|
||||
func (w *flateWriteWrapper) Write(p []byte) (int, error) {
|
||||
if w.fw == nil {
|
||||
return 0, errWriteClosed
|
||||
}
|
||||
return w.fw.Write(p)
|
||||
}
|
||||
|
||||
func (w *flateWriteWrapper) Close() error {
|
||||
if w.fw == nil {
|
||||
return errWriteClosed
|
||||
}
|
||||
err1 := w.fw.Flush()
|
||||
w.p.Put(w.fw)
|
||||
w.fw = nil
|
||||
if w.tw.p != [4]byte{0, 0, 0xff, 0xff} {
|
||||
return errors.New("websocket: internal error, unexpected bytes at end of flate stream")
|
||||
}
|
||||
err2 := w.tw.w.Close()
|
||||
if err1 != nil {
|
||||
return err1
|
||||
}
|
||||
return err2
|
||||
}
|
||||
|
||||
type flateReadWrapper struct {
|
||||
fr io.ReadCloser
|
||||
}
|
||||
|
||||
func (r *flateReadWrapper) Read(p []byte) (int, error) {
|
||||
if r.fr == nil {
|
||||
return 0, io.ErrClosedPipe
|
||||
}
|
||||
n, err := r.fr.Read(p)
|
||||
if err == io.EOF {
|
||||
// Preemptively place the reader back in the pool. This helps with
|
||||
// scenarios where the application does not call NextReader() soon after
|
||||
// this final read.
|
||||
r.Close()
|
||||
}
|
||||
return n, err
|
||||
}
|
||||
|
||||
func (r *flateReadWrapper) Close() error {
|
||||
if r.fr == nil {
|
||||
return io.ErrClosedPipe
|
||||
}
|
||||
err := r.fr.Close()
|
||||
flateReaderPool.Put(r.fr)
|
||||
r.fr = nil
|
||||
return err
|
||||
}
|
||||
+1165
File diff suppressed because it is too large
Load Diff
+15
@@ -0,0 +1,15 @@
|
||||
// Copyright 2016 The Gorilla WebSocket Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// +build go1.8
|
||||
|
||||
package websocket
|
||||
|
||||
import "net"
|
||||
|
||||
func (c *Conn) writeBufs(bufs ...[]byte) error {
|
||||
b := net.Buffers(bufs)
|
||||
_, err := b.WriteTo(c.conn)
|
||||
return err
|
||||
}
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
// Copyright 2016 The Gorilla WebSocket Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// +build !go1.8
|
||||
|
||||
package websocket
|
||||
|
||||
func (c *Conn) writeBufs(bufs ...[]byte) error {
|
||||
for _, buf := range bufs {
|
||||
if len(buf) > 0 {
|
||||
if _, err := c.conn.Write(buf); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
+180
@@ -0,0 +1,180 @@
|
||||
// Copyright 2013 The Gorilla WebSocket Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// Package websocket implements the WebSocket protocol defined in RFC 6455.
|
||||
//
|
||||
// Overview
|
||||
//
|
||||
// The Conn type represents a WebSocket connection. A server application calls
|
||||
// the Upgrader.Upgrade method from an HTTP request handler to get a *Conn:
|
||||
//
|
||||
// var upgrader = websocket.Upgrader{
|
||||
// ReadBufferSize: 1024,
|
||||
// WriteBufferSize: 1024,
|
||||
// }
|
||||
//
|
||||
// func handler(w http.ResponseWriter, r *http.Request) {
|
||||
// conn, err := upgrader.Upgrade(w, r, nil)
|
||||
// if err != nil {
|
||||
// log.Println(err)
|
||||
// return
|
||||
// }
|
||||
// ... Use conn to send and receive messages.
|
||||
// }
|
||||
//
|
||||
// Call the connection's WriteMessage and ReadMessage methods to send and
|
||||
// receive messages as a slice of bytes. This snippet of code shows how to echo
|
||||
// messages using these methods:
|
||||
//
|
||||
// for {
|
||||
// messageType, p, err := conn.ReadMessage()
|
||||
// if err != nil {
|
||||
// log.Println(err)
|
||||
// return
|
||||
// }
|
||||
// if err := conn.WriteMessage(messageType, p); err != nil {
|
||||
// log.Println(err)
|
||||
// return
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// In above snippet of code, p is a []byte and messageType is an int with value
|
||||
// websocket.BinaryMessage or websocket.TextMessage.
|
||||
//
|
||||
// An application can also send and receive messages using the io.WriteCloser
|
||||
// and io.Reader interfaces. To send a message, call the connection NextWriter
|
||||
// method to get an io.WriteCloser, write the message to the writer and close
|
||||
// the writer when done. To receive a message, call the connection NextReader
|
||||
// method to get an io.Reader and read until io.EOF is returned. This snippet
|
||||
// shows how to echo messages using the NextWriter and NextReader methods:
|
||||
//
|
||||
// for {
|
||||
// messageType, r, err := conn.NextReader()
|
||||
// if err != nil {
|
||||
// return
|
||||
// }
|
||||
// w, err := conn.NextWriter(messageType)
|
||||
// if err != nil {
|
||||
// return err
|
||||
// }
|
||||
// if _, err := io.Copy(w, r); err != nil {
|
||||
// return err
|
||||
// }
|
||||
// if err := w.Close(); err != nil {
|
||||
// return err
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// Data Messages
|
||||
//
|
||||
// The WebSocket protocol distinguishes between text and binary data messages.
|
||||
// Text messages are interpreted as UTF-8 encoded text. The interpretation of
|
||||
// binary messages is left to the application.
|
||||
//
|
||||
// This package uses the TextMessage and BinaryMessage integer constants to
|
||||
// identify the two data message types. The ReadMessage and NextReader methods
|
||||
// return the type of the received message. The messageType argument to the
|
||||
// WriteMessage and NextWriter methods specifies the type of a sent message.
|
||||
//
|
||||
// It is the application's responsibility to ensure that text messages are
|
||||
// valid UTF-8 encoded text.
|
||||
//
|
||||
// Control Messages
|
||||
//
|
||||
// The WebSocket protocol defines three types of control messages: close, ping
|
||||
// and pong. Call the connection WriteControl, WriteMessage or NextWriter
|
||||
// methods to send a control message to the peer.
|
||||
//
|
||||
// Connections handle received close messages by calling the handler function
|
||||
// set with the SetCloseHandler method and by returning a *CloseError from the
|
||||
// NextReader, ReadMessage or the message Read method. The default close
|
||||
// handler sends a close message to the peer.
|
||||
//
|
||||
// Connections handle received ping messages by calling the handler function
|
||||
// set with the SetPingHandler method. The default ping handler sends a pong
|
||||
// message to the peer.
|
||||
//
|
||||
// Connections handle received pong messages by calling the handler function
|
||||
// set with the SetPongHandler method. The default pong handler does nothing.
|
||||
// If an application sends ping messages, then the application should set a
|
||||
// pong handler to receive the corresponding pong.
|
||||
//
|
||||
// The control message handler functions are called from the NextReader,
|
||||
// ReadMessage and message reader Read methods. The default close and ping
|
||||
// handlers can block these methods for a short time when the handler writes to
|
||||
// the connection.
|
||||
//
|
||||
// The application must read the connection to process close, ping and pong
|
||||
// messages sent from the peer. If the application is not otherwise interested
|
||||
// in messages from the peer, then the application should start a goroutine to
|
||||
// read and discard messages from the peer. A simple example is:
|
||||
//
|
||||
// func readLoop(c *websocket.Conn) {
|
||||
// for {
|
||||
// if _, _, err := c.NextReader(); err != nil {
|
||||
// c.Close()
|
||||
// break
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// Concurrency
|
||||
//
|
||||
// Connections support one concurrent reader and one concurrent writer.
|
||||
//
|
||||
// Applications are responsible for ensuring that no more than one goroutine
|
||||
// calls the write methods (NextWriter, SetWriteDeadline, WriteMessage,
|
||||
// WriteJSON, EnableWriteCompression, SetCompressionLevel) concurrently and
|
||||
// that no more than one goroutine calls the read methods (NextReader,
|
||||
// SetReadDeadline, ReadMessage, ReadJSON, SetPongHandler, SetPingHandler)
|
||||
// concurrently.
|
||||
//
|
||||
// The Close and WriteControl methods can be called concurrently with all other
|
||||
// methods.
|
||||
//
|
||||
// Origin Considerations
|
||||
//
|
||||
// Web browsers allow Javascript applications to open a WebSocket connection to
|
||||
// any host. It's up to the server to enforce an origin policy using the Origin
|
||||
// request header sent by the browser.
|
||||
//
|
||||
// The Upgrader calls the function specified in the CheckOrigin field to check
|
||||
// the origin. If the CheckOrigin function returns false, then the Upgrade
|
||||
// method fails the WebSocket handshake with HTTP status 403.
|
||||
//
|
||||
// If the CheckOrigin field is nil, then the Upgrader uses a safe default: fail
|
||||
// the handshake if the Origin request header is present and the Origin host is
|
||||
// not equal to the Host request header.
|
||||
//
|
||||
// The deprecated package-level Upgrade function does not perform origin
|
||||
// checking. The application is responsible for checking the Origin header
|
||||
// before calling the Upgrade function.
|
||||
//
|
||||
// Compression EXPERIMENTAL
|
||||
//
|
||||
// Per message compression extensions (RFC 7692) are experimentally supported
|
||||
// by this package in a limited capacity. Setting the EnableCompression option
|
||||
// to true in Dialer or Upgrader will attempt to negotiate per message deflate
|
||||
// support.
|
||||
//
|
||||
// var upgrader = websocket.Upgrader{
|
||||
// EnableCompression: true,
|
||||
// }
|
||||
//
|
||||
// If compression was successfully negotiated with the connection's peer, any
|
||||
// message received in compressed form will be automatically decompressed.
|
||||
// All Read methods will return uncompressed bytes.
|
||||
//
|
||||
// Per message compression of messages written to a connection can be enabled
|
||||
// or disabled by calling the corresponding Conn method:
|
||||
//
|
||||
// conn.EnableWriteCompression(false)
|
||||
//
|
||||
// Currently this package does not support compression with "context takeover".
|
||||
// This means that messages must be compressed and decompressed in isolation,
|
||||
// without retaining sliding window or dictionary state across messages. For
|
||||
// more details refer to RFC 7692.
|
||||
//
|
||||
// Use of compression is experimental and may result in decreased performance.
|
||||
package websocket
|
||||
+60
@@ -0,0 +1,60 @@
|
||||
// Copyright 2013 The Gorilla WebSocket Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package websocket
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"io"
|
||||
)
|
||||
|
||||
// WriteJSON writes the JSON encoding of v as a message.
|
||||
//
|
||||
// Deprecated: Use c.WriteJSON instead.
|
||||
func WriteJSON(c *Conn, v interface{}) error {
|
||||
return c.WriteJSON(v)
|
||||
}
|
||||
|
||||
// WriteJSON writes the JSON encoding of v as a message.
|
||||
//
|
||||
// See the documentation for encoding/json Marshal for details about the
|
||||
// conversion of Go values to JSON.
|
||||
func (c *Conn) WriteJSON(v interface{}) error {
|
||||
w, err := c.NextWriter(TextMessage)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err1 := json.NewEncoder(w).Encode(v)
|
||||
err2 := w.Close()
|
||||
if err1 != nil {
|
||||
return err1
|
||||
}
|
||||
return err2
|
||||
}
|
||||
|
||||
// ReadJSON reads the next JSON-encoded message from the connection and stores
|
||||
// it in the value pointed to by v.
|
||||
//
|
||||
// Deprecated: Use c.ReadJSON instead.
|
||||
func ReadJSON(c *Conn, v interface{}) error {
|
||||
return c.ReadJSON(v)
|
||||
}
|
||||
|
||||
// ReadJSON reads the next JSON-encoded message from the connection and stores
|
||||
// it in the value pointed to by v.
|
||||
//
|
||||
// See the documentation for the encoding/json Unmarshal function for details
|
||||
// about the conversion of JSON to a Go value.
|
||||
func (c *Conn) ReadJSON(v interface{}) error {
|
||||
_, r, err := c.NextReader()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = json.NewDecoder(r).Decode(v)
|
||||
if err == io.EOF {
|
||||
// One value is expected in the message.
|
||||
err = io.ErrUnexpectedEOF
|
||||
}
|
||||
return err
|
||||
}
|
||||
+54
@@ -0,0 +1,54 @@
|
||||
// Copyright 2016 The Gorilla WebSocket Authors. All rights reserved. Use of
|
||||
// this source code is governed by a BSD-style license that can be found in the
|
||||
// LICENSE file.
|
||||
|
||||
// +build !appengine
|
||||
|
||||
package websocket
|
||||
|
||||
import "unsafe"
|
||||
|
||||
const wordSize = int(unsafe.Sizeof(uintptr(0)))
|
||||
|
||||
func maskBytes(key [4]byte, pos int, b []byte) int {
|
||||
// Mask one byte at a time for small buffers.
|
||||
if len(b) < 2*wordSize {
|
||||
for i := range b {
|
||||
b[i] ^= key[pos&3]
|
||||
pos++
|
||||
}
|
||||
return pos & 3
|
||||
}
|
||||
|
||||
// Mask one byte at a time to word boundary.
|
||||
if n := int(uintptr(unsafe.Pointer(&b[0]))) % wordSize; n != 0 {
|
||||
n = wordSize - n
|
||||
for i := range b[:n] {
|
||||
b[i] ^= key[pos&3]
|
||||
pos++
|
||||
}
|
||||
b = b[n:]
|
||||
}
|
||||
|
||||
// Create aligned word size key.
|
||||
var k [wordSize]byte
|
||||
for i := range k {
|
||||
k[i] = key[(pos+i)&3]
|
||||
}
|
||||
kw := *(*uintptr)(unsafe.Pointer(&k))
|
||||
|
||||
// Mask one word at a time.
|
||||
n := (len(b) / wordSize) * wordSize
|
||||
for i := 0; i < n; i += wordSize {
|
||||
*(*uintptr)(unsafe.Pointer(uintptr(unsafe.Pointer(&b[0])) + uintptr(i))) ^= kw
|
||||
}
|
||||
|
||||
// Mask one byte at a time for remaining bytes.
|
||||
b = b[n:]
|
||||
for i := range b {
|
||||
b[i] ^= key[pos&3]
|
||||
pos++
|
||||
}
|
||||
|
||||
return pos & 3
|
||||
}
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
// Copyright 2016 The Gorilla WebSocket Authors. All rights reserved. Use of
|
||||
// this source code is governed by a BSD-style license that can be found in the
|
||||
// LICENSE file.
|
||||
|
||||
// +build appengine
|
||||
|
||||
package websocket
|
||||
|
||||
func maskBytes(key [4]byte, pos int, b []byte) int {
|
||||
for i := range b {
|
||||
b[i] ^= key[pos&3]
|
||||
pos++
|
||||
}
|
||||
return pos & 3
|
||||
}
|
||||
+102
@@ -0,0 +1,102 @@
|
||||
// Copyright 2017 The Gorilla WebSocket Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package websocket
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"net"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// PreparedMessage caches on the wire representations of a message payload.
|
||||
// Use PreparedMessage to efficiently send a message payload to multiple
|
||||
// connections. PreparedMessage is especially useful when compression is used
|
||||
// because the CPU and memory expensive compression operation can be executed
|
||||
// once for a given set of compression options.
|
||||
type PreparedMessage struct {
|
||||
messageType int
|
||||
data []byte
|
||||
mu sync.Mutex
|
||||
frames map[prepareKey]*preparedFrame
|
||||
}
|
||||
|
||||
// prepareKey defines a unique set of options to cache prepared frames in PreparedMessage.
|
||||
type prepareKey struct {
|
||||
isServer bool
|
||||
compress bool
|
||||
compressionLevel int
|
||||
}
|
||||
|
||||
// preparedFrame contains data in wire representation.
|
||||
type preparedFrame struct {
|
||||
once sync.Once
|
||||
data []byte
|
||||
}
|
||||
|
||||
// NewPreparedMessage returns an initialized PreparedMessage. You can then send
|
||||
// it to connection using WritePreparedMessage method. Valid wire
|
||||
// representation will be calculated lazily only once for a set of current
|
||||
// connection options.
|
||||
func NewPreparedMessage(messageType int, data []byte) (*PreparedMessage, error) {
|
||||
pm := &PreparedMessage{
|
||||
messageType: messageType,
|
||||
frames: make(map[prepareKey]*preparedFrame),
|
||||
data: data,
|
||||
}
|
||||
|
||||
// Prepare a plain server frame.
|
||||
_, frameData, err := pm.frame(prepareKey{isServer: true, compress: false})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// To protect against caller modifying the data argument, remember the data
|
||||
// copied to the plain server frame.
|
||||
pm.data = frameData[len(frameData)-len(data):]
|
||||
return pm, nil
|
||||
}
|
||||
|
||||
func (pm *PreparedMessage) frame(key prepareKey) (int, []byte, error) {
|
||||
pm.mu.Lock()
|
||||
frame, ok := pm.frames[key]
|
||||
if !ok {
|
||||
frame = &preparedFrame{}
|
||||
pm.frames[key] = frame
|
||||
}
|
||||
pm.mu.Unlock()
|
||||
|
||||
var err error
|
||||
frame.once.Do(func() {
|
||||
// Prepare a frame using a 'fake' connection.
|
||||
// TODO: Refactor code in conn.go to allow more direct construction of
|
||||
// the frame.
|
||||
mu := make(chan bool, 1)
|
||||
mu <- true
|
||||
var nc prepareConn
|
||||
c := &Conn{
|
||||
conn: &nc,
|
||||
mu: mu,
|
||||
isServer: key.isServer,
|
||||
compressionLevel: key.compressionLevel,
|
||||
enableWriteCompression: true,
|
||||
writeBuf: make([]byte, defaultWriteBufferSize+maxFrameHeaderSize),
|
||||
}
|
||||
if key.compress {
|
||||
c.newCompressionWriter = compressNoContextTakeover
|
||||
}
|
||||
err = c.WriteMessage(pm.messageType, pm.data)
|
||||
frame.data = nc.buf.Bytes()
|
||||
})
|
||||
return pm.messageType, frame.data, err
|
||||
}
|
||||
|
||||
type prepareConn struct {
|
||||
buf bytes.Buffer
|
||||
net.Conn
|
||||
}
|
||||
|
||||
func (pc *prepareConn) Write(p []byte) (int, error) { return pc.buf.Write(p) }
|
||||
func (pc *prepareConn) SetWriteDeadline(t time.Time) error { return nil }
|
||||
+77
@@ -0,0 +1,77 @@
|
||||
// Copyright 2017 The Gorilla WebSocket Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package websocket
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type netDialerFunc func(network, addr string) (net.Conn, error)
|
||||
|
||||
func (fn netDialerFunc) Dial(network, addr string) (net.Conn, error) {
|
||||
return fn(network, addr)
|
||||
}
|
||||
|
||||
func init() {
|
||||
proxy_RegisterDialerType("http", func(proxyURL *url.URL, forwardDialer proxy_Dialer) (proxy_Dialer, error) {
|
||||
return &httpProxyDialer{proxyURL: proxyURL, fowardDial: forwardDialer.Dial}, nil
|
||||
})
|
||||
}
|
||||
|
||||
type httpProxyDialer struct {
|
||||
proxyURL *url.URL
|
||||
fowardDial func(network, addr string) (net.Conn, error)
|
||||
}
|
||||
|
||||
func (hpd *httpProxyDialer) Dial(network string, addr string) (net.Conn, error) {
|
||||
hostPort, _ := hostPortNoPort(hpd.proxyURL)
|
||||
conn, err := hpd.fowardDial(network, hostPort)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
connectHeader := make(http.Header)
|
||||
if user := hpd.proxyURL.User; user != nil {
|
||||
proxyUser := user.Username()
|
||||
if proxyPassword, passwordSet := user.Password(); passwordSet {
|
||||
credential := base64.StdEncoding.EncodeToString([]byte(proxyUser + ":" + proxyPassword))
|
||||
connectHeader.Set("Proxy-Authorization", "Basic "+credential)
|
||||
}
|
||||
}
|
||||
|
||||
connectReq := &http.Request{
|
||||
Method: "CONNECT",
|
||||
URL: &url.URL{Opaque: addr},
|
||||
Host: addr,
|
||||
Header: connectHeader,
|
||||
}
|
||||
|
||||
if err := connectReq.Write(conn); err != nil {
|
||||
conn.Close()
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Read response. It's OK to use and discard buffered reader here becaue
|
||||
// the remote server does not speak until spoken to.
|
||||
br := bufio.NewReader(conn)
|
||||
resp, err := http.ReadResponse(br, connectReq)
|
||||
if err != nil {
|
||||
conn.Close()
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if resp.StatusCode != 200 {
|
||||
conn.Close()
|
||||
f := strings.SplitN(resp.Status, " ", 2)
|
||||
return nil, errors.New(f[1])
|
||||
}
|
||||
return conn, nil
|
||||
}
|
||||
+363
@@ -0,0 +1,363 @@
|
||||
// Copyright 2013 The Gorilla WebSocket Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package websocket
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"errors"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// HandshakeError describes an error with the handshake from the peer.
|
||||
type HandshakeError struct {
|
||||
message string
|
||||
}
|
||||
|
||||
func (e HandshakeError) Error() string { return e.message }
|
||||
|
||||
// Upgrader specifies parameters for upgrading an HTTP connection to a
|
||||
// WebSocket connection.
|
||||
type Upgrader struct {
|
||||
// HandshakeTimeout specifies the duration for the handshake to complete.
|
||||
HandshakeTimeout time.Duration
|
||||
|
||||
// ReadBufferSize and WriteBufferSize specify I/O buffer sizes. If a buffer
|
||||
// size is zero, then buffers allocated by the HTTP server are used. The
|
||||
// I/O buffer sizes do not limit the size of the messages that can be sent
|
||||
// or received.
|
||||
ReadBufferSize, WriteBufferSize int
|
||||
|
||||
// WriteBufferPool is a pool of buffers for write operations. If the value
|
||||
// is not set, then write buffers are allocated to the connection for the
|
||||
// lifetime of the connection.
|
||||
//
|
||||
// A pool is most useful when the application has a modest volume of writes
|
||||
// across a large number of connections.
|
||||
//
|
||||
// Applications should use a single pool for each unique value of
|
||||
// WriteBufferSize.
|
||||
WriteBufferPool BufferPool
|
||||
|
||||
// Subprotocols specifies the server's supported protocols in order of
|
||||
// preference. If this field is not nil, then the Upgrade method negotiates a
|
||||
// subprotocol by selecting the first match in this list with a protocol
|
||||
// requested by the client. If there's no match, then no protocol is
|
||||
// negotiated (the Sec-Websocket-Protocol header is not included in the
|
||||
// handshake response).
|
||||
Subprotocols []string
|
||||
|
||||
// Error specifies the function for generating HTTP error responses. If Error
|
||||
// is nil, then http.Error is used to generate the HTTP response.
|
||||
Error func(w http.ResponseWriter, r *http.Request, status int, reason error)
|
||||
|
||||
// CheckOrigin returns true if the request Origin header is acceptable. If
|
||||
// CheckOrigin is nil, then a safe default is used: return false if the
|
||||
// Origin request header is present and the origin host is not equal to
|
||||
// request Host header.
|
||||
//
|
||||
// A CheckOrigin function should carefully validate the request origin to
|
||||
// prevent cross-site request forgery.
|
||||
CheckOrigin func(r *http.Request) bool
|
||||
|
||||
// EnableCompression specify if the server should attempt to negotiate per
|
||||
// message compression (RFC 7692). Setting this value to true does not
|
||||
// guarantee that compression will be supported. Currently only "no context
|
||||
// takeover" modes are supported.
|
||||
EnableCompression bool
|
||||
}
|
||||
|
||||
func (u *Upgrader) returnError(w http.ResponseWriter, r *http.Request, status int, reason string) (*Conn, error) {
|
||||
err := HandshakeError{reason}
|
||||
if u.Error != nil {
|
||||
u.Error(w, r, status, err)
|
||||
} else {
|
||||
w.Header().Set("Sec-Websocket-Version", "13")
|
||||
http.Error(w, http.StatusText(status), status)
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// checkSameOrigin returns true if the origin is not set or is equal to the request host.
|
||||
func checkSameOrigin(r *http.Request) bool {
|
||||
origin := r.Header["Origin"]
|
||||
if len(origin) == 0 {
|
||||
return true
|
||||
}
|
||||
u, err := url.Parse(origin[0])
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return equalASCIIFold(u.Host, r.Host)
|
||||
}
|
||||
|
||||
func (u *Upgrader) selectSubprotocol(r *http.Request, responseHeader http.Header) string {
|
||||
if u.Subprotocols != nil {
|
||||
clientProtocols := Subprotocols(r)
|
||||
for _, serverProtocol := range u.Subprotocols {
|
||||
for _, clientProtocol := range clientProtocols {
|
||||
if clientProtocol == serverProtocol {
|
||||
return clientProtocol
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if responseHeader != nil {
|
||||
return responseHeader.Get("Sec-Websocket-Protocol")
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// Upgrade upgrades the HTTP server connection to the WebSocket protocol.
|
||||
//
|
||||
// The responseHeader is included in the response to the client's upgrade
|
||||
// request. Use the responseHeader to specify cookies (Set-Cookie) and the
|
||||
// application negotiated subprotocol (Sec-WebSocket-Protocol).
|
||||
//
|
||||
// If the upgrade fails, then Upgrade replies to the client with an HTTP error
|
||||
// response.
|
||||
func (u *Upgrader) Upgrade(w http.ResponseWriter, r *http.Request, responseHeader http.Header) (*Conn, error) {
|
||||
const badHandshake = "websocket: the client is not using the websocket protocol: "
|
||||
|
||||
if !tokenListContainsValue(r.Header, "Connection", "upgrade") {
|
||||
return u.returnError(w, r, http.StatusBadRequest, badHandshake+"'upgrade' token not found in 'Connection' header")
|
||||
}
|
||||
|
||||
if !tokenListContainsValue(r.Header, "Upgrade", "websocket") {
|
||||
return u.returnError(w, r, http.StatusBadRequest, badHandshake+"'websocket' token not found in 'Upgrade' header")
|
||||
}
|
||||
|
||||
if r.Method != "GET" {
|
||||
return u.returnError(w, r, http.StatusMethodNotAllowed, badHandshake+"request method is not GET")
|
||||
}
|
||||
|
||||
if !tokenListContainsValue(r.Header, "Sec-Websocket-Version", "13") {
|
||||
return u.returnError(w, r, http.StatusBadRequest, "websocket: unsupported version: 13 not found in 'Sec-Websocket-Version' header")
|
||||
}
|
||||
|
||||
if _, ok := responseHeader["Sec-Websocket-Extensions"]; ok {
|
||||
return u.returnError(w, r, http.StatusInternalServerError, "websocket: application specific 'Sec-WebSocket-Extensions' headers are unsupported")
|
||||
}
|
||||
|
||||
checkOrigin := u.CheckOrigin
|
||||
if checkOrigin == nil {
|
||||
checkOrigin = checkSameOrigin
|
||||
}
|
||||
if !checkOrigin(r) {
|
||||
return u.returnError(w, r, http.StatusForbidden, "websocket: request origin not allowed by Upgrader.CheckOrigin")
|
||||
}
|
||||
|
||||
challengeKey := r.Header.Get("Sec-Websocket-Key")
|
||||
if challengeKey == "" {
|
||||
return u.returnError(w, r, http.StatusBadRequest, "websocket: not a websocket handshake: `Sec-WebSocket-Key' header is missing or blank")
|
||||
}
|
||||
|
||||
subprotocol := u.selectSubprotocol(r, responseHeader)
|
||||
|
||||
// Negotiate PMCE
|
||||
var compress bool
|
||||
if u.EnableCompression {
|
||||
for _, ext := range parseExtensions(r.Header) {
|
||||
if ext[""] != "permessage-deflate" {
|
||||
continue
|
||||
}
|
||||
compress = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
h, ok := w.(http.Hijacker)
|
||||
if !ok {
|
||||
return u.returnError(w, r, http.StatusInternalServerError, "websocket: response does not implement http.Hijacker")
|
||||
}
|
||||
var brw *bufio.ReadWriter
|
||||
netConn, brw, err := h.Hijack()
|
||||
if err != nil {
|
||||
return u.returnError(w, r, http.StatusInternalServerError, err.Error())
|
||||
}
|
||||
|
||||
if brw.Reader.Buffered() > 0 {
|
||||
netConn.Close()
|
||||
return nil, errors.New("websocket: client sent data before handshake is complete")
|
||||
}
|
||||
|
||||
var br *bufio.Reader
|
||||
if u.ReadBufferSize == 0 && bufioReaderSize(netConn, brw.Reader) > 256 {
|
||||
// Reuse hijacked buffered reader as connection reader.
|
||||
br = brw.Reader
|
||||
}
|
||||
|
||||
buf := bufioWriterBuffer(netConn, brw.Writer)
|
||||
|
||||
var writeBuf []byte
|
||||
if u.WriteBufferPool == nil && u.WriteBufferSize == 0 && len(buf) >= maxFrameHeaderSize+256 {
|
||||
// Reuse hijacked write buffer as connection buffer.
|
||||
writeBuf = buf
|
||||
}
|
||||
|
||||
c := newConn(netConn, true, u.ReadBufferSize, u.WriteBufferSize, u.WriteBufferPool, br, writeBuf)
|
||||
c.subprotocol = subprotocol
|
||||
|
||||
if compress {
|
||||
c.newCompressionWriter = compressNoContextTakeover
|
||||
c.newDecompressionReader = decompressNoContextTakeover
|
||||
}
|
||||
|
||||
// Use larger of hijacked buffer and connection write buffer for header.
|
||||
p := buf
|
||||
if len(c.writeBuf) > len(p) {
|
||||
p = c.writeBuf
|
||||
}
|
||||
p = p[:0]
|
||||
|
||||
p = append(p, "HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Accept: "...)
|
||||
p = append(p, computeAcceptKey(challengeKey)...)
|
||||
p = append(p, "\r\n"...)
|
||||
if c.subprotocol != "" {
|
||||
p = append(p, "Sec-WebSocket-Protocol: "...)
|
||||
p = append(p, c.subprotocol...)
|
||||
p = append(p, "\r\n"...)
|
||||
}
|
||||
if compress {
|
||||
p = append(p, "Sec-WebSocket-Extensions: permessage-deflate; server_no_context_takeover; client_no_context_takeover\r\n"...)
|
||||
}
|
||||
for k, vs := range responseHeader {
|
||||
if k == "Sec-Websocket-Protocol" {
|
||||
continue
|
||||
}
|
||||
for _, v := range vs {
|
||||
p = append(p, k...)
|
||||
p = append(p, ": "...)
|
||||
for i := 0; i < len(v); i++ {
|
||||
b := v[i]
|
||||
if b <= 31 {
|
||||
// prevent response splitting.
|
||||
b = ' '
|
||||
}
|
||||
p = append(p, b)
|
||||
}
|
||||
p = append(p, "\r\n"...)
|
||||
}
|
||||
}
|
||||
p = append(p, "\r\n"...)
|
||||
|
||||
// Clear deadlines set by HTTP server.
|
||||
netConn.SetDeadline(time.Time{})
|
||||
|
||||
if u.HandshakeTimeout > 0 {
|
||||
netConn.SetWriteDeadline(time.Now().Add(u.HandshakeTimeout))
|
||||
}
|
||||
if _, err = netConn.Write(p); err != nil {
|
||||
netConn.Close()
|
||||
return nil, err
|
||||
}
|
||||
if u.HandshakeTimeout > 0 {
|
||||
netConn.SetWriteDeadline(time.Time{})
|
||||
}
|
||||
|
||||
return c, nil
|
||||
}
|
||||
|
||||
// Upgrade upgrades the HTTP server connection to the WebSocket protocol.
|
||||
//
|
||||
// Deprecated: Use websocket.Upgrader instead.
|
||||
//
|
||||
// Upgrade does not perform origin checking. The application is responsible for
|
||||
// checking the Origin header before calling Upgrade. An example implementation
|
||||
// of the same origin policy check is:
|
||||
//
|
||||
// if req.Header.Get("Origin") != "http://"+req.Host {
|
||||
// http.Error(w, "Origin not allowed", http.StatusForbidden)
|
||||
// return
|
||||
// }
|
||||
//
|
||||
// If the endpoint supports subprotocols, then the application is responsible
|
||||
// for negotiating the protocol used on the connection. Use the Subprotocols()
|
||||
// function to get the subprotocols requested by the client. Use the
|
||||
// Sec-Websocket-Protocol response header to specify the subprotocol selected
|
||||
// by the application.
|
||||
//
|
||||
// The responseHeader is included in the response to the client's upgrade
|
||||
// request. Use the responseHeader to specify cookies (Set-Cookie) and the
|
||||
// negotiated subprotocol (Sec-Websocket-Protocol).
|
||||
//
|
||||
// The connection buffers IO to the underlying network connection. The
|
||||
// readBufSize and writeBufSize parameters specify the size of the buffers to
|
||||
// use. Messages can be larger than the buffers.
|
||||
//
|
||||
// If the request is not a valid WebSocket handshake, then Upgrade returns an
|
||||
// error of type HandshakeError. Applications should handle this error by
|
||||
// replying to the client with an HTTP error response.
|
||||
func Upgrade(w http.ResponseWriter, r *http.Request, responseHeader http.Header, readBufSize, writeBufSize int) (*Conn, error) {
|
||||
u := Upgrader{ReadBufferSize: readBufSize, WriteBufferSize: writeBufSize}
|
||||
u.Error = func(w http.ResponseWriter, r *http.Request, status int, reason error) {
|
||||
// don't return errors to maintain backwards compatibility
|
||||
}
|
||||
u.CheckOrigin = func(r *http.Request) bool {
|
||||
// allow all connections by default
|
||||
return true
|
||||
}
|
||||
return u.Upgrade(w, r, responseHeader)
|
||||
}
|
||||
|
||||
// Subprotocols returns the subprotocols requested by the client in the
|
||||
// Sec-Websocket-Protocol header.
|
||||
func Subprotocols(r *http.Request) []string {
|
||||
h := strings.TrimSpace(r.Header.Get("Sec-Websocket-Protocol"))
|
||||
if h == "" {
|
||||
return nil
|
||||
}
|
||||
protocols := strings.Split(h, ",")
|
||||
for i := range protocols {
|
||||
protocols[i] = strings.TrimSpace(protocols[i])
|
||||
}
|
||||
return protocols
|
||||
}
|
||||
|
||||
// IsWebSocketUpgrade returns true if the client requested upgrade to the
|
||||
// WebSocket protocol.
|
||||
func IsWebSocketUpgrade(r *http.Request) bool {
|
||||
return tokenListContainsValue(r.Header, "Connection", "upgrade") &&
|
||||
tokenListContainsValue(r.Header, "Upgrade", "websocket")
|
||||
}
|
||||
|
||||
// bufioReaderSize size returns the size of a bufio.Reader.
|
||||
func bufioReaderSize(originalReader io.Reader, br *bufio.Reader) int {
|
||||
// This code assumes that peek on a reset reader returns
|
||||
// bufio.Reader.buf[:0].
|
||||
// TODO: Use bufio.Reader.Size() after Go 1.10
|
||||
br.Reset(originalReader)
|
||||
if p, err := br.Peek(0); err == nil {
|
||||
return cap(p)
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
// writeHook is an io.Writer that records the last slice passed to it vio
|
||||
// io.Writer.Write.
|
||||
type writeHook struct {
|
||||
p []byte
|
||||
}
|
||||
|
||||
func (wh *writeHook) Write(p []byte) (int, error) {
|
||||
wh.p = p
|
||||
return len(p), nil
|
||||
}
|
||||
|
||||
// bufioWriterBuffer grabs the buffer from a bufio.Writer.
|
||||
func bufioWriterBuffer(originalWriter io.Writer, bw *bufio.Writer) []byte {
|
||||
// This code assumes that bufio.Writer.buf[:1] is passed to the
|
||||
// bufio.Writer's underlying writer.
|
||||
var wh writeHook
|
||||
bw.Reset(&wh)
|
||||
bw.WriteByte(0)
|
||||
bw.Flush()
|
||||
|
||||
bw.Reset(originalWriter)
|
||||
|
||||
return wh.p[:cap(wh.p)]
|
||||
}
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
// +build go1.8
|
||||
|
||||
package websocket
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"net/http/httptrace"
|
||||
)
|
||||
|
||||
func doHandshakeWithTrace(trace *httptrace.ClientTrace, tlsConn *tls.Conn, cfg *tls.Config) error {
|
||||
if trace.TLSHandshakeStart != nil {
|
||||
trace.TLSHandshakeStart()
|
||||
}
|
||||
err := doHandshake(tlsConn, cfg)
|
||||
if trace.TLSHandshakeDone != nil {
|
||||
trace.TLSHandshakeDone(tlsConn.ConnectionState(), err)
|
||||
}
|
||||
return err
|
||||
}
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
// +build !go1.8
|
||||
|
||||
package websocket
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"net/http/httptrace"
|
||||
)
|
||||
|
||||
func doHandshakeWithTrace(trace *httptrace.ClientTrace, tlsConn *tls.Conn, cfg *tls.Config) error {
|
||||
return doHandshake(tlsConn, cfg)
|
||||
}
|
||||
+237
@@ -0,0 +1,237 @@
|
||||
// Copyright 2013 The Gorilla WebSocket Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package websocket
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"crypto/sha1"
|
||||
"encoding/base64"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
"unicode/utf8"
|
||||
)
|
||||
|
||||
var keyGUID = []byte("258EAFA5-E914-47DA-95CA-C5AB0DC85B11")
|
||||
|
||||
func computeAcceptKey(challengeKey string) string {
|
||||
h := sha1.New()
|
||||
h.Write([]byte(challengeKey))
|
||||
h.Write(keyGUID)
|
||||
return base64.StdEncoding.EncodeToString(h.Sum(nil))
|
||||
}
|
||||
|
||||
func generateChallengeKey() (string, error) {
|
||||
p := make([]byte, 16)
|
||||
if _, err := io.ReadFull(rand.Reader, p); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return base64.StdEncoding.EncodeToString(p), nil
|
||||
}
|
||||
|
||||
// Octet types from RFC 2616.
|
||||
var octetTypes [256]byte
|
||||
|
||||
const (
|
||||
isTokenOctet = 1 << iota
|
||||
isSpaceOctet
|
||||
)
|
||||
|
||||
func init() {
|
||||
// From RFC 2616
|
||||
//
|
||||
// OCTET = <any 8-bit sequence of data>
|
||||
// CHAR = <any US-ASCII character (octets 0 - 127)>
|
||||
// CTL = <any US-ASCII control character (octets 0 - 31) and DEL (127)>
|
||||
// CR = <US-ASCII CR, carriage return (13)>
|
||||
// LF = <US-ASCII LF, linefeed (10)>
|
||||
// SP = <US-ASCII SP, space (32)>
|
||||
// HT = <US-ASCII HT, horizontal-tab (9)>
|
||||
// <"> = <US-ASCII double-quote mark (34)>
|
||||
// CRLF = CR LF
|
||||
// LWS = [CRLF] 1*( SP | HT )
|
||||
// TEXT = <any OCTET except CTLs, but including LWS>
|
||||
// separators = "(" | ")" | "<" | ">" | "@" | "," | ";" | ":" | "\" | <">
|
||||
// | "/" | "[" | "]" | "?" | "=" | "{" | "}" | SP | HT
|
||||
// token = 1*<any CHAR except CTLs or separators>
|
||||
// qdtext = <any TEXT except <">>
|
||||
|
||||
for c := 0; c < 256; c++ {
|
||||
var t byte
|
||||
isCtl := c <= 31 || c == 127
|
||||
isChar := 0 <= c && c <= 127
|
||||
isSeparator := strings.IndexRune(" \t\"(),/:;<=>?@[]\\{}", rune(c)) >= 0
|
||||
if strings.IndexRune(" \t\r\n", rune(c)) >= 0 {
|
||||
t |= isSpaceOctet
|
||||
}
|
||||
if isChar && !isCtl && !isSeparator {
|
||||
t |= isTokenOctet
|
||||
}
|
||||
octetTypes[c] = t
|
||||
}
|
||||
}
|
||||
|
||||
func skipSpace(s string) (rest string) {
|
||||
i := 0
|
||||
for ; i < len(s); i++ {
|
||||
if octetTypes[s[i]]&isSpaceOctet == 0 {
|
||||
break
|
||||
}
|
||||
}
|
||||
return s[i:]
|
||||
}
|
||||
|
||||
func nextToken(s string) (token, rest string) {
|
||||
i := 0
|
||||
for ; i < len(s); i++ {
|
||||
if octetTypes[s[i]]&isTokenOctet == 0 {
|
||||
break
|
||||
}
|
||||
}
|
||||
return s[:i], s[i:]
|
||||
}
|
||||
|
||||
func nextTokenOrQuoted(s string) (value string, rest string) {
|
||||
if !strings.HasPrefix(s, "\"") {
|
||||
return nextToken(s)
|
||||
}
|
||||
s = s[1:]
|
||||
for i := 0; i < len(s); i++ {
|
||||
switch s[i] {
|
||||
case '"':
|
||||
return s[:i], s[i+1:]
|
||||
case '\\':
|
||||
p := make([]byte, len(s)-1)
|
||||
j := copy(p, s[:i])
|
||||
escape := true
|
||||
for i = i + 1; i < len(s); i++ {
|
||||
b := s[i]
|
||||
switch {
|
||||
case escape:
|
||||
escape = false
|
||||
p[j] = b
|
||||
j++
|
||||
case b == '\\':
|
||||
escape = true
|
||||
case b == '"':
|
||||
return string(p[:j]), s[i+1:]
|
||||
default:
|
||||
p[j] = b
|
||||
j++
|
||||
}
|
||||
}
|
||||
return "", ""
|
||||
}
|
||||
}
|
||||
return "", ""
|
||||
}
|
||||
|
||||
// equalASCIIFold returns true if s is equal to t with ASCII case folding.
|
||||
func equalASCIIFold(s, t string) bool {
|
||||
for s != "" && t != "" {
|
||||
sr, size := utf8.DecodeRuneInString(s)
|
||||
s = s[size:]
|
||||
tr, size := utf8.DecodeRuneInString(t)
|
||||
t = t[size:]
|
||||
if sr == tr {
|
||||
continue
|
||||
}
|
||||
if 'A' <= sr && sr <= 'Z' {
|
||||
sr = sr + 'a' - 'A'
|
||||
}
|
||||
if 'A' <= tr && tr <= 'Z' {
|
||||
tr = tr + 'a' - 'A'
|
||||
}
|
||||
if sr != tr {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return s == t
|
||||
}
|
||||
|
||||
// tokenListContainsValue returns true if the 1#token header with the given
|
||||
// name contains a token equal to value with ASCII case folding.
|
||||
func tokenListContainsValue(header http.Header, name string, value string) bool {
|
||||
headers:
|
||||
for _, s := range header[name] {
|
||||
for {
|
||||
var t string
|
||||
t, s = nextToken(skipSpace(s))
|
||||
if t == "" {
|
||||
continue headers
|
||||
}
|
||||
s = skipSpace(s)
|
||||
if s != "" && s[0] != ',' {
|
||||
continue headers
|
||||
}
|
||||
if equalASCIIFold(t, value) {
|
||||
return true
|
||||
}
|
||||
if s == "" {
|
||||
continue headers
|
||||
}
|
||||
s = s[1:]
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// parseExtensions parses WebSocket extensions from a header.
|
||||
func parseExtensions(header http.Header) []map[string]string {
|
||||
// From RFC 6455:
|
||||
//
|
||||
// Sec-WebSocket-Extensions = extension-list
|
||||
// extension-list = 1#extension
|
||||
// extension = extension-token *( ";" extension-param )
|
||||
// extension-token = registered-token
|
||||
// registered-token = token
|
||||
// extension-param = token [ "=" (token | quoted-string) ]
|
||||
// ;When using the quoted-string syntax variant, the value
|
||||
// ;after quoted-string unescaping MUST conform to the
|
||||
// ;'token' ABNF.
|
||||
|
||||
var result []map[string]string
|
||||
headers:
|
||||
for _, s := range header["Sec-Websocket-Extensions"] {
|
||||
for {
|
||||
var t string
|
||||
t, s = nextToken(skipSpace(s))
|
||||
if t == "" {
|
||||
continue headers
|
||||
}
|
||||
ext := map[string]string{"": t}
|
||||
for {
|
||||
s = skipSpace(s)
|
||||
if !strings.HasPrefix(s, ";") {
|
||||
break
|
||||
}
|
||||
var k string
|
||||
k, s = nextToken(skipSpace(s[1:]))
|
||||
if k == "" {
|
||||
continue headers
|
||||
}
|
||||
s = skipSpace(s)
|
||||
var v string
|
||||
if strings.HasPrefix(s, "=") {
|
||||
v, s = nextTokenOrQuoted(skipSpace(s[1:]))
|
||||
s = skipSpace(s)
|
||||
}
|
||||
if s != "" && s[0] != ',' && s[0] != ';' {
|
||||
continue headers
|
||||
}
|
||||
ext[k] = v
|
||||
}
|
||||
if s != "" && s[0] != ',' {
|
||||
continue headers
|
||||
}
|
||||
result = append(result, ext)
|
||||
if s == "" {
|
||||
continue headers
|
||||
}
|
||||
s = s[1:]
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
+473
@@ -0,0 +1,473 @@
|
||||
// Code generated by golang.org/x/tools/cmd/bundle. DO NOT EDIT.
|
||||
//go:generate bundle -o x_net_proxy.go golang.org/x/net/proxy
|
||||
|
||||
// Package proxy provides support for a variety of protocols to proxy network
|
||||
// data.
|
||||
//
|
||||
|
||||
package websocket
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"io"
|
||||
"net"
|
||||
"net/url"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
)
|
||||
|
||||
type proxy_direct struct{}
|
||||
|
||||
// Direct is a direct proxy: one that makes network connections directly.
|
||||
var proxy_Direct = proxy_direct{}
|
||||
|
||||
func (proxy_direct) Dial(network, addr string) (net.Conn, error) {
|
||||
return net.Dial(network, addr)
|
||||
}
|
||||
|
||||
// A PerHost directs connections to a default Dialer unless the host name
|
||||
// requested matches one of a number of exceptions.
|
||||
type proxy_PerHost struct {
|
||||
def, bypass proxy_Dialer
|
||||
|
||||
bypassNetworks []*net.IPNet
|
||||
bypassIPs []net.IP
|
||||
bypassZones []string
|
||||
bypassHosts []string
|
||||
}
|
||||
|
||||
// NewPerHost returns a PerHost Dialer that directs connections to either
|
||||
// defaultDialer or bypass, depending on whether the connection matches one of
|
||||
// the configured rules.
|
||||
func proxy_NewPerHost(defaultDialer, bypass proxy_Dialer) *proxy_PerHost {
|
||||
return &proxy_PerHost{
|
||||
def: defaultDialer,
|
||||
bypass: bypass,
|
||||
}
|
||||
}
|
||||
|
||||
// Dial connects to the address addr on the given network through either
|
||||
// defaultDialer or bypass.
|
||||
func (p *proxy_PerHost) Dial(network, addr string) (c net.Conn, err error) {
|
||||
host, _, err := net.SplitHostPort(addr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return p.dialerForRequest(host).Dial(network, addr)
|
||||
}
|
||||
|
||||
func (p *proxy_PerHost) dialerForRequest(host string) proxy_Dialer {
|
||||
if ip := net.ParseIP(host); ip != nil {
|
||||
for _, net := range p.bypassNetworks {
|
||||
if net.Contains(ip) {
|
||||
return p.bypass
|
||||
}
|
||||
}
|
||||
for _, bypassIP := range p.bypassIPs {
|
||||
if bypassIP.Equal(ip) {
|
||||
return p.bypass
|
||||
}
|
||||
}
|
||||
return p.def
|
||||
}
|
||||
|
||||
for _, zone := range p.bypassZones {
|
||||
if strings.HasSuffix(host, zone) {
|
||||
return p.bypass
|
||||
}
|
||||
if host == zone[1:] {
|
||||
// For a zone ".example.com", we match "example.com"
|
||||
// too.
|
||||
return p.bypass
|
||||
}
|
||||
}
|
||||
for _, bypassHost := range p.bypassHosts {
|
||||
if bypassHost == host {
|
||||
return p.bypass
|
||||
}
|
||||
}
|
||||
return p.def
|
||||
}
|
||||
|
||||
// AddFromString parses a string that contains comma-separated values
|
||||
// specifying hosts that should use the bypass proxy. Each value is either an
|
||||
// IP address, a CIDR range, a zone (*.example.com) or a host name
|
||||
// (localhost). A best effort is made to parse the string and errors are
|
||||
// ignored.
|
||||
func (p *proxy_PerHost) AddFromString(s string) {
|
||||
hosts := strings.Split(s, ",")
|
||||
for _, host := range hosts {
|
||||
host = strings.TrimSpace(host)
|
||||
if len(host) == 0 {
|
||||
continue
|
||||
}
|
||||
if strings.Contains(host, "/") {
|
||||
// We assume that it's a CIDR address like 127.0.0.0/8
|
||||
if _, net, err := net.ParseCIDR(host); err == nil {
|
||||
p.AddNetwork(net)
|
||||
}
|
||||
continue
|
||||
}
|
||||
if ip := net.ParseIP(host); ip != nil {
|
||||
p.AddIP(ip)
|
||||
continue
|
||||
}
|
||||
if strings.HasPrefix(host, "*.") {
|
||||
p.AddZone(host[1:])
|
||||
continue
|
||||
}
|
||||
p.AddHost(host)
|
||||
}
|
||||
}
|
||||
|
||||
// AddIP specifies an IP address that will use the bypass proxy. Note that
|
||||
// this will only take effect if a literal IP address is dialed. A connection
|
||||
// to a named host will never match an IP.
|
||||
func (p *proxy_PerHost) AddIP(ip net.IP) {
|
||||
p.bypassIPs = append(p.bypassIPs, ip)
|
||||
}
|
||||
|
||||
// AddNetwork specifies an IP range that will use the bypass proxy. Note that
|
||||
// this will only take effect if a literal IP address is dialed. A connection
|
||||
// to a named host will never match.
|
||||
func (p *proxy_PerHost) AddNetwork(net *net.IPNet) {
|
||||
p.bypassNetworks = append(p.bypassNetworks, net)
|
||||
}
|
||||
|
||||
// AddZone specifies a DNS suffix that will use the bypass proxy. A zone of
|
||||
// "example.com" matches "example.com" and all of its subdomains.
|
||||
func (p *proxy_PerHost) AddZone(zone string) {
|
||||
if strings.HasSuffix(zone, ".") {
|
||||
zone = zone[:len(zone)-1]
|
||||
}
|
||||
if !strings.HasPrefix(zone, ".") {
|
||||
zone = "." + zone
|
||||
}
|
||||
p.bypassZones = append(p.bypassZones, zone)
|
||||
}
|
||||
|
||||
// AddHost specifies a host name that will use the bypass proxy.
|
||||
func (p *proxy_PerHost) AddHost(host string) {
|
||||
if strings.HasSuffix(host, ".") {
|
||||
host = host[:len(host)-1]
|
||||
}
|
||||
p.bypassHosts = append(p.bypassHosts, host)
|
||||
}
|
||||
|
||||
// A Dialer is a means to establish a connection.
|
||||
type proxy_Dialer interface {
|
||||
// Dial connects to the given address via the proxy.
|
||||
Dial(network, addr string) (c net.Conn, err error)
|
||||
}
|
||||
|
||||
// Auth contains authentication parameters that specific Dialers may require.
|
||||
type proxy_Auth struct {
|
||||
User, Password string
|
||||
}
|
||||
|
||||
// FromEnvironment returns the dialer specified by the proxy related variables in
|
||||
// the environment.
|
||||
func proxy_FromEnvironment() proxy_Dialer {
|
||||
allProxy := proxy_allProxyEnv.Get()
|
||||
if len(allProxy) == 0 {
|
||||
return proxy_Direct
|
||||
}
|
||||
|
||||
proxyURL, err := url.Parse(allProxy)
|
||||
if err != nil {
|
||||
return proxy_Direct
|
||||
}
|
||||
proxy, err := proxy_FromURL(proxyURL, proxy_Direct)
|
||||
if err != nil {
|
||||
return proxy_Direct
|
||||
}
|
||||
|
||||
noProxy := proxy_noProxyEnv.Get()
|
||||
if len(noProxy) == 0 {
|
||||
return proxy
|
||||
}
|
||||
|
||||
perHost := proxy_NewPerHost(proxy, proxy_Direct)
|
||||
perHost.AddFromString(noProxy)
|
||||
return perHost
|
||||
}
|
||||
|
||||
// proxySchemes is a map from URL schemes to a function that creates a Dialer
|
||||
// from a URL with such a scheme.
|
||||
var proxy_proxySchemes map[string]func(*url.URL, proxy_Dialer) (proxy_Dialer, error)
|
||||
|
||||
// RegisterDialerType takes a URL scheme and a function to generate Dialers from
|
||||
// a URL with that scheme and a forwarding Dialer. Registered schemes are used
|
||||
// by FromURL.
|
||||
func proxy_RegisterDialerType(scheme string, f func(*url.URL, proxy_Dialer) (proxy_Dialer, error)) {
|
||||
if proxy_proxySchemes == nil {
|
||||
proxy_proxySchemes = make(map[string]func(*url.URL, proxy_Dialer) (proxy_Dialer, error))
|
||||
}
|
||||
proxy_proxySchemes[scheme] = f
|
||||
}
|
||||
|
||||
// FromURL returns a Dialer given a URL specification and an underlying
|
||||
// Dialer for it to make network requests.
|
||||
func proxy_FromURL(u *url.URL, forward proxy_Dialer) (proxy_Dialer, error) {
|
||||
var auth *proxy_Auth
|
||||
if u.User != nil {
|
||||
auth = new(proxy_Auth)
|
||||
auth.User = u.User.Username()
|
||||
if p, ok := u.User.Password(); ok {
|
||||
auth.Password = p
|
||||
}
|
||||
}
|
||||
|
||||
switch u.Scheme {
|
||||
case "socks5":
|
||||
return proxy_SOCKS5("tcp", u.Host, auth, forward)
|
||||
}
|
||||
|
||||
// If the scheme doesn't match any of the built-in schemes, see if it
|
||||
// was registered by another package.
|
||||
if proxy_proxySchemes != nil {
|
||||
if f, ok := proxy_proxySchemes[u.Scheme]; ok {
|
||||
return f(u, forward)
|
||||
}
|
||||
}
|
||||
|
||||
return nil, errors.New("proxy: unknown scheme: " + u.Scheme)
|
||||
}
|
||||
|
||||
var (
|
||||
proxy_allProxyEnv = &proxy_envOnce{
|
||||
names: []string{"ALL_PROXY", "all_proxy"},
|
||||
}
|
||||
proxy_noProxyEnv = &proxy_envOnce{
|
||||
names: []string{"NO_PROXY", "no_proxy"},
|
||||
}
|
||||
)
|
||||
|
||||
// envOnce looks up an environment variable (optionally by multiple
|
||||
// names) once. It mitigates expensive lookups on some platforms
|
||||
// (e.g. Windows).
|
||||
// (Borrowed from net/http/transport.go)
|
||||
type proxy_envOnce struct {
|
||||
names []string
|
||||
once sync.Once
|
||||
val string
|
||||
}
|
||||
|
||||
func (e *proxy_envOnce) Get() string {
|
||||
e.once.Do(e.init)
|
||||
return e.val
|
||||
}
|
||||
|
||||
func (e *proxy_envOnce) init() {
|
||||
for _, n := range e.names {
|
||||
e.val = os.Getenv(n)
|
||||
if e.val != "" {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// SOCKS5 returns a Dialer that makes SOCKSv5 connections to the given address
|
||||
// with an optional username and password. See RFC 1928 and RFC 1929.
|
||||
func proxy_SOCKS5(network, addr string, auth *proxy_Auth, forward proxy_Dialer) (proxy_Dialer, error) {
|
||||
s := &proxy_socks5{
|
||||
network: network,
|
||||
addr: addr,
|
||||
forward: forward,
|
||||
}
|
||||
if auth != nil {
|
||||
s.user = auth.User
|
||||
s.password = auth.Password
|
||||
}
|
||||
|
||||
return s, nil
|
||||
}
|
||||
|
||||
type proxy_socks5 struct {
|
||||
user, password string
|
||||
network, addr string
|
||||
forward proxy_Dialer
|
||||
}
|
||||
|
||||
const proxy_socks5Version = 5
|
||||
|
||||
const (
|
||||
proxy_socks5AuthNone = 0
|
||||
proxy_socks5AuthPassword = 2
|
||||
)
|
||||
|
||||
const proxy_socks5Connect = 1
|
||||
|
||||
const (
|
||||
proxy_socks5IP4 = 1
|
||||
proxy_socks5Domain = 3
|
||||
proxy_socks5IP6 = 4
|
||||
)
|
||||
|
||||
var proxy_socks5Errors = []string{
|
||||
"",
|
||||
"general failure",
|
||||
"connection forbidden",
|
||||
"network unreachable",
|
||||
"host unreachable",
|
||||
"connection refused",
|
||||
"TTL expired",
|
||||
"command not supported",
|
||||
"address type not supported",
|
||||
}
|
||||
|
||||
// Dial connects to the address addr on the given network via the SOCKS5 proxy.
|
||||
func (s *proxy_socks5) Dial(network, addr string) (net.Conn, error) {
|
||||
switch network {
|
||||
case "tcp", "tcp6", "tcp4":
|
||||
default:
|
||||
return nil, errors.New("proxy: no support for SOCKS5 proxy connections of type " + network)
|
||||
}
|
||||
|
||||
conn, err := s.forward.Dial(s.network, s.addr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := s.connect(conn, addr); err != nil {
|
||||
conn.Close()
|
||||
return nil, err
|
||||
}
|
||||
return conn, nil
|
||||
}
|
||||
|
||||
// connect takes an existing connection to a socks5 proxy server,
|
||||
// and commands the server to extend that connection to target,
|
||||
// which must be a canonical address with a host and port.
|
||||
func (s *proxy_socks5) connect(conn net.Conn, target string) error {
|
||||
host, portStr, err := net.SplitHostPort(target)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
port, err := strconv.Atoi(portStr)
|
||||
if err != nil {
|
||||
return errors.New("proxy: failed to parse port number: " + portStr)
|
||||
}
|
||||
if port < 1 || port > 0xffff {
|
||||
return errors.New("proxy: port number out of range: " + portStr)
|
||||
}
|
||||
|
||||
// the size here is just an estimate
|
||||
buf := make([]byte, 0, 6+len(host))
|
||||
|
||||
buf = append(buf, proxy_socks5Version)
|
||||
if len(s.user) > 0 && len(s.user) < 256 && len(s.password) < 256 {
|
||||
buf = append(buf, 2 /* num auth methods */, proxy_socks5AuthNone, proxy_socks5AuthPassword)
|
||||
} else {
|
||||
buf = append(buf, 1 /* num auth methods */, proxy_socks5AuthNone)
|
||||
}
|
||||
|
||||
if _, err := conn.Write(buf); err != nil {
|
||||
return errors.New("proxy: failed to write greeting to SOCKS5 proxy at " + s.addr + ": " + err.Error())
|
||||
}
|
||||
|
||||
if _, err := io.ReadFull(conn, buf[:2]); err != nil {
|
||||
return errors.New("proxy: failed to read greeting from SOCKS5 proxy at " + s.addr + ": " + err.Error())
|
||||
}
|
||||
if buf[0] != 5 {
|
||||
return errors.New("proxy: SOCKS5 proxy at " + s.addr + " has unexpected version " + strconv.Itoa(int(buf[0])))
|
||||
}
|
||||
if buf[1] == 0xff {
|
||||
return errors.New("proxy: SOCKS5 proxy at " + s.addr + " requires authentication")
|
||||
}
|
||||
|
||||
// See RFC 1929
|
||||
if buf[1] == proxy_socks5AuthPassword {
|
||||
buf = buf[:0]
|
||||
buf = append(buf, 1 /* password protocol version */)
|
||||
buf = append(buf, uint8(len(s.user)))
|
||||
buf = append(buf, s.user...)
|
||||
buf = append(buf, uint8(len(s.password)))
|
||||
buf = append(buf, s.password...)
|
||||
|
||||
if _, err := conn.Write(buf); err != nil {
|
||||
return errors.New("proxy: failed to write authentication request to SOCKS5 proxy at " + s.addr + ": " + err.Error())
|
||||
}
|
||||
|
||||
if _, err := io.ReadFull(conn, buf[:2]); err != nil {
|
||||
return errors.New("proxy: failed to read authentication reply from SOCKS5 proxy at " + s.addr + ": " + err.Error())
|
||||
}
|
||||
|
||||
if buf[1] != 0 {
|
||||
return errors.New("proxy: SOCKS5 proxy at " + s.addr + " rejected username/password")
|
||||
}
|
||||
}
|
||||
|
||||
buf = buf[:0]
|
||||
buf = append(buf, proxy_socks5Version, proxy_socks5Connect, 0 /* reserved */)
|
||||
|
||||
if ip := net.ParseIP(host); ip != nil {
|
||||
if ip4 := ip.To4(); ip4 != nil {
|
||||
buf = append(buf, proxy_socks5IP4)
|
||||
ip = ip4
|
||||
} else {
|
||||
buf = append(buf, proxy_socks5IP6)
|
||||
}
|
||||
buf = append(buf, ip...)
|
||||
} else {
|
||||
if len(host) > 255 {
|
||||
return errors.New("proxy: destination host name too long: " + host)
|
||||
}
|
||||
buf = append(buf, proxy_socks5Domain)
|
||||
buf = append(buf, byte(len(host)))
|
||||
buf = append(buf, host...)
|
||||
}
|
||||
buf = append(buf, byte(port>>8), byte(port))
|
||||
|
||||
if _, err := conn.Write(buf); err != nil {
|
||||
return errors.New("proxy: failed to write connect request to SOCKS5 proxy at " + s.addr + ": " + err.Error())
|
||||
}
|
||||
|
||||
if _, err := io.ReadFull(conn, buf[:4]); err != nil {
|
||||
return errors.New("proxy: failed to read connect reply from SOCKS5 proxy at " + s.addr + ": " + err.Error())
|
||||
}
|
||||
|
||||
failure := "unknown error"
|
||||
if int(buf[1]) < len(proxy_socks5Errors) {
|
||||
failure = proxy_socks5Errors[buf[1]]
|
||||
}
|
||||
|
||||
if len(failure) > 0 {
|
||||
return errors.New("proxy: SOCKS5 proxy at " + s.addr + " failed to connect: " + failure)
|
||||
}
|
||||
|
||||
bytesToDiscard := 0
|
||||
switch buf[3] {
|
||||
case proxy_socks5IP4:
|
||||
bytesToDiscard = net.IPv4len
|
||||
case proxy_socks5IP6:
|
||||
bytesToDiscard = net.IPv6len
|
||||
case proxy_socks5Domain:
|
||||
_, err := io.ReadFull(conn, buf[:1])
|
||||
if err != nil {
|
||||
return errors.New("proxy: failed to read domain length from SOCKS5 proxy at " + s.addr + ": " + err.Error())
|
||||
}
|
||||
bytesToDiscard = int(buf[0])
|
||||
default:
|
||||
return errors.New("proxy: got unknown address type " + strconv.Itoa(int(buf[3])) + " from SOCKS5 proxy at " + s.addr)
|
||||
}
|
||||
|
||||
if cap(buf) < bytesToDiscard {
|
||||
buf = make([]byte, bytesToDiscard)
|
||||
} else {
|
||||
buf = buf[:bytesToDiscard]
|
||||
}
|
||||
if _, err := io.ReadFull(conn, buf); err != nil {
|
||||
return errors.New("proxy: failed to read address from SOCKS5 proxy at " + s.addr + ": " + err.Error())
|
||||
}
|
||||
|
||||
// Also need to discard the port number
|
||||
if _, err := io.ReadFull(conn, buf[:2]); err != nil {
|
||||
return errors.New("proxy: failed to read port from SOCKS5 proxy at " + s.addr + ": " + err.Error())
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
-1
Submodule vendor/github.com/konsorten/go-windows-terminal-sequences deleted from 5c8c8bd35d
+9
@@ -0,0 +1,9 @@
|
||||
(The MIT License)
|
||||
|
||||
Copyright (c) 2017 marvin + konsorten GmbH (open-source@konsorten.de)
|
||||
|
||||
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.
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
# Windows Terminal Sequences
|
||||
|
||||
This library allow for enabling Windows terminal color support for Go.
|
||||
|
||||
See [Console Virtual Terminal Sequences](https://docs.microsoft.com/en-us/windows/console/console-virtual-terminal-sequences) for details.
|
||||
|
||||
## Usage
|
||||
|
||||
```go
|
||||
import (
|
||||
"syscall"
|
||||
|
||||
sequences "github.com/konsorten/go-windows-terminal-sequences"
|
||||
)
|
||||
|
||||
func main() {
|
||||
sequences.EnableVirtualTerminalProcessing(syscall.Stdout, true)
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
## Authors
|
||||
|
||||
The tool is sponsored by the [marvin + konsorten GmbH](http://www.konsorten.de).
|
||||
|
||||
We thank all the authors who provided code to this library:
|
||||
|
||||
* Felix Kollmann
|
||||
|
||||
## License
|
||||
|
||||
(The MIT License)
|
||||
|
||||
Copyright (c) 2018 marvin + konsorten GmbH (open-source@konsorten.de)
|
||||
|
||||
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.
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
// +build windows
|
||||
|
||||
package sequences
|
||||
|
||||
import (
|
||||
"syscall"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
var (
|
||||
kernel32Dll *syscall.LazyDLL = syscall.NewLazyDLL("Kernel32.dll")
|
||||
setConsoleMode *syscall.LazyProc = kernel32Dll.NewProc("SetConsoleMode")
|
||||
)
|
||||
|
||||
func EnableVirtualTerminalProcessing(stream syscall.Handle, enable bool) error {
|
||||
const ENABLE_VIRTUAL_TERMINAL_PROCESSING uint32 = 0x4
|
||||
|
||||
var mode uint32
|
||||
err := syscall.GetConsoleMode(syscall.Stdout, &mode)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if enable {
|
||||
mode |= ENABLE_VIRTUAL_TERMINAL_PROCESSING
|
||||
} else {
|
||||
mode &^= ENABLE_VIRTUAL_TERMINAL_PROCESSING
|
||||
}
|
||||
|
||||
ret, _, err := setConsoleMode.Call(uintptr(unsafe.Pointer(stream)), uintptr(mode))
|
||||
if ret == 0 {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
-1
Submodule vendor/github.com/satori/go.uuid deleted from b2ce2384e1
+23
@@ -0,0 +1,23 @@
|
||||
language: go
|
||||
sudo: false
|
||||
go:
|
||||
- 1.2
|
||||
- 1.3
|
||||
- 1.4
|
||||
- 1.5
|
||||
- 1.6
|
||||
- 1.7
|
||||
- 1.8
|
||||
- 1.9
|
||||
- tip
|
||||
matrix:
|
||||
allow_failures:
|
||||
- go: tip
|
||||
fast_finish: true
|
||||
before_install:
|
||||
- go get github.com/mattn/goveralls
|
||||
- go get golang.org/x/tools/cmd/cover
|
||||
script:
|
||||
- $HOME/gopath/bin/goveralls -service=travis-ci
|
||||
notifications:
|
||||
email: false
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
Copyright (C) 2013-2018 by Maxim Bublis <b@codemonkey.ru>
|
||||
|
||||
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.
|
||||
+65
@@ -0,0 +1,65 @@
|
||||
# UUID package for Go language
|
||||
|
||||
[](https://travis-ci.org/satori/go.uuid)
|
||||
[](https://coveralls.io/github/satori/go.uuid)
|
||||
[](http://godoc.org/github.com/satori/go.uuid)
|
||||
|
||||
This package provides pure Go implementation of Universally Unique Identifier (UUID). Supported both creation and parsing of UUIDs.
|
||||
|
||||
With 100% test coverage and benchmarks out of box.
|
||||
|
||||
Supported versions:
|
||||
* Version 1, based on timestamp and MAC address (RFC 4122)
|
||||
* Version 2, based on timestamp, MAC address and POSIX UID/GID (DCE 1.1)
|
||||
* Version 3, based on MD5 hashing (RFC 4122)
|
||||
* Version 4, based on random numbers (RFC 4122)
|
||||
* Version 5, based on SHA-1 hashing (RFC 4122)
|
||||
|
||||
## Installation
|
||||
|
||||
Use the `go` command:
|
||||
|
||||
$ go get github.com/satori/go.uuid
|
||||
|
||||
## Requirements
|
||||
|
||||
UUID package requires Go >= 1.2.
|
||||
|
||||
## Example
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/satori/go.uuid"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// Creating UUID Version 4
|
||||
u1 := uuid.NewV4()
|
||||
fmt.Printf("UUIDv4: %s\n", u1)
|
||||
|
||||
// Parsing UUID from string input
|
||||
u2, err := uuid.FromString("6ba7b810-9dad-11d1-80b4-00c04fd430c8")
|
||||
if err != nil {
|
||||
fmt.Printf("Something gone wrong: %s", err)
|
||||
}
|
||||
fmt.Printf("Successfully parsed: %s", u2)
|
||||
}
|
||||
```
|
||||
|
||||
## Documentation
|
||||
|
||||
[Documentation](http://godoc.org/github.com/satori/go.uuid) is hosted at GoDoc project.
|
||||
|
||||
## Links
|
||||
* [RFC 4122](http://tools.ietf.org/html/rfc4122)
|
||||
* [DCE 1.1: Authentication and Security Services](http://pubs.opengroup.org/onlinepubs/9696989899/chap5.htm#tagcjh_08_02_01_01)
|
||||
|
||||
## Copyright
|
||||
|
||||
Copyright (C) 2013-2018 by Maxim Bublis <b@codemonkey.ru>.
|
||||
|
||||
UUID package released under MIT License.
|
||||
See [LICENSE](https://github.com/satori/go.uuid/blob/master/LICENSE) for details.
|
||||
+206
@@ -0,0 +1,206 @@
|
||||
// Copyright (C) 2013-2018 by Maxim Bublis <b@codemonkey.ru>
|
||||
//
|
||||
// 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.
|
||||
|
||||
package uuid
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// FromBytes returns UUID converted from raw byte slice input.
|
||||
// It will return error if the slice isn't 16 bytes long.
|
||||
func FromBytes(input []byte) (u UUID, err error) {
|
||||
err = u.UnmarshalBinary(input)
|
||||
return
|
||||
}
|
||||
|
||||
// FromBytesOrNil returns UUID converted from raw byte slice input.
|
||||
// Same behavior as FromBytes, but returns a Nil UUID on error.
|
||||
func FromBytesOrNil(input []byte) UUID {
|
||||
uuid, err := FromBytes(input)
|
||||
if err != nil {
|
||||
return Nil
|
||||
}
|
||||
return uuid
|
||||
}
|
||||
|
||||
// FromString returns UUID parsed from string input.
|
||||
// Input is expected in a form accepted by UnmarshalText.
|
||||
func FromString(input string) (u UUID, err error) {
|
||||
err = u.UnmarshalText([]byte(input))
|
||||
return
|
||||
}
|
||||
|
||||
// FromStringOrNil returns UUID parsed from string input.
|
||||
// Same behavior as FromString, but returns a Nil UUID on error.
|
||||
func FromStringOrNil(input string) UUID {
|
||||
uuid, err := FromString(input)
|
||||
if err != nil {
|
||||
return Nil
|
||||
}
|
||||
return uuid
|
||||
}
|
||||
|
||||
// MarshalText implements the encoding.TextMarshaler interface.
|
||||
// The encoding is the same as returned by String.
|
||||
func (u UUID) MarshalText() (text []byte, err error) {
|
||||
text = []byte(u.String())
|
||||
return
|
||||
}
|
||||
|
||||
// UnmarshalText implements the encoding.TextUnmarshaler interface.
|
||||
// Following formats are supported:
|
||||
// "6ba7b810-9dad-11d1-80b4-00c04fd430c8",
|
||||
// "{6ba7b810-9dad-11d1-80b4-00c04fd430c8}",
|
||||
// "urn:uuid:6ba7b810-9dad-11d1-80b4-00c04fd430c8"
|
||||
// "6ba7b8109dad11d180b400c04fd430c8"
|
||||
// ABNF for supported UUID text representation follows:
|
||||
// uuid := canonical | hashlike | braced | urn
|
||||
// plain := canonical | hashlike
|
||||
// canonical := 4hexoct '-' 2hexoct '-' 2hexoct '-' 6hexoct
|
||||
// hashlike := 12hexoct
|
||||
// braced := '{' plain '}'
|
||||
// urn := URN ':' UUID-NID ':' plain
|
||||
// URN := 'urn'
|
||||
// UUID-NID := 'uuid'
|
||||
// 12hexoct := 6hexoct 6hexoct
|
||||
// 6hexoct := 4hexoct 2hexoct
|
||||
// 4hexoct := 2hexoct 2hexoct
|
||||
// 2hexoct := hexoct hexoct
|
||||
// hexoct := hexdig hexdig
|
||||
// hexdig := '0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9' |
|
||||
// 'a' | 'b' | 'c' | 'd' | 'e' | 'f' |
|
||||
// 'A' | 'B' | 'C' | 'D' | 'E' | 'F'
|
||||
func (u *UUID) UnmarshalText(text []byte) (err error) {
|
||||
switch len(text) {
|
||||
case 32:
|
||||
return u.decodeHashLike(text)
|
||||
case 36:
|
||||
return u.decodeCanonical(text)
|
||||
case 38:
|
||||
return u.decodeBraced(text)
|
||||
case 41:
|
||||
fallthrough
|
||||
case 45:
|
||||
return u.decodeURN(text)
|
||||
default:
|
||||
return fmt.Errorf("uuid: incorrect UUID length: %s", text)
|
||||
}
|
||||
}
|
||||
|
||||
// decodeCanonical decodes UUID string in format
|
||||
// "6ba7b810-9dad-11d1-80b4-00c04fd430c8".
|
||||
func (u *UUID) decodeCanonical(t []byte) (err error) {
|
||||
if t[8] != '-' || t[13] != '-' || t[18] != '-' || t[23] != '-' {
|
||||
return fmt.Errorf("uuid: incorrect UUID format %s", t)
|
||||
}
|
||||
|
||||
src := t[:]
|
||||
dst := u[:]
|
||||
|
||||
for i, byteGroup := range byteGroups {
|
||||
if i > 0 {
|
||||
src = src[1:] // skip dash
|
||||
}
|
||||
_, err = hex.Decode(dst[:byteGroup/2], src[:byteGroup])
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
src = src[byteGroup:]
|
||||
dst = dst[byteGroup/2:]
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// decodeHashLike decodes UUID string in format
|
||||
// "6ba7b8109dad11d180b400c04fd430c8".
|
||||
func (u *UUID) decodeHashLike(t []byte) (err error) {
|
||||
src := t[:]
|
||||
dst := u[:]
|
||||
|
||||
if _, err = hex.Decode(dst, src); err != nil {
|
||||
return err
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// decodeBraced decodes UUID string in format
|
||||
// "{6ba7b810-9dad-11d1-80b4-00c04fd430c8}" or in format
|
||||
// "{6ba7b8109dad11d180b400c04fd430c8}".
|
||||
func (u *UUID) decodeBraced(t []byte) (err error) {
|
||||
l := len(t)
|
||||
|
||||
if t[0] != '{' || t[l-1] != '}' {
|
||||
return fmt.Errorf("uuid: incorrect UUID format %s", t)
|
||||
}
|
||||
|
||||
return u.decodePlain(t[1 : l-1])
|
||||
}
|
||||
|
||||
// decodeURN decodes UUID string in format
|
||||
// "urn:uuid:6ba7b810-9dad-11d1-80b4-00c04fd430c8" or in format
|
||||
// "urn:uuid:6ba7b8109dad11d180b400c04fd430c8".
|
||||
func (u *UUID) decodeURN(t []byte) (err error) {
|
||||
total := len(t)
|
||||
|
||||
urn_uuid_prefix := t[:9]
|
||||
|
||||
if !bytes.Equal(urn_uuid_prefix, urnPrefix) {
|
||||
return fmt.Errorf("uuid: incorrect UUID format: %s", t)
|
||||
}
|
||||
|
||||
return u.decodePlain(t[9:total])
|
||||
}
|
||||
|
||||
// decodePlain decodes UUID string in canonical format
|
||||
// "6ba7b810-9dad-11d1-80b4-00c04fd430c8" or in hash-like format
|
||||
// "6ba7b8109dad11d180b400c04fd430c8".
|
||||
func (u *UUID) decodePlain(t []byte) (err error) {
|
||||
switch len(t) {
|
||||
case 32:
|
||||
return u.decodeHashLike(t)
|
||||
case 36:
|
||||
return u.decodeCanonical(t)
|
||||
default:
|
||||
return fmt.Errorf("uuid: incorrrect UUID length: %s", t)
|
||||
}
|
||||
}
|
||||
|
||||
// MarshalBinary implements the encoding.BinaryMarshaler interface.
|
||||
func (u UUID) MarshalBinary() (data []byte, err error) {
|
||||
data = u.Bytes()
|
||||
return
|
||||
}
|
||||
|
||||
// UnmarshalBinary implements the encoding.BinaryUnmarshaler interface.
|
||||
// It will return error if the slice isn't 16 bytes long.
|
||||
func (u *UUID) UnmarshalBinary(data []byte) (err error) {
|
||||
if len(data) != Size {
|
||||
err = fmt.Errorf("uuid: UUID must be exactly 16 bytes long, got %d bytes", len(data))
|
||||
return
|
||||
}
|
||||
copy(u[:], data)
|
||||
|
||||
return
|
||||
}
|
||||
+239
@@ -0,0 +1,239 @@
|
||||
// Copyright (C) 2013-2018 by Maxim Bublis <b@codemonkey.ru>
|
||||
//
|
||||
// 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.
|
||||
|
||||
package uuid
|
||||
|
||||
import (
|
||||
"crypto/md5"
|
||||
"crypto/rand"
|
||||
"crypto/sha1"
|
||||
"encoding/binary"
|
||||
"hash"
|
||||
"net"
|
||||
"os"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Difference in 100-nanosecond intervals between
|
||||
// UUID epoch (October 15, 1582) and Unix epoch (January 1, 1970).
|
||||
const epochStart = 122192928000000000
|
||||
|
||||
var (
|
||||
global = newDefaultGenerator()
|
||||
|
||||
epochFunc = unixTimeFunc
|
||||
posixUID = uint32(os.Getuid())
|
||||
posixGID = uint32(os.Getgid())
|
||||
)
|
||||
|
||||
// NewV1 returns UUID based on current timestamp and MAC address.
|
||||
func NewV1() UUID {
|
||||
return global.NewV1()
|
||||
}
|
||||
|
||||
// NewV2 returns DCE Security UUID based on POSIX UID/GID.
|
||||
func NewV2(domain byte) UUID {
|
||||
return global.NewV2(domain)
|
||||
}
|
||||
|
||||
// NewV3 returns UUID based on MD5 hash of namespace UUID and name.
|
||||
func NewV3(ns UUID, name string) UUID {
|
||||
return global.NewV3(ns, name)
|
||||
}
|
||||
|
||||
// NewV4 returns random generated UUID.
|
||||
func NewV4() UUID {
|
||||
return global.NewV4()
|
||||
}
|
||||
|
||||
// NewV5 returns UUID based on SHA-1 hash of namespace UUID and name.
|
||||
func NewV5(ns UUID, name string) UUID {
|
||||
return global.NewV5(ns, name)
|
||||
}
|
||||
|
||||
// Generator provides interface for generating UUIDs.
|
||||
type Generator interface {
|
||||
NewV1() UUID
|
||||
NewV2(domain byte) UUID
|
||||
NewV3(ns UUID, name string) UUID
|
||||
NewV4() UUID
|
||||
NewV5(ns UUID, name string) UUID
|
||||
}
|
||||
|
||||
// Default generator implementation.
|
||||
type generator struct {
|
||||
storageOnce sync.Once
|
||||
storageMutex sync.Mutex
|
||||
|
||||
lastTime uint64
|
||||
clockSequence uint16
|
||||
hardwareAddr [6]byte
|
||||
}
|
||||
|
||||
func newDefaultGenerator() Generator {
|
||||
return &generator{}
|
||||
}
|
||||
|
||||
// NewV1 returns UUID based on current timestamp and MAC address.
|
||||
func (g *generator) NewV1() UUID {
|
||||
u := UUID{}
|
||||
|
||||
timeNow, clockSeq, hardwareAddr := g.getStorage()
|
||||
|
||||
binary.BigEndian.PutUint32(u[0:], uint32(timeNow))
|
||||
binary.BigEndian.PutUint16(u[4:], uint16(timeNow>>32))
|
||||
binary.BigEndian.PutUint16(u[6:], uint16(timeNow>>48))
|
||||
binary.BigEndian.PutUint16(u[8:], clockSeq)
|
||||
|
||||
copy(u[10:], hardwareAddr)
|
||||
|
||||
u.SetVersion(V1)
|
||||
u.SetVariant(VariantRFC4122)
|
||||
|
||||
return u
|
||||
}
|
||||
|
||||
// NewV2 returns DCE Security UUID based on POSIX UID/GID.
|
||||
func (g *generator) NewV2(domain byte) UUID {
|
||||
u := UUID{}
|
||||
|
||||
timeNow, clockSeq, hardwareAddr := g.getStorage()
|
||||
|
||||
switch domain {
|
||||
case DomainPerson:
|
||||
binary.BigEndian.PutUint32(u[0:], posixUID)
|
||||
case DomainGroup:
|
||||
binary.BigEndian.PutUint32(u[0:], posixGID)
|
||||
}
|
||||
|
||||
binary.BigEndian.PutUint16(u[4:], uint16(timeNow>>32))
|
||||
binary.BigEndian.PutUint16(u[6:], uint16(timeNow>>48))
|
||||
binary.BigEndian.PutUint16(u[8:], clockSeq)
|
||||
u[9] = domain
|
||||
|
||||
copy(u[10:], hardwareAddr)
|
||||
|
||||
u.SetVersion(V2)
|
||||
u.SetVariant(VariantRFC4122)
|
||||
|
||||
return u
|
||||
}
|
||||
|
||||
// NewV3 returns UUID based on MD5 hash of namespace UUID and name.
|
||||
func (g *generator) NewV3(ns UUID, name string) UUID {
|
||||
u := newFromHash(md5.New(), ns, name)
|
||||
u.SetVersion(V3)
|
||||
u.SetVariant(VariantRFC4122)
|
||||
|
||||
return u
|
||||
}
|
||||
|
||||
// NewV4 returns random generated UUID.
|
||||
func (g *generator) NewV4() UUID {
|
||||
u := UUID{}
|
||||
g.safeRandom(u[:])
|
||||
u.SetVersion(V4)
|
||||
u.SetVariant(VariantRFC4122)
|
||||
|
||||
return u
|
||||
}
|
||||
|
||||
// NewV5 returns UUID based on SHA-1 hash of namespace UUID and name.
|
||||
func (g *generator) NewV5(ns UUID, name string) UUID {
|
||||
u := newFromHash(sha1.New(), ns, name)
|
||||
u.SetVersion(V5)
|
||||
u.SetVariant(VariantRFC4122)
|
||||
|
||||
return u
|
||||
}
|
||||
|
||||
func (g *generator) initStorage() {
|
||||
g.initClockSequence()
|
||||
g.initHardwareAddr()
|
||||
}
|
||||
|
||||
func (g *generator) initClockSequence() {
|
||||
buf := make([]byte, 2)
|
||||
g.safeRandom(buf)
|
||||
g.clockSequence = binary.BigEndian.Uint16(buf)
|
||||
}
|
||||
|
||||
func (g *generator) initHardwareAddr() {
|
||||
interfaces, err := net.Interfaces()
|
||||
if err == nil {
|
||||
for _, iface := range interfaces {
|
||||
if len(iface.HardwareAddr) >= 6 {
|
||||
copy(g.hardwareAddr[:], iface.HardwareAddr)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize hardwareAddr randomly in case
|
||||
// of real network interfaces absence
|
||||
g.safeRandom(g.hardwareAddr[:])
|
||||
|
||||
// Set multicast bit as recommended in RFC 4122
|
||||
g.hardwareAddr[0] |= 0x01
|
||||
}
|
||||
|
||||
func (g *generator) safeRandom(dest []byte) {
|
||||
if _, err := rand.Read(dest); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
// Returns UUID v1/v2 storage state.
|
||||
// Returns epoch timestamp, clock sequence, and hardware address.
|
||||
func (g *generator) getStorage() (uint64, uint16, []byte) {
|
||||
g.storageOnce.Do(g.initStorage)
|
||||
|
||||
g.storageMutex.Lock()
|
||||
defer g.storageMutex.Unlock()
|
||||
|
||||
timeNow := epochFunc()
|
||||
// Clock changed backwards since last UUID generation.
|
||||
// Should increase clock sequence.
|
||||
if timeNow <= g.lastTime {
|
||||
g.clockSequence++
|
||||
}
|
||||
g.lastTime = timeNow
|
||||
|
||||
return timeNow, g.clockSequence, g.hardwareAddr[:]
|
||||
}
|
||||
|
||||
// Returns difference in 100-nanosecond intervals between
|
||||
// UUID epoch (October 15, 1582) and current time.
|
||||
// This is default epoch calculation function.
|
||||
func unixTimeFunc() uint64 {
|
||||
return epochStart + uint64(time.Now().UnixNano()/100)
|
||||
}
|
||||
|
||||
// Returns UUID based on hashing of namespace UUID and name.
|
||||
func newFromHash(h hash.Hash, ns UUID, name string) UUID {
|
||||
u := UUID{}
|
||||
h.Write(ns[:])
|
||||
h.Write([]byte(name))
|
||||
copy(u[:], h.Sum(nil))
|
||||
|
||||
return u
|
||||
}
|
||||
+78
@@ -0,0 +1,78 @@
|
||||
// Copyright (C) 2013-2018 by Maxim Bublis <b@codemonkey.ru>
|
||||
//
|
||||
// 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.
|
||||
|
||||
package uuid
|
||||
|
||||
import (
|
||||
"database/sql/driver"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// Value implements the driver.Valuer interface.
|
||||
func (u UUID) Value() (driver.Value, error) {
|
||||
return u.String(), nil
|
||||
}
|
||||
|
||||
// Scan implements the sql.Scanner interface.
|
||||
// A 16-byte slice is handled by UnmarshalBinary, while
|
||||
// a longer byte slice or a string is handled by UnmarshalText.
|
||||
func (u *UUID) Scan(src interface{}) error {
|
||||
switch src := src.(type) {
|
||||
case []byte:
|
||||
if len(src) == Size {
|
||||
return u.UnmarshalBinary(src)
|
||||
}
|
||||
return u.UnmarshalText(src)
|
||||
|
||||
case string:
|
||||
return u.UnmarshalText([]byte(src))
|
||||
}
|
||||
|
||||
return fmt.Errorf("uuid: cannot convert %T to UUID", src)
|
||||
}
|
||||
|
||||
// NullUUID can be used with the standard sql package to represent a
|
||||
// UUID value that can be NULL in the database
|
||||
type NullUUID struct {
|
||||
UUID UUID
|
||||
Valid bool
|
||||
}
|
||||
|
||||
// Value implements the driver.Valuer interface.
|
||||
func (u NullUUID) Value() (driver.Value, error) {
|
||||
if !u.Valid {
|
||||
return nil, nil
|
||||
}
|
||||
// Delegate to UUID Value function
|
||||
return u.UUID.Value()
|
||||
}
|
||||
|
||||
// Scan implements the sql.Scanner interface.
|
||||
func (u *NullUUID) Scan(src interface{}) error {
|
||||
if src == nil {
|
||||
u.UUID, u.Valid = Nil, false
|
||||
return nil
|
||||
}
|
||||
|
||||
// Delegate to UUID Scan function
|
||||
u.Valid = true
|
||||
return u.UUID.Scan(src)
|
||||
}
|
||||
+161
@@ -0,0 +1,161 @@
|
||||
// Copyright (C) 2013-2018 by Maxim Bublis <b@codemonkey.ru>
|
||||
//
|
||||
// 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.
|
||||
|
||||
// Package uuid provides implementation of Universally Unique Identifier (UUID).
|
||||
// Supported versions are 1, 3, 4 and 5 (as specified in RFC 4122) and
|
||||
// version 2 (as specified in DCE 1.1).
|
||||
package uuid
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/hex"
|
||||
)
|
||||
|
||||
// Size of a UUID in bytes.
|
||||
const Size = 16
|
||||
|
||||
// UUID representation compliant with specification
|
||||
// described in RFC 4122.
|
||||
type UUID [Size]byte
|
||||
|
||||
// UUID versions
|
||||
const (
|
||||
_ byte = iota
|
||||
V1
|
||||
V2
|
||||
V3
|
||||
V4
|
||||
V5
|
||||
)
|
||||
|
||||
// UUID layout variants.
|
||||
const (
|
||||
VariantNCS byte = iota
|
||||
VariantRFC4122
|
||||
VariantMicrosoft
|
||||
VariantFuture
|
||||
)
|
||||
|
||||
// UUID DCE domains.
|
||||
const (
|
||||
DomainPerson = iota
|
||||
DomainGroup
|
||||
DomainOrg
|
||||
)
|
||||
|
||||
// String parse helpers.
|
||||
var (
|
||||
urnPrefix = []byte("urn:uuid:")
|
||||
byteGroups = []int{8, 4, 4, 4, 12}
|
||||
)
|
||||
|
||||
// Nil is special form of UUID that is specified to have all
|
||||
// 128 bits set to zero.
|
||||
var Nil = UUID{}
|
||||
|
||||
// Predefined namespace UUIDs.
|
||||
var (
|
||||
NamespaceDNS = Must(FromString("6ba7b810-9dad-11d1-80b4-00c04fd430c8"))
|
||||
NamespaceURL = Must(FromString("6ba7b811-9dad-11d1-80b4-00c04fd430c8"))
|
||||
NamespaceOID = Must(FromString("6ba7b812-9dad-11d1-80b4-00c04fd430c8"))
|
||||
NamespaceX500 = Must(FromString("6ba7b814-9dad-11d1-80b4-00c04fd430c8"))
|
||||
)
|
||||
|
||||
// Equal returns true if u1 and u2 equals, otherwise returns false.
|
||||
func Equal(u1 UUID, u2 UUID) bool {
|
||||
return bytes.Equal(u1[:], u2[:])
|
||||
}
|
||||
|
||||
// Version returns algorithm version used to generate UUID.
|
||||
func (u UUID) Version() byte {
|
||||
return u[6] >> 4
|
||||
}
|
||||
|
||||
// Variant returns UUID layout variant.
|
||||
func (u UUID) Variant() byte {
|
||||
switch {
|
||||
case (u[8] >> 7) == 0x00:
|
||||
return VariantNCS
|
||||
case (u[8] >> 6) == 0x02:
|
||||
return VariantRFC4122
|
||||
case (u[8] >> 5) == 0x06:
|
||||
return VariantMicrosoft
|
||||
case (u[8] >> 5) == 0x07:
|
||||
fallthrough
|
||||
default:
|
||||
return VariantFuture
|
||||
}
|
||||
}
|
||||
|
||||
// Bytes returns bytes slice representation of UUID.
|
||||
func (u UUID) Bytes() []byte {
|
||||
return u[:]
|
||||
}
|
||||
|
||||
// Returns canonical string representation of UUID:
|
||||
// xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx.
|
||||
func (u UUID) String() string {
|
||||
buf := make([]byte, 36)
|
||||
|
||||
hex.Encode(buf[0:8], u[0:4])
|
||||
buf[8] = '-'
|
||||
hex.Encode(buf[9:13], u[4:6])
|
||||
buf[13] = '-'
|
||||
hex.Encode(buf[14:18], u[6:8])
|
||||
buf[18] = '-'
|
||||
hex.Encode(buf[19:23], u[8:10])
|
||||
buf[23] = '-'
|
||||
hex.Encode(buf[24:], u[10:])
|
||||
|
||||
return string(buf)
|
||||
}
|
||||
|
||||
// SetVersion sets version bits.
|
||||
func (u *UUID) SetVersion(v byte) {
|
||||
u[6] = (u[6] & 0x0f) | (v << 4)
|
||||
}
|
||||
|
||||
// SetVariant sets variant bits.
|
||||
func (u *UUID) SetVariant(v byte) {
|
||||
switch v {
|
||||
case VariantNCS:
|
||||
u[8] = (u[8]&(0xff>>1) | (0x00 << 7))
|
||||
case VariantRFC4122:
|
||||
u[8] = (u[8]&(0xff>>2) | (0x02 << 6))
|
||||
case VariantMicrosoft:
|
||||
u[8] = (u[8]&(0xff>>3) | (0x06 << 5))
|
||||
case VariantFuture:
|
||||
fallthrough
|
||||
default:
|
||||
u[8] = (u[8]&(0xff>>3) | (0x07 << 5))
|
||||
}
|
||||
}
|
||||
|
||||
// Must is a helper that wraps a call to a function returning (UUID, error)
|
||||
// and panics if the error is non-nil. It is intended for use in variable
|
||||
// initializations such as
|
||||
// var packageUUID = uuid.Must(uuid.FromString("123e4567-e89b-12d3-a456-426655440000"));
|
||||
func Must(u UUID, err error) UUID {
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return u
|
||||
}
|
||||
-1
Submodule vendor/github.com/segmentio/kafka-go deleted from 963714c486
+34
@@ -0,0 +1,34 @@
|
||||
# Compiled Object files, Static and Dynamic libs (Shared Objects)
|
||||
*.o
|
||||
*.a
|
||||
*.so
|
||||
|
||||
# Folders
|
||||
_obj
|
||||
_test
|
||||
|
||||
# Architecture specific extensions/prefixes
|
||||
*.[568vq]
|
||||
[568vq].out
|
||||
|
||||
*.cgo1.go
|
||||
*.cgo2.c
|
||||
_cgo_defun.c
|
||||
_cgo_gotypes.go
|
||||
_cgo_export.*
|
||||
|
||||
_testmain.go
|
||||
|
||||
*.exe
|
||||
*.test
|
||||
*.prof
|
||||
/kafkacli
|
||||
|
||||
# Emacs
|
||||
*~
|
||||
|
||||
# Goland
|
||||
.idea
|
||||
|
||||
# govendor
|
||||
/vendor/*/
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2017 Segment
|
||||
|
||||
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.
|
||||
+301
@@ -0,0 +1,301 @@
|
||||
# kafka-go [](https://circleci.com/gh/segmentio/kafka-go) [](https://goreportcard.com/report/github.com/segmentio/kafka-go) [](https://godoc.org/github.com/segmentio/kafka-go)
|
||||
|
||||
## Motivations
|
||||
|
||||
We rely on both Go and Kafka a lot at Segment. Unfortunately, the state of the Go
|
||||
client libraries for Kafka at the time of this writing was not ideal. The available
|
||||
options were:
|
||||
|
||||
- [sarama](https://github.com/Shopify/sarama), which is by far the most popular
|
||||
but is quite difficult to work with. It is poorly documented, the API exposes
|
||||
low level concepts of the Kafka protocol, and it doesn't support recent Go features
|
||||
like [contexts](https://golang.org/pkg/context/). It also passes all values as
|
||||
pointers which causes large numbers of dynamic memory allocations, more frequent
|
||||
garbage collections, and higher memory usage.
|
||||
|
||||
- [confluent-kafka-go](https://github.com/confluentinc/confluent-kafka-go) is a
|
||||
cgo based wrapper around [librdkafka](https://github.com/edenhill/librdkafka),
|
||||
which means it introduces a dependency to a C library on all Go code that uses
|
||||
the package. It has much better documentation than sarama but still lacks support
|
||||
for Go contexts.
|
||||
|
||||
- [goka](https://github.com/lovoo/goka) is a more recent Kafka client for Go
|
||||
which focuses on a specific usage pattern. It provides abstractions for using Kafka
|
||||
as a message passing bus between services rather than an ordered log of events, but
|
||||
this is not the typical use case of Kafka for us at Segment. The package also
|
||||
depends on sarama for all interactions with Kafka.
|
||||
|
||||
This is where `kafka-go` comes into play. It provides both low and high level
|
||||
APIs for interacting with Kafka, mirroring concepts and implementing interfaces of
|
||||
the Go standard library to make it easy to use and integrate with existing
|
||||
software.
|
||||
|
||||
## Connection [](https://godoc.org/github.com/segmentio/kafka-go#Conn)
|
||||
|
||||
The `Conn` type is the core of the `kafka-go` package. It wraps around a raw
|
||||
network connection to expose a low-level API to a Kafka server.
|
||||
|
||||
Here are some examples showing typical use of a connection object:
|
||||
```go
|
||||
// to produce messages
|
||||
topic := "my-topic"
|
||||
partition := 0
|
||||
|
||||
conn, _ := kafka.DialLeader(context.Background(), "tcp", "localhost:9092", topic, partition)
|
||||
|
||||
conn.SetWriteDeadline(time.Now().Add(10*time.Second))
|
||||
conn.WriteMessages(
|
||||
kafka.Message{Value: []byte("one!")},
|
||||
kafka.Message{Value: []byte("two!")},
|
||||
kafka.Message{Value: []byte("three!")},
|
||||
)
|
||||
|
||||
conn.Close()
|
||||
```
|
||||
```go
|
||||
// to consume messages
|
||||
topic := "my-topic"
|
||||
partition := 0
|
||||
|
||||
conn, _ := kafka.DialLeader(context.Background(), "tcp", "localhost:9092", topic, partition)
|
||||
|
||||
conn.SetReadDeadline(time.Now().Add(10*time.Second))
|
||||
batch := conn.ReadBatch(10e3, 1e6) // fetch 10KB min, 1MB max
|
||||
|
||||
b := make([]byte, 10e3) // 10KB max per message
|
||||
for {
|
||||
_, err := batch.Read(b)
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
fmt.Println(string(b))
|
||||
}
|
||||
|
||||
batch.Close()
|
||||
conn.Close()
|
||||
```
|
||||
|
||||
Because it is low level, the `Conn` type turns out to be a great building block
|
||||
for higher level abstractions, like the `Reader` for example.
|
||||
|
||||
## Reader [](https://godoc.org/github.com/segmentio/kafka-go#Reader)
|
||||
|
||||
A `Reader` is another concept exposed by the `kafka-go` package, which intends
|
||||
to make it simpler to implement the typical use case of consuming from a single
|
||||
topic-partition pair.
|
||||
A `Reader` also automatically handles reconnections and offset management, and
|
||||
exposes an API that supports asynchronous cancellations and timeouts using Go
|
||||
contexts.
|
||||
|
||||
```go
|
||||
// make a new reader that consumes from topic-A, partition 0, at offset 42
|
||||
r := kafka.NewReader(kafka.ReaderConfig{
|
||||
Brokers: []string{"localhost:9092"},
|
||||
Topic: "topic-A",
|
||||
Partition: 0,
|
||||
MinBytes: 10e3, // 10KB
|
||||
MaxBytes: 10e6, // 10MB
|
||||
})
|
||||
r.SetOffset(42)
|
||||
|
||||
for {
|
||||
m, err := r.ReadMessage(context.Background())
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
fmt.Printf("message at offset %d: %s = %s\n", m.Offset, string(m.Key), string(m.Value))
|
||||
}
|
||||
|
||||
r.Close()
|
||||
```
|
||||
|
||||
### Consumer Groups
|
||||
|
||||
```kafka-go``` also supports Kafka consumer groups including broker managed offsets.
|
||||
To enable consumer groups, simplify specify the GroupID in the ReaderConfig.
|
||||
|
||||
ReadMessage automatically commits offsets when using consumer groups.
|
||||
|
||||
```go
|
||||
// make a new reader that consumes from topic-A
|
||||
r := kafka.NewReader(kafka.ReaderConfig{
|
||||
Brokers: []string{"localhost:9092"},
|
||||
GroupID: "consumer-group-id",
|
||||
Topic: "topic-A",
|
||||
MinBytes: 10e3, // 10KB
|
||||
MaxBytes: 10e6, // 10MB
|
||||
})
|
||||
|
||||
for {
|
||||
m, err := r.ReadMessage(context.Background())
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
fmt.Printf("message at topic/partition/offset %v/%v/%v: %s = %s\n", m.Topic, m.Partition, m.Offset, string(m.Key), string(m.Value))
|
||||
}
|
||||
|
||||
r.Close()
|
||||
```
|
||||
|
||||
There are a number of limitations when using consumer groups:
|
||||
|
||||
* ```(*Reader).SetOffset``` will return an error when GroupID is set
|
||||
* ```(*Reader).Offset``` will always return ```-1``` when GroupID is set
|
||||
* ```(*Reader).Lag``` will always return ```-1``` when GroupID is set
|
||||
* ```(*Reader).ReadLag``` will return an error when GroupID is set
|
||||
* ```(*Reader).Stats``` will return a partition of ```-1``` when GroupID is set
|
||||
|
||||
### Explicit Commits
|
||||
|
||||
```kafka-go``` also supports explicit commits. Instead of calling ```ReadMessage```,
|
||||
call ```FetchMessage``` followed by ```CommitMessages```.
|
||||
|
||||
```go
|
||||
ctx := context.Background()
|
||||
for {
|
||||
m, err := r.FetchMessage(ctx)
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
fmt.Printf("message at topic/partition/offset %v/%v/%v: %s = %s\n", m.Topic, m.Partition, m.Offset, string(m.Key), string(m.Value))
|
||||
r.CommitMessages(ctx, m)
|
||||
}
|
||||
```
|
||||
|
||||
### Managing Commits
|
||||
|
||||
By default, CommitMessages will synchronously commit offsets to Kafka. For
|
||||
improved performance, you can instead periodically commit offsets to Kafka
|
||||
by setting CommitInterval on the ReaderConfig.
|
||||
|
||||
|
||||
```go
|
||||
// make a new reader that consumes from topic-A
|
||||
r := kafka.NewReader(kafka.ReaderConfig{
|
||||
Brokers: []string{"localhost:9092"},
|
||||
GroupID: "consumer-group-id",
|
||||
Topic: "topic-A",
|
||||
MinBytes: 10e3, // 10KB
|
||||
MaxBytes: 10e6, // 10MB
|
||||
CommitInterval: time.Second, // flushes commits to Kafka every second
|
||||
})
|
||||
```
|
||||
|
||||
## Writer [](https://godoc.org/github.com/segmentio/kafka-go#Writer)
|
||||
|
||||
To produce messages to Kafka, a program may use the low-level `Conn` API, but
|
||||
the package also provides a higher level `Writer` type which is more appropriate
|
||||
to use in most cases as it provides additional features:
|
||||
|
||||
- Automatic retries and reconnections on errors.
|
||||
- Configurable distribution of messages across available partitions.
|
||||
- Synchronous or asynchronous writes of messages to Kafka.
|
||||
- Asynchronous cancellation using contexts.
|
||||
- Flushing of pending messages on close to support graceful shutdowns.
|
||||
|
||||
```go
|
||||
// make a writer that produces to topic-A, using the least-bytes distribution
|
||||
w := kafka.NewWriter(kafka.WriterConfig{
|
||||
Brokers: []string{"localhost:9092"},
|
||||
Topic: "topic-A",
|
||||
Balancer: &kafka.LeastBytes{},
|
||||
})
|
||||
|
||||
w.WriteMessages(context.Background(),
|
||||
kafka.Message{
|
||||
Key: []byte("Key-A"),
|
||||
Value: []byte("Hello World!"),
|
||||
},
|
||||
kafka.Message{
|
||||
Key: []byte("Key-B"),
|
||||
Value: []byte("One!"),
|
||||
},
|
||||
kafka.Message{
|
||||
Key: []byte("Key-C"),
|
||||
Value: []byte("Two!"),
|
||||
},
|
||||
)
|
||||
|
||||
w.Close()
|
||||
```
|
||||
|
||||
**Note:** Even though kafka.Message contain ```Topic``` and ```Partition``` fields, they **MUST NOT** be
|
||||
set when writing messages. They are intended for read use only.
|
||||
|
||||
### Compatibility with Sarama
|
||||
|
||||
If you're switching from Sarama and need/want to use the same algorithm for message
|
||||
partitioning, you can use the ```kafka.Hash``` balancer. ```kafka.Hash``` routes
|
||||
messages to the same partitions that sarama's default partitioner would route to.
|
||||
|
||||
```go
|
||||
w := kafka.NewWriter(kafka.WriterConfig{
|
||||
Brokers: []string{"localhost:9092"},
|
||||
Topic: "topic-A",
|
||||
Balancer: &kafka.Hash{},
|
||||
})
|
||||
```
|
||||
|
||||
### Compression
|
||||
|
||||
Compression can be enable on the writer :
|
||||
|
||||
```go
|
||||
w := kafka.NewWriter(kafka.WriterConfig{
|
||||
Brokers: []string{"localhost:9092"},
|
||||
Topic: "topic-A",
|
||||
CompressionCodec: snappy.NewCompressionCodec(),
|
||||
})
|
||||
```
|
||||
|
||||
The reader will by default figure out if the consumed messages are compressed by intepreting the message attributes.
|
||||
|
||||
## TLS Support
|
||||
|
||||
For a bare bones Conn type or in the Reader/Writer configs you can specify a dialer option for TLS support. If the TLS field is nil, it will not connect with TLS.
|
||||
|
||||
### Connection
|
||||
|
||||
```go
|
||||
dialer := &kafka.Dialer{
|
||||
Timeout: 10 * time.Second,
|
||||
DualStack: true,
|
||||
TLS: &tls.Config{...tls config...},
|
||||
}
|
||||
|
||||
conn, err := dialer.DialContext(ctx, "tcp", "localhost:9093")
|
||||
```
|
||||
|
||||
### Reader
|
||||
|
||||
```go
|
||||
dialer := &kafka.Dialer{
|
||||
Timeout: 10 * time.Second,
|
||||
DualStack: true,
|
||||
TLS: &tls.Config{...tls config...},
|
||||
}
|
||||
|
||||
r := kafka.NewReader(kafka.ReaderConfig{
|
||||
Brokers: []string{"localhost:9093"},
|
||||
GroupID: "consumer-group-id",
|
||||
Topic: "topic-A",
|
||||
Dialer: dialer,
|
||||
})
|
||||
```
|
||||
|
||||
### Writer
|
||||
|
||||
```go
|
||||
dialer := &kafka.Dialer{
|
||||
Timeout: 10 * time.Second,
|
||||
DualStack: true,
|
||||
TLS: &tls.Config{...tls config...},
|
||||
}
|
||||
|
||||
w := kafka.NewWriter(kafka.WriterConfig{
|
||||
Brokers: []string{"localhost:9093"},
|
||||
Topic: "topic-A",
|
||||
Balancer: &kafka.Hash{},
|
||||
Dialer: dialer,
|
||||
})
|
||||
```
|
||||
+160
@@ -0,0 +1,160 @@
|
||||
package kafka
|
||||
|
||||
import (
|
||||
"hash"
|
||||
"hash/fnv"
|
||||
"sort"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// The Balancer interface provides an abstraction of the message distribution
|
||||
// logic used by Writer instances to route messages to the partitions available
|
||||
// on a kafka cluster.
|
||||
//
|
||||
// Instances of Balancer do not have to be safe to use concurrently by multiple
|
||||
// goroutines, the Writer implementation ensures that calls to Balance are
|
||||
// synchronized.
|
||||
type Balancer interface {
|
||||
// Balance receives a message and a set of available partitions and
|
||||
// returns the partition number that the message should be routed to.
|
||||
//
|
||||
// An application should refrain from using a balancer to manage multiple
|
||||
// sets of partitions (from different topics for examples), use one balancer
|
||||
// instance for each partition set, so the balancer can detect when the
|
||||
// partitions change and assume that the kafka topic has been rebalanced.
|
||||
Balance(msg Message, partitions ...int) (partition int)
|
||||
}
|
||||
|
||||
// BalancerFunc is an implementation of the Balancer interface that makes it
|
||||
// possible to use regular functions to distribute messages across partitions.
|
||||
type BalancerFunc func(Message, ...int) int
|
||||
|
||||
// Balance calls f, satisfies the Balancer interface.
|
||||
func (f BalancerFunc) Balance(msg Message, partitions ...int) int {
|
||||
return f(msg, partitions...)
|
||||
}
|
||||
|
||||
// RoundRobin is an Balancer implementation that equally distributes messages
|
||||
// across all available partitions.
|
||||
type RoundRobin struct {
|
||||
offset uint64
|
||||
}
|
||||
|
||||
// Balance satisfies the Balancer interface.
|
||||
func (rr *RoundRobin) Balance(msg Message, partitions ...int) int {
|
||||
length := uint64(len(partitions))
|
||||
offset := rr.offset
|
||||
rr.offset++
|
||||
return partitions[offset%length]
|
||||
}
|
||||
|
||||
// LeastBytes is a Balancer implementation that routes messages to the partition
|
||||
// that has received the least amount of data.
|
||||
//
|
||||
// Note that no coordination is done between multiple producers, having good
|
||||
// balancing relies on the fact that each producer using a LeastBytes balancer
|
||||
// should produce well balanced messages.
|
||||
type LeastBytes struct {
|
||||
counters []leastBytesCounter
|
||||
}
|
||||
|
||||
type leastBytesCounter struct {
|
||||
partition int
|
||||
bytes uint64
|
||||
}
|
||||
|
||||
// Balance satisfies the Balancer interface.
|
||||
func (lb *LeastBytes) Balance(msg Message, partitions ...int) int {
|
||||
for _, p := range partitions {
|
||||
if c := lb.counterOf(p); c == nil {
|
||||
lb.counters = lb.makeCounters(partitions...)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
minBytes := lb.counters[0].bytes
|
||||
minIndex := 0
|
||||
|
||||
for i, c := range lb.counters[1:] {
|
||||
if c.bytes < minBytes {
|
||||
minIndex = i + 1
|
||||
minBytes = c.bytes
|
||||
}
|
||||
}
|
||||
|
||||
c := &lb.counters[minIndex]
|
||||
c.bytes += uint64(len(msg.Key)) + uint64(len(msg.Value))
|
||||
return c.partition
|
||||
}
|
||||
|
||||
func (lb *LeastBytes) counterOf(partition int) *leastBytesCounter {
|
||||
i := sort.Search(len(lb.counters), func(i int) bool {
|
||||
return lb.counters[i].partition >= partition
|
||||
})
|
||||
if i == len(lb.counters) || lb.counters[i].partition != partition {
|
||||
return nil
|
||||
}
|
||||
return &lb.counters[i]
|
||||
}
|
||||
|
||||
func (lb *LeastBytes) makeCounters(partitions ...int) (counters []leastBytesCounter) {
|
||||
counters = make([]leastBytesCounter, len(partitions))
|
||||
|
||||
for i, p := range partitions {
|
||||
counters[i].partition = p
|
||||
}
|
||||
|
||||
sort.Slice(counters, func(i int, j int) bool {
|
||||
return counters[i].partition < counters[j].partition
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
var (
|
||||
fnv1aPool = &sync.Pool{
|
||||
New: func() interface{} {
|
||||
return fnv.New32a()
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
// Hash is a Balancer that uses the provided hash function to determine which
|
||||
// partition to route messages to. This ensures that messages with the same key
|
||||
// are routed to the same partition.
|
||||
//
|
||||
// The logic to calculate the partition is:
|
||||
//
|
||||
// hasher.Sum32() % len(partitions) => partition
|
||||
//
|
||||
// By default, Hash uses the FNV-1a algorithm. This is the same algorithm used
|
||||
// by the Sarama Producer and ensures that messages produced by kafka-go will
|
||||
// be delivered to the same topics that the Sarama producer would be delivered to
|
||||
type Hash struct {
|
||||
rr RoundRobin
|
||||
Hasher hash.Hash32
|
||||
}
|
||||
|
||||
func (h *Hash) Balance(msg Message, partitions ...int) (partition int) {
|
||||
if msg.Key == nil {
|
||||
return h.rr.Balance(msg, partitions...)
|
||||
}
|
||||
|
||||
hasher := h.Hasher
|
||||
if hasher == nil {
|
||||
hasher = fnv1aPool.Get().(hash.Hash32)
|
||||
defer fnv1aPool.Put(hasher)
|
||||
}
|
||||
|
||||
hasher.Reset()
|
||||
if _, err := hasher.Write(msg.Key); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
// uses same algorithm that Sarama's hashPartitioner uses
|
||||
partition = int(hasher.Sum32()) % len(partitions)
|
||||
if partition < 0 {
|
||||
partition = -partition
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
+213
@@ -0,0 +1,213 @@
|
||||
package kafka
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"io"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// A Batch is an iterator over a sequence of messages fetched from a kafka
|
||||
// server.
|
||||
//
|
||||
// Batches are created by calling (*Conn).ReadBatch. They hold a internal lock
|
||||
// on the connection, which is released when the batch is closed. Failing to
|
||||
// call a batch's Close method will likely result in a dead-lock when trying to
|
||||
// use the connection.
|
||||
//
|
||||
// Batches are safe to use concurrently from multiple goroutines.
|
||||
type Batch struct {
|
||||
mutex sync.Mutex
|
||||
conn *Conn
|
||||
lock *sync.Mutex
|
||||
msgs *messageSetReader
|
||||
deadline time.Time
|
||||
throttle time.Duration
|
||||
topic string
|
||||
partition int
|
||||
offset int64
|
||||
highWaterMark int64
|
||||
err error
|
||||
}
|
||||
|
||||
// Throttle gives the throttling duration applied by the kafka server on the
|
||||
// connection.
|
||||
func (batch *Batch) Throttle() time.Duration {
|
||||
return batch.throttle
|
||||
}
|
||||
|
||||
// Watermark returns the current highest watermark in a partition.
|
||||
func (batch *Batch) HighWaterMark() int64 {
|
||||
return batch.highWaterMark
|
||||
}
|
||||
|
||||
// Offset returns the offset of the next message in the batch.
|
||||
func (batch *Batch) Offset() int64 {
|
||||
batch.mutex.Lock()
|
||||
offset := batch.offset
|
||||
batch.mutex.Unlock()
|
||||
return offset
|
||||
}
|
||||
|
||||
// Close closes the batch, releasing the connection lock and returning an error
|
||||
// if reading the batch failed for any reason.
|
||||
func (batch *Batch) Close() error {
|
||||
batch.mutex.Lock()
|
||||
err := batch.close()
|
||||
batch.mutex.Unlock()
|
||||
return err
|
||||
}
|
||||
|
||||
func (batch *Batch) close() (err error) {
|
||||
conn := batch.conn
|
||||
lock := batch.lock
|
||||
|
||||
batch.conn = nil
|
||||
batch.lock = nil
|
||||
if batch.msgs != nil {
|
||||
batch.msgs.discard()
|
||||
}
|
||||
|
||||
if err = batch.err; err == io.EOF {
|
||||
err = nil
|
||||
}
|
||||
|
||||
if conn != nil {
|
||||
conn.rdeadline.unsetConnReadDeadline()
|
||||
conn.mutex.Lock()
|
||||
conn.offset = batch.offset
|
||||
conn.mutex.Unlock()
|
||||
|
||||
if err != nil {
|
||||
if _, ok := err.(Error); !ok && err != io.ErrShortBuffer {
|
||||
conn.Close()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if lock != nil {
|
||||
lock.Unlock()
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// Read reads the value of the next message from the batch into b, returning the
|
||||
// number of bytes read, or an error if the next message couldn't be read.
|
||||
//
|
||||
// If an error is returned the batch cannot be used anymore and calling Read
|
||||
// again will keep returning that error. All errors except io.EOF (indicating
|
||||
// that the program consumed all messages from the batch) are also returned by
|
||||
// Close.
|
||||
//
|
||||
// The method fails with io.ErrShortBuffer if the buffer passed as argument is
|
||||
// too small to hold the message value.
|
||||
func (batch *Batch) Read(b []byte) (int, error) {
|
||||
n := 0
|
||||
|
||||
batch.mutex.Lock()
|
||||
offset := batch.offset
|
||||
|
||||
_, _, err := batch.readMessage(
|
||||
func(r *bufio.Reader, size int, nbytes int) (int, error) {
|
||||
if nbytes < 0 {
|
||||
return size, nil
|
||||
}
|
||||
return discardN(r, size, nbytes)
|
||||
},
|
||||
func(r *bufio.Reader, size int, nbytes int) (int, error) {
|
||||
if nbytes < 0 {
|
||||
return size, nil
|
||||
}
|
||||
n = nbytes // return value
|
||||
if nbytes > len(b) {
|
||||
nbytes = len(b)
|
||||
}
|
||||
nbytes, err := io.ReadFull(r, b[:nbytes])
|
||||
if err != nil {
|
||||
return size - nbytes, err
|
||||
}
|
||||
return discardN(r, size-nbytes, n-nbytes)
|
||||
},
|
||||
)
|
||||
|
||||
if err == nil && n > len(b) {
|
||||
n, err = len(b), io.ErrShortBuffer
|
||||
batch.err = io.ErrShortBuffer
|
||||
batch.offset = offset // rollback
|
||||
}
|
||||
|
||||
batch.mutex.Unlock()
|
||||
return n, err
|
||||
}
|
||||
|
||||
// ReadMessage reads and return the next message from the batch.
|
||||
//
|
||||
// Because this method allocate memory buffers for the message key and value
|
||||
// it is less memory-efficient than Read, but has the advantage of never
|
||||
// failing with io.ErrShortBuffer.
|
||||
func (batch *Batch) ReadMessage() (Message, error) {
|
||||
msg := Message{}
|
||||
batch.mutex.Lock()
|
||||
|
||||
offset, timestamp, err := batch.readMessage(
|
||||
func(r *bufio.Reader, size int, nbytes int) (remain int, err error) {
|
||||
msg.Key, remain, err = readNewBytes(r, size, nbytes)
|
||||
return
|
||||
},
|
||||
func(r *bufio.Reader, size int, nbytes int) (remain int, err error) {
|
||||
msg.Value, remain, err = readNewBytes(r, size, nbytes)
|
||||
return
|
||||
},
|
||||
)
|
||||
|
||||
batch.mutex.Unlock()
|
||||
msg.Topic = batch.topic
|
||||
msg.Partition = batch.partition
|
||||
msg.Offset = offset
|
||||
msg.Time = timestampToTime(timestamp)
|
||||
|
||||
return msg, err
|
||||
}
|
||||
|
||||
func (batch *Batch) readMessage(
|
||||
key func(*bufio.Reader, int, int) (int, error),
|
||||
val func(*bufio.Reader, int, int) (int, error),
|
||||
) (offset int64, timestamp int64, err error) {
|
||||
if err = batch.err; err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
offset, timestamp, err = batch.msgs.readMessage(batch.offset, key, val)
|
||||
switch err {
|
||||
case nil:
|
||||
batch.offset = offset + 1
|
||||
case errShortRead:
|
||||
// As an "optimization" kafka truncates the returned response after
|
||||
// producing MaxBytes, which could then cause the code to return
|
||||
// errShortRead.
|
||||
err = batch.msgs.discard()
|
||||
switch {
|
||||
case err != nil:
|
||||
batch.err = err
|
||||
case batch.msgs.remaining() == 0:
|
||||
// Because we use the adjusted deadline we could end up returning
|
||||
// before the actual deadline occurred. This is necessary otherwise
|
||||
// timing out the connection for real could end up leaving it in an
|
||||
// unpredictable state, which would require closing it.
|
||||
// This design decision was made to maximize the chances of keeping
|
||||
// the connection open, the trade off being to lose precision on the
|
||||
// read deadline management.
|
||||
if !batch.deadline.IsZero() && time.Now().After(batch.deadline) {
|
||||
err = RequestTimedOut
|
||||
} else {
|
||||
err = io.EOF
|
||||
}
|
||||
batch.err = err
|
||||
}
|
||||
default:
|
||||
batch.err = err
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
package kafka
|
||||
|
||||
// A commit represents the instruction of publishing an update of the last
|
||||
// offset read by a program for a topic and partition.
|
||||
type commit struct {
|
||||
topic string
|
||||
partition int
|
||||
offset int64
|
||||
}
|
||||
|
||||
// makeCommit builds a commit value from a message, the resulting commit takes
|
||||
// its topic, partition, and offset from the message.
|
||||
func makeCommit(msg Message) commit {
|
||||
return commit{
|
||||
topic: msg.Topic,
|
||||
partition: msg.Partition,
|
||||
offset: msg.Offset + 1,
|
||||
}
|
||||
}
|
||||
|
||||
// makeCommits generates a slice of commits from a list of messages, it extracts
|
||||
// the topic, partition, and offset of each message and builds the corresponding
|
||||
// commit slice.
|
||||
func makeCommits(msgs ...Message) []commit {
|
||||
commits := make([]commit, len(msgs))
|
||||
|
||||
for i, m := range msgs {
|
||||
commits[i] = makeCommit(m)
|
||||
}
|
||||
|
||||
return commits
|
||||
}
|
||||
|
||||
// commitRequest is the data type exchanged between the CommitMessages method
|
||||
// and internals of the reader's implementation.
|
||||
type commitRequest struct {
|
||||
commits []commit
|
||||
errch chan<- error
|
||||
}
|
||||
+52
@@ -0,0 +1,52 @@
|
||||
package kafka
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"sync"
|
||||
)
|
||||
|
||||
var errUnknownCodec = errors.New("invalid codec")
|
||||
|
||||
var codecs = make(map[int8]CompressionCodec)
|
||||
var codecsMutex sync.RWMutex
|
||||
|
||||
// RegisterCompressionCodec registers a compression codec so it can be used by a Writer.
|
||||
func RegisterCompressionCodec(codec func() CompressionCodec) {
|
||||
c := codec()
|
||||
codecsMutex.Lock()
|
||||
codecs[c.Code()] = c
|
||||
codecsMutex.Unlock()
|
||||
}
|
||||
|
||||
// resolveCodec looks up a codec by Code()
|
||||
func resolveCodec(code int8) (codec CompressionCodec, err error) {
|
||||
codecsMutex.RLock()
|
||||
codec = codecs[code]
|
||||
codecsMutex.RUnlock()
|
||||
|
||||
if codec == nil {
|
||||
err = errUnknownCodec
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// CompressionCodec represents a compression codec to encode and decode
|
||||
// the messages.
|
||||
// See : https://cwiki.apache.org/confluence/display/KAFKA/Compression
|
||||
//
|
||||
// A CompressionCodec must be safe for concurrent access by multiple go
|
||||
// routines.
|
||||
type CompressionCodec interface {
|
||||
// Code returns the compression codec code
|
||||
Code() int8
|
||||
|
||||
// Encode encodes the src data
|
||||
Encode(src []byte) ([]byte, error)
|
||||
|
||||
// Decode decodes the src data
|
||||
Decode(src []byte) ([]byte, error)
|
||||
}
|
||||
|
||||
const compressionCodecMask int8 = 0x03
|
||||
const DefaultCompressionLevel int = -1
|
||||
const CompressionNoneCode = 0
|
||||
+1074
File diff suppressed because it is too large
Load Diff
+80
@@ -0,0 +1,80 @@
|
||||
package kafka
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"hash/crc32"
|
||||
"sync"
|
||||
)
|
||||
|
||||
func crc32OfMessage(magicByte int8, attributes int8, timestamp int64, key []byte, value []byte) uint32 {
|
||||
b := acquireCrc32Buffer()
|
||||
b.writeInt8(magicByte)
|
||||
b.writeInt8(attributes)
|
||||
if magicByte != 0 {
|
||||
b.writeInt64(timestamp)
|
||||
}
|
||||
b.writeBytes(key)
|
||||
b.writeBytes(value)
|
||||
sum := b.sum
|
||||
releaseCrc32Buffer(b)
|
||||
return sum
|
||||
}
|
||||
|
||||
type crc32Buffer struct {
|
||||
sum uint32
|
||||
buf bytes.Buffer
|
||||
}
|
||||
|
||||
func (c *crc32Buffer) writeInt8(i int8) {
|
||||
c.buf.Truncate(0)
|
||||
c.buf.WriteByte(byte(i))
|
||||
c.update()
|
||||
}
|
||||
|
||||
func (c *crc32Buffer) writeInt32(i int32) {
|
||||
a := [4]byte{}
|
||||
binary.BigEndian.PutUint32(a[:], uint32(i))
|
||||
c.buf.Truncate(0)
|
||||
c.buf.Write(a[:])
|
||||
c.update()
|
||||
}
|
||||
|
||||
func (c *crc32Buffer) writeInt64(i int64) {
|
||||
a := [8]byte{}
|
||||
binary.BigEndian.PutUint64(a[:], uint64(i))
|
||||
c.buf.Truncate(0)
|
||||
c.buf.Write(a[:])
|
||||
c.update()
|
||||
}
|
||||
|
||||
func (c *crc32Buffer) writeBytes(b []byte) {
|
||||
if b == nil {
|
||||
c.writeInt32(-1)
|
||||
} else {
|
||||
c.writeInt32(int32(len(b)))
|
||||
}
|
||||
c.sum = crc32Update(c.sum, b)
|
||||
}
|
||||
|
||||
func (c *crc32Buffer) update() {
|
||||
c.sum = crc32Update(c.sum, c.buf.Bytes())
|
||||
}
|
||||
|
||||
func crc32Update(sum uint32, b []byte) uint32 {
|
||||
return crc32.Update(sum, crc32.IEEETable, b)
|
||||
}
|
||||
|
||||
var crc32BufferPool = sync.Pool{
|
||||
New: func() interface{} { return &crc32Buffer{} },
|
||||
}
|
||||
|
||||
func acquireCrc32Buffer() *crc32Buffer {
|
||||
c := crc32BufferPool.Get().(*crc32Buffer)
|
||||
c.sum = 0
|
||||
return c
|
||||
}
|
||||
|
||||
func releaseCrc32Buffer(b *crc32Buffer) {
|
||||
crc32BufferPool.Put(b)
|
||||
}
|
||||
+267
@@ -0,0 +1,267 @@
|
||||
package kafka
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"time"
|
||||
)
|
||||
|
||||
type ConfigEntry struct {
|
||||
ConfigName string
|
||||
ConfigValue string
|
||||
}
|
||||
|
||||
func (c ConfigEntry) toCreateTopicsRequestV0ConfigEntry() createTopicsRequestV0ConfigEntry {
|
||||
return createTopicsRequestV0ConfigEntry{
|
||||
ConfigName: c.ConfigName,
|
||||
ConfigValue: c.ConfigValue,
|
||||
}
|
||||
}
|
||||
|
||||
type createTopicsRequestV0ConfigEntry struct {
|
||||
ConfigName string
|
||||
ConfigValue string
|
||||
}
|
||||
|
||||
func (t createTopicsRequestV0ConfigEntry) size() int32 {
|
||||
return sizeofString(t.ConfigName) +
|
||||
sizeofString(t.ConfigValue)
|
||||
}
|
||||
|
||||
func (t createTopicsRequestV0ConfigEntry) writeTo(w *bufio.Writer) {
|
||||
writeString(w, t.ConfigName)
|
||||
writeString(w, t.ConfigValue)
|
||||
}
|
||||
|
||||
type ReplicaAssignment struct {
|
||||
Partition int
|
||||
Replicas int
|
||||
}
|
||||
|
||||
func (a ReplicaAssignment) toCreateTopicsRequestV0ReplicaAssignment() createTopicsRequestV0ReplicaAssignment {
|
||||
return createTopicsRequestV0ReplicaAssignment{
|
||||
Partition: int32(a.Partition),
|
||||
Replicas: int32(a.Replicas),
|
||||
}
|
||||
}
|
||||
|
||||
type createTopicsRequestV0ReplicaAssignment struct {
|
||||
Partition int32
|
||||
Replicas int32
|
||||
}
|
||||
|
||||
func (t createTopicsRequestV0ReplicaAssignment) size() int32 {
|
||||
return sizeofInt32(t.Partition) +
|
||||
sizeofInt32(t.Replicas)
|
||||
}
|
||||
|
||||
func (t createTopicsRequestV0ReplicaAssignment) writeTo(w *bufio.Writer) {
|
||||
writeInt32(w, t.Partition)
|
||||
writeInt32(w, t.Replicas)
|
||||
}
|
||||
|
||||
type TopicConfig struct {
|
||||
// Topic name
|
||||
Topic string
|
||||
|
||||
// NumPartitions created. -1 indicates unset.
|
||||
NumPartitions int
|
||||
|
||||
// ReplicationFactor for the topic. -1 indicates unset.
|
||||
ReplicationFactor int
|
||||
|
||||
// ReplicaAssignments among kafka brokers for this topic partitions. If this
|
||||
// is set num_partitions and replication_factor must be unset.
|
||||
ReplicaAssignments []ReplicaAssignment
|
||||
|
||||
// ConfigEntries holds topic level configuration for topic to be set.
|
||||
ConfigEntries []ConfigEntry
|
||||
}
|
||||
|
||||
func (t TopicConfig) toCreateTopicsRequestV0Topic() createTopicsRequestV0Topic {
|
||||
var requestV0ReplicaAssignments []createTopicsRequestV0ReplicaAssignment
|
||||
for _, a := range t.ReplicaAssignments {
|
||||
requestV0ReplicaAssignments = append(
|
||||
requestV0ReplicaAssignments,
|
||||
a.toCreateTopicsRequestV0ReplicaAssignment())
|
||||
}
|
||||
var requestV0ConfigEntries []createTopicsRequestV0ConfigEntry
|
||||
for _, c := range t.ConfigEntries {
|
||||
requestV0ConfigEntries = append(
|
||||
requestV0ConfigEntries,
|
||||
c.toCreateTopicsRequestV0ConfigEntry())
|
||||
}
|
||||
|
||||
return createTopicsRequestV0Topic{
|
||||
Topic: t.Topic,
|
||||
NumPartitions: int32(t.NumPartitions),
|
||||
ReplicationFactor: int16(t.ReplicationFactor),
|
||||
ReplicaAssignments: requestV0ReplicaAssignments,
|
||||
ConfigEntries: requestV0ConfigEntries,
|
||||
}
|
||||
}
|
||||
|
||||
type createTopicsRequestV0Topic struct {
|
||||
// Topic name
|
||||
Topic string
|
||||
|
||||
// NumPartitions created. -1 indicates unset.
|
||||
NumPartitions int32
|
||||
|
||||
// ReplicationFactor for the topic. -1 indicates unset.
|
||||
ReplicationFactor int16
|
||||
|
||||
// ReplicaAssignments among kafka brokers for this topic partitions. If this
|
||||
// is set num_partitions and replication_factor must be unset.
|
||||
ReplicaAssignments []createTopicsRequestV0ReplicaAssignment
|
||||
|
||||
// ConfigEntries holds topic level configuration for topic to be set.
|
||||
ConfigEntries []createTopicsRequestV0ConfigEntry
|
||||
}
|
||||
|
||||
func (t createTopicsRequestV0Topic) size() int32 {
|
||||
return sizeofString(t.Topic) +
|
||||
sizeofInt32(t.NumPartitions) +
|
||||
sizeofInt16(t.ReplicationFactor) +
|
||||
sizeofArray(len(t.ReplicaAssignments), func(i int) int32 { return t.ReplicaAssignments[i].size() }) +
|
||||
sizeofArray(len(t.ConfigEntries), func(i int) int32 { return t.ConfigEntries[i].size() })
|
||||
}
|
||||
|
||||
func (t createTopicsRequestV0Topic) writeTo(w *bufio.Writer) {
|
||||
writeString(w, t.Topic)
|
||||
writeInt32(w, t.NumPartitions)
|
||||
writeInt16(w, t.ReplicationFactor)
|
||||
writeArray(w, len(t.ReplicaAssignments), func(i int) { t.ReplicaAssignments[i].writeTo(w) })
|
||||
writeArray(w, len(t.ConfigEntries), func(i int) { t.ConfigEntries[i].writeTo(w) })
|
||||
}
|
||||
|
||||
// See http://kafka.apache.org/protocol.html#The_Messages_CreateTopics
|
||||
type createTopicsRequestV0 struct {
|
||||
// Topics contains n array of single topic creation requests. Can not
|
||||
// have multiple entries for the same topic.
|
||||
Topics []createTopicsRequestV0Topic
|
||||
|
||||
// Timeout ms to wait for a topic to be completely created on the
|
||||
// controller node. Values <= 0 will trigger topic creation and return immediately
|
||||
Timeout int32
|
||||
}
|
||||
|
||||
func (t createTopicsRequestV0) size() int32 {
|
||||
return sizeofArray(len(t.Topics), func(i int) int32 { return t.Topics[i].size() }) +
|
||||
sizeofInt32(t.Timeout)
|
||||
}
|
||||
|
||||
func (t createTopicsRequestV0) writeTo(w *bufio.Writer) {
|
||||
writeArray(w, len(t.Topics), func(i int) { t.Topics[i].writeTo(w) })
|
||||
writeInt32(w, t.Timeout)
|
||||
}
|
||||
|
||||
type createTopicsResponseV0TopicError struct {
|
||||
// Topic name
|
||||
Topic string
|
||||
|
||||
// ErrorCode holds response error code
|
||||
ErrorCode int16
|
||||
}
|
||||
|
||||
func (t createTopicsResponseV0TopicError) size() int32 {
|
||||
return sizeofString(t.Topic) +
|
||||
sizeofInt16(t.ErrorCode)
|
||||
}
|
||||
|
||||
func (t createTopicsResponseV0TopicError) writeTo(w *bufio.Writer) {
|
||||
writeString(w, t.Topic)
|
||||
writeInt16(w, t.ErrorCode)
|
||||
}
|
||||
|
||||
func (t *createTopicsResponseV0TopicError) readFrom(r *bufio.Reader, size int) (remain int, err error) {
|
||||
if remain, err = readString(r, size, &t.Topic); err != nil {
|
||||
return
|
||||
}
|
||||
if remain, err = readInt16(r, remain, &t.ErrorCode); err != nil {
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// See http://kafka.apache.org/protocol.html#The_Messages_CreateTopics
|
||||
type createTopicsResponseV0 struct {
|
||||
TopicErrors []createTopicsResponseV0TopicError
|
||||
}
|
||||
|
||||
func (t createTopicsResponseV0) size() int32 {
|
||||
return sizeofArray(len(t.TopicErrors), func(i int) int32 { return t.TopicErrors[i].size() })
|
||||
}
|
||||
|
||||
func (t createTopicsResponseV0) writeTo(w *bufio.Writer) {
|
||||
writeArray(w, len(t.TopicErrors), func(i int) { t.TopicErrors[i].writeTo(w) })
|
||||
}
|
||||
|
||||
func (t *createTopicsResponseV0) readFrom(r *bufio.Reader, size int) (remain int, err error) {
|
||||
fn := func(r *bufio.Reader, size int) (fnRemain int, fnErr error) {
|
||||
var topic createTopicsResponseV0TopicError
|
||||
if fnRemain, fnErr = (&topic).readFrom(r, size); err != nil {
|
||||
return
|
||||
}
|
||||
t.TopicErrors = append(t.TopicErrors, topic)
|
||||
return
|
||||
}
|
||||
if remain, err = readArrayWith(r, size, fn); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (c *Conn) createTopics(request createTopicsRequestV0) (createTopicsResponseV0, error) {
|
||||
var response createTopicsResponseV0
|
||||
|
||||
err := c.writeOperation(
|
||||
func(deadline time.Time, id int32) error {
|
||||
if request.Timeout == 0 {
|
||||
now := time.Now()
|
||||
deadline = adjustDeadlineForRTT(deadline, now, defaultRTT)
|
||||
request.Timeout = milliseconds(deadlineToTimeout(deadline, now))
|
||||
}
|
||||
return c.writeRequest(createTopicsRequest, v0, id, request)
|
||||
},
|
||||
func(deadline time.Time, size int) error {
|
||||
return expectZeroSize(func() (remain int, err error) {
|
||||
return (&response).readFrom(&c.rbuf, size)
|
||||
}())
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
return response, err
|
||||
}
|
||||
for _, tr := range response.TopicErrors {
|
||||
if tr.ErrorCode != 0 {
|
||||
return response, Error(tr.ErrorCode)
|
||||
}
|
||||
}
|
||||
|
||||
return response, nil
|
||||
}
|
||||
|
||||
// CreateTopics creates one topic per provided configuration with idempotent
|
||||
// operational semantics. In other words, if CreateTopics is invoked with a
|
||||
// configuration for an existing topic, it will have no effect.
|
||||
func (c *Conn) CreateTopics(topics ...TopicConfig) error {
|
||||
var requestV0Topics []createTopicsRequestV0Topic
|
||||
for _, t := range topics {
|
||||
requestV0Topics = append(
|
||||
requestV0Topics,
|
||||
t.toCreateTopicsRequestV0Topic())
|
||||
}
|
||||
|
||||
_, err := c.createTopics(createTopicsRequestV0{
|
||||
Topics: requestV0Topics,
|
||||
})
|
||||
|
||||
switch err {
|
||||
case TopicAlreadyExists:
|
||||
// ok
|
||||
return nil
|
||||
default:
|
||||
return err
|
||||
}
|
||||
}
|
||||
+124
@@ -0,0 +1,124 @@
|
||||
package kafka
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"time"
|
||||
)
|
||||
|
||||
// See http://kafka.apache.org/protocol.html#The_Messages_DeleteTopics
|
||||
type deleteTopicsRequestV1 struct {
|
||||
// Topics holds the topic names
|
||||
Topics []string
|
||||
|
||||
// Timeout holds the time in ms to wait for a topic to be completely deleted
|
||||
// on the controller node. Values <= 0 will trigger topic deletion and return
|
||||
// immediately.
|
||||
Timeout int32
|
||||
}
|
||||
|
||||
func (t deleteTopicsRequestV1) size() int32 {
|
||||
return sizeofStringArray(t.Topics) +
|
||||
sizeofInt32(t.Timeout)
|
||||
}
|
||||
|
||||
func (t deleteTopicsRequestV1) writeTo(w *bufio.Writer) {
|
||||
writeStringArray(w, t.Topics)
|
||||
writeInt32(w, t.Timeout)
|
||||
}
|
||||
|
||||
type deleteTopicsResponseV1 struct {
|
||||
// ThrottleTimeMS holds the duration in milliseconds for which the request
|
||||
// was throttled due to quota violation (Zero if the request did not violate
|
||||
// any quota)
|
||||
ThrottleTimeMS int32
|
||||
|
||||
// TopicErrorCodes holds per topic error codes
|
||||
TopicErrorCodes []deleteTopicsResponseV1TopicErrorCode
|
||||
}
|
||||
|
||||
func (t deleteTopicsResponseV1) size() int32 {
|
||||
return sizeofInt32(t.ThrottleTimeMS) +
|
||||
sizeofArray(len(t.TopicErrorCodes), func(i int) int32 { return t.TopicErrorCodes[i].size() })
|
||||
}
|
||||
|
||||
func (t *deleteTopicsResponseV1) readFrom(r *bufio.Reader, size int) (remain int, err error) {
|
||||
if remain, err = readInt32(r, size, &t.ThrottleTimeMS); err != nil {
|
||||
return
|
||||
}
|
||||
fn := func(withReader *bufio.Reader, withSize int) (fnRemain int, fnErr error) {
|
||||
var item deleteTopicsResponseV1TopicErrorCode
|
||||
if fnRemain, fnErr = (&item).readFrom(withReader, withSize); err != nil {
|
||||
return
|
||||
}
|
||||
t.TopicErrorCodes = append(t.TopicErrorCodes, item)
|
||||
return
|
||||
}
|
||||
if remain, err = readArrayWith(r, remain, fn); err != nil {
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (t deleteTopicsResponseV1) writeTo(w *bufio.Writer) {
|
||||
writeInt32(w, t.ThrottleTimeMS)
|
||||
writeArray(w, len(t.TopicErrorCodes), func(i int) { t.TopicErrorCodes[i].writeTo(w) })
|
||||
}
|
||||
|
||||
type deleteTopicsResponseV1TopicErrorCode struct {
|
||||
// Topic holds the topic name
|
||||
Topic string
|
||||
|
||||
// ErrorCode holds the error code
|
||||
ErrorCode int16
|
||||
}
|
||||
|
||||
func (t deleteTopicsResponseV1TopicErrorCode) size() int32 {
|
||||
return sizeofString(t.Topic) +
|
||||
sizeofInt16(t.ErrorCode)
|
||||
}
|
||||
|
||||
func (t *deleteTopicsResponseV1TopicErrorCode) readFrom(r *bufio.Reader, size int) (remain int, err error) {
|
||||
if remain, err = readString(r, size, &t.Topic); err != nil {
|
||||
return
|
||||
}
|
||||
if remain, err = readInt16(r, remain, &t.ErrorCode); err != nil {
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (t deleteTopicsResponseV1TopicErrorCode) writeTo(w *bufio.Writer) {
|
||||
writeString(w, t.Topic)
|
||||
writeInt16(w, t.ErrorCode)
|
||||
}
|
||||
|
||||
// deleteTopics deletes the specified topics.
|
||||
//
|
||||
// See http://kafka.apache.org/protocol.html#The_Messages_DeleteTopics
|
||||
func (c *Conn) deleteTopics(request deleteTopicsRequestV1) (deleteTopicsResponseV1, error) {
|
||||
var response deleteTopicsResponseV1
|
||||
err := c.writeOperation(
|
||||
func(deadline time.Time, id int32) error {
|
||||
if request.Timeout == 0 {
|
||||
now := time.Now()
|
||||
deadline = adjustDeadlineForRTT(deadline, now, defaultRTT)
|
||||
request.Timeout = milliseconds(deadlineToTimeout(deadline, now))
|
||||
}
|
||||
return c.writeRequest(deleteTopicsRequest, v1, id, request)
|
||||
},
|
||||
func(deadline time.Time, size int) error {
|
||||
return expectZeroSize(func() (remain int, err error) {
|
||||
return (&response).readFrom(&c.rbuf, size)
|
||||
}())
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
return deleteTopicsResponseV1{}, err
|
||||
}
|
||||
for _, c := range response.TopicErrorCodes {
|
||||
if c.ErrorCode != 0 {
|
||||
return response, Error(c.ErrorCode)
|
||||
}
|
||||
}
|
||||
return response, nil
|
||||
}
|
||||
+186
@@ -0,0 +1,186 @@
|
||||
package kafka
|
||||
|
||||
import "bufio"
|
||||
|
||||
// See http://kafka.apache.org/protocol.html#The_Messages_DescribeGroups
|
||||
type describeGroupsRequestV1 struct {
|
||||
// List of groupIds to request metadata for (an empty groupId array
|
||||
// will return empty group metadata).
|
||||
GroupIDs []string
|
||||
}
|
||||
|
||||
func (t describeGroupsRequestV1) size() int32 {
|
||||
return sizeofStringArray(t.GroupIDs)
|
||||
}
|
||||
|
||||
func (t describeGroupsRequestV1) writeTo(w *bufio.Writer) {
|
||||
writeStringArray(w, t.GroupIDs)
|
||||
}
|
||||
|
||||
type describeGroupsResponseMemberV1 struct {
|
||||
// MemberID assigned by the group coordinator
|
||||
MemberID string
|
||||
|
||||
// ClientID used in the member's latest join group request
|
||||
ClientID string
|
||||
|
||||
// ClientHost used in the request session corresponding to the member's
|
||||
// join group.
|
||||
ClientHost string
|
||||
|
||||
// MemberMetadata the metadata corresponding to the current group protocol
|
||||
// in use (will only be present if the group is stable).
|
||||
MemberMetadata []byte
|
||||
|
||||
// MemberAssignments provided by the group leader (will only be present if
|
||||
// the group is stable).
|
||||
//
|
||||
// See consumer groups section of https://cwiki.apache.org/confluence/display/KAFKA/A+Guide+To+The+Kafka+Protocol
|
||||
MemberAssignments []byte
|
||||
}
|
||||
|
||||
func (t describeGroupsResponseMemberV1) size() int32 {
|
||||
return sizeofString(t.MemberID) +
|
||||
sizeofString(t.ClientID) +
|
||||
sizeofString(t.ClientHost) +
|
||||
sizeofBytes(t.MemberMetadata) +
|
||||
sizeofBytes(t.MemberAssignments)
|
||||
}
|
||||
|
||||
func (t describeGroupsResponseMemberV1) writeTo(w *bufio.Writer) {
|
||||
writeString(w, t.MemberID)
|
||||
writeString(w, t.ClientID)
|
||||
writeString(w, t.ClientHost)
|
||||
writeBytes(w, t.MemberMetadata)
|
||||
writeBytes(w, t.MemberAssignments)
|
||||
}
|
||||
|
||||
func (t *describeGroupsResponseMemberV1) readFrom(r *bufio.Reader, size int) (remain int, err error) {
|
||||
if remain, err = readString(r, size, &t.MemberID); err != nil {
|
||||
return
|
||||
}
|
||||
if remain, err = readString(r, remain, &t.ClientID); err != nil {
|
||||
return
|
||||
}
|
||||
if remain, err = readString(r, remain, &t.ClientHost); err != nil {
|
||||
return
|
||||
}
|
||||
if remain, err = readBytes(r, remain, &t.MemberMetadata); err != nil {
|
||||
return
|
||||
}
|
||||
if remain, err = readBytes(r, remain, &t.MemberAssignments); err != nil {
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
type describeGroupsResponseGroupV1 struct {
|
||||
// ErrorCode holds response error code
|
||||
ErrorCode int16
|
||||
|
||||
// GroupID holds the unique group identifier
|
||||
GroupID string
|
||||
|
||||
// State holds current state of the group (one of: Dead, Stable, AwaitingSync,
|
||||
// PreparingRebalance, or empty if there is no active group)
|
||||
State string
|
||||
|
||||
// ProtocolType holds the current group protocol type (will be empty if there is
|
||||
// no active group)
|
||||
ProtocolType string
|
||||
|
||||
// Protocol holds the current group protocol (only provided if the group is Stable)
|
||||
Protocol string
|
||||
|
||||
// Members contains the current group members (only provided if the group is not Dead)
|
||||
Members []describeGroupsResponseMemberV1
|
||||
}
|
||||
|
||||
func (t describeGroupsResponseGroupV1) size() int32 {
|
||||
return sizeofInt16(t.ErrorCode) +
|
||||
sizeofString(t.GroupID) +
|
||||
sizeofString(t.State) +
|
||||
sizeofString(t.ProtocolType) +
|
||||
sizeofString(t.Protocol) +
|
||||
sizeofArray(len(t.Members), func(i int) int32 { return t.Members[i].size() })
|
||||
}
|
||||
|
||||
func (t describeGroupsResponseGroupV1) writeTo(w *bufio.Writer) {
|
||||
writeInt16(w, t.ErrorCode)
|
||||
writeString(w, t.GroupID)
|
||||
writeString(w, t.State)
|
||||
writeString(w, t.ProtocolType)
|
||||
writeString(w, t.Protocol)
|
||||
writeArray(w, len(t.Members), func(i int) { t.Members[i].writeTo(w) })
|
||||
}
|
||||
|
||||
func (t *describeGroupsResponseGroupV1) readFrom(r *bufio.Reader, size int) (remain int, err error) {
|
||||
if remain, err = readInt16(r, size, &t.ErrorCode); err != nil {
|
||||
return
|
||||
}
|
||||
if remain, err = readString(r, remain, &t.GroupID); err != nil {
|
||||
return
|
||||
}
|
||||
if remain, err = readString(r, remain, &t.State); err != nil {
|
||||
return
|
||||
}
|
||||
if remain, err = readString(r, remain, &t.ProtocolType); err != nil {
|
||||
return
|
||||
}
|
||||
if remain, err = readString(r, remain, &t.Protocol); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
fn := func(r *bufio.Reader, size int) (fnRemain int, fnErr error) {
|
||||
item := describeGroupsResponseMemberV1{}
|
||||
if fnRemain, fnErr = (&item).readFrom(r, size); err != nil {
|
||||
return
|
||||
}
|
||||
t.Members = append(t.Members, item)
|
||||
return
|
||||
}
|
||||
if remain, err = readArrayWith(r, remain, fn); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
type describeGroupsResponseV1 struct {
|
||||
// Duration in milliseconds for which the request was throttled due
|
||||
// to quota violation (Zero if the request did not violate any quota)
|
||||
ThrottleTimeMS int32
|
||||
|
||||
// Groups holds selected group information
|
||||
Groups []describeGroupsResponseGroupV1
|
||||
}
|
||||
|
||||
func (t describeGroupsResponseV1) size() int32 {
|
||||
return sizeofInt32(t.ThrottleTimeMS) +
|
||||
sizeofArray(len(t.Groups), func(i int) int32 { return t.Groups[i].size() })
|
||||
}
|
||||
|
||||
func (t describeGroupsResponseV1) writeTo(w *bufio.Writer) {
|
||||
writeInt32(w, t.ThrottleTimeMS)
|
||||
writeArray(w, len(t.Groups), func(i int) { t.Groups[i].writeTo(w) })
|
||||
}
|
||||
|
||||
func (t *describeGroupsResponseV1) readFrom(r *bufio.Reader, size int) (remain int, err error) {
|
||||
if remain, err = readInt32(r, size, &t.ThrottleTimeMS); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
fn := func(r *bufio.Reader, size int) (fnRemain int, fnErr error) {
|
||||
item := describeGroupsResponseGroupV1{}
|
||||
if fnRemain, fnErr = (&item).readFrom(r, size); fnErr != nil {
|
||||
return
|
||||
}
|
||||
t.Groups = append(t.Groups, item)
|
||||
return
|
||||
}
|
||||
if remain, err = readArrayWith(r, remain, fn); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
+365
@@ -0,0 +1,365 @@
|
||||
package kafka
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"net"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// The Dialer type mirrors the net.Dialer API but is designed to open kafka
|
||||
// connections instead of raw network connections.
|
||||
type Dialer struct {
|
||||
// Unique identifier for client connections established by this Dialer.
|
||||
ClientID string
|
||||
|
||||
// Timeout is the maximum amount of time a dial will wait for a connect to
|
||||
// complete. If Deadline is also set, it may fail earlier.
|
||||
//
|
||||
// The default is no timeout.
|
||||
//
|
||||
// When dialing a name with multiple IP addresses, the timeout may be
|
||||
// divided between them.
|
||||
//
|
||||
// With or without a timeout, the operating system may impose its own
|
||||
// earlier timeout. For instance, TCP timeouts are often around 3 minutes.
|
||||
Timeout time.Duration
|
||||
|
||||
// Deadline is the absolute point in time after which dials will fail.
|
||||
// If Timeout is set, it may fail earlier.
|
||||
// Zero means no deadline, or dependent on the operating system as with the
|
||||
// Timeout option.
|
||||
Deadline time.Time
|
||||
|
||||
// LocalAddr is the local address to use when dialing an address.
|
||||
// The address must be of a compatible type for the network being dialed.
|
||||
// If nil, a local address is automatically chosen.
|
||||
LocalAddr net.Addr
|
||||
|
||||
// DualStack enables RFC 6555-compliant "Happy Eyeballs" dialing when the
|
||||
// network is "tcp" and the destination is a host name with both IPv4 and
|
||||
// IPv6 addresses. This allows a client to tolerate networks where one
|
||||
// address family is silently broken.
|
||||
DualStack bool
|
||||
|
||||
// FallbackDelay specifies the length of time to wait before spawning a
|
||||
// fallback connection, when DualStack is enabled.
|
||||
// If zero, a default delay of 300ms is used.
|
||||
FallbackDelay time.Duration
|
||||
|
||||
// KeepAlive specifies the keep-alive period for an active network
|
||||
// connection.
|
||||
// If zero, keep-alives are not enabled. Network protocols that do not
|
||||
// support keep-alives ignore this field.
|
||||
KeepAlive time.Duration
|
||||
|
||||
// Resolver optionally specifies an alternate resolver to use.
|
||||
Resolver Resolver
|
||||
|
||||
// TLS enables Dialer to open secure connections. If nil, standard net.Conn
|
||||
// will be used.
|
||||
TLS *tls.Config
|
||||
}
|
||||
|
||||
// Dial connects to the address on the named network.
|
||||
func (d *Dialer) Dial(network string, address string) (*Conn, error) {
|
||||
return d.DialContext(context.Background(), network, address)
|
||||
}
|
||||
|
||||
// DialContext connects to the address on the named network using the provided
|
||||
// context.
|
||||
//
|
||||
// The provided Context must be non-nil. If the context expires before the
|
||||
// connection is complete, an error is returned. Once successfully connected,
|
||||
// any expiration of the context will not affect the connection.
|
||||
//
|
||||
// When using TCP, and the host in the address parameter resolves to multiple
|
||||
// network addresses, any dial timeout (from d.Timeout or ctx) is spread over
|
||||
// each consecutive dial, such that each is given an appropriate fraction of the
|
||||
// time to connect. For example, if a host has 4 IP addresses and the timeout is
|
||||
// 1 minute, the connect to each single address will be given 15 seconds to
|
||||
// complete before trying the next one.
|
||||
func (d *Dialer) DialContext(ctx context.Context, network string, address string) (*Conn, error) {
|
||||
if d.Timeout != 0 {
|
||||
var cancel context.CancelFunc
|
||||
ctx, cancel = context.WithTimeout(ctx, d.Timeout)
|
||||
defer cancel()
|
||||
}
|
||||
|
||||
if !d.Deadline.IsZero() {
|
||||
var cancel context.CancelFunc
|
||||
ctx, cancel = context.WithDeadline(ctx, d.Deadline)
|
||||
defer cancel()
|
||||
}
|
||||
|
||||
c, err := d.dialContext(ctx, network, address)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return NewConnWith(c, ConnConfig{ClientID: d.ClientID}), nil
|
||||
}
|
||||
|
||||
// DialLeader opens a connection to the leader of the partition for a given
|
||||
// topic.
|
||||
//
|
||||
// The address given to the DialContext method may not be the one that the
|
||||
// connection will end up being established to, because the dialer will lookup
|
||||
// the partition leader for the topic and return a connection to that server.
|
||||
// The original address is only used as a mechanism to discover the
|
||||
// configuration of the kafka cluster that we're connecting to.
|
||||
func (d *Dialer) DialLeader(ctx context.Context, network string, address string, topic string, partition int) (*Conn, error) {
|
||||
p, err := d.LookupPartition(ctx, network, address, topic, partition)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return d.DialPartition(ctx, network, address, p)
|
||||
}
|
||||
|
||||
// DialPartition opens a connection to the leader of the partition specified by partition
|
||||
// descriptor. It's strongly advised to use descriptor of the partition that comes out of
|
||||
// functions LookupPartition or LookupPartitions.
|
||||
func (d *Dialer) DialPartition(ctx context.Context, network string, address string, partition Partition) (*Conn, error) {
|
||||
c, err := d.dialContext(ctx, network, net.JoinHostPort(partition.Leader.Host, strconv.Itoa(partition.Leader.Port)))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return NewConnWith(c, ConnConfig{
|
||||
ClientID: d.ClientID,
|
||||
Topic: partition.Topic,
|
||||
Partition: partition.ID,
|
||||
}), nil
|
||||
}
|
||||
|
||||
// LookupLeader searches for the kafka broker that is the leader of the
|
||||
// partition for a given topic, returning a Broker value representing it.
|
||||
func (d *Dialer) LookupLeader(ctx context.Context, network string, address string, topic string, partition int) (Broker, error) {
|
||||
p, err := d.LookupPartition(ctx, network, address, topic, partition)
|
||||
return p.Leader, err
|
||||
}
|
||||
|
||||
// LookupPartition searches for the description of specified partition id.
|
||||
func (d *Dialer) LookupPartition(ctx context.Context, network string, address string, topic string, partition int) (Partition, error) {
|
||||
c, err := d.DialContext(ctx, network, address)
|
||||
if err != nil {
|
||||
return Partition{}, err
|
||||
}
|
||||
defer c.Close()
|
||||
|
||||
brkch := make(chan Partition, 1)
|
||||
errch := make(chan error, 1)
|
||||
|
||||
go func() {
|
||||
for attempt := 0; true; attempt++ {
|
||||
if attempt != 0 {
|
||||
sleep(ctx, backoff(attempt, 100*time.Millisecond, 10*time.Second))
|
||||
}
|
||||
|
||||
partitions, err := c.ReadPartitions(topic)
|
||||
if err != nil {
|
||||
if isTemporary(err) {
|
||||
continue
|
||||
}
|
||||
errch <- err
|
||||
return
|
||||
}
|
||||
|
||||
for _, p := range partitions {
|
||||
if p.ID == partition {
|
||||
brkch <- p
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
errch <- UnknownTopicOrPartition
|
||||
}()
|
||||
|
||||
var prt Partition
|
||||
select {
|
||||
case prt = <-brkch:
|
||||
case err = <-errch:
|
||||
case <-ctx.Done():
|
||||
err = ctx.Err()
|
||||
}
|
||||
return prt, err
|
||||
}
|
||||
|
||||
// LookupPartitions returns the list of partitions that exist for the given topic.
|
||||
func (d *Dialer) LookupPartitions(ctx context.Context, network string, address string, topic string) ([]Partition, error) {
|
||||
conn, err := d.DialContext(ctx, network, address)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
prtch := make(chan []Partition, 1)
|
||||
errch := make(chan error, 1)
|
||||
|
||||
go func() {
|
||||
if prt, err := conn.ReadPartitions(topic); err != nil {
|
||||
errch <- err
|
||||
} else {
|
||||
prtch <- prt
|
||||
}
|
||||
}()
|
||||
|
||||
var prt []Partition
|
||||
select {
|
||||
case prt = <-prtch:
|
||||
case err = <-errch:
|
||||
case <-ctx.Done():
|
||||
err = ctx.Err()
|
||||
}
|
||||
return prt, err
|
||||
}
|
||||
|
||||
// connectTLS returns a tls.Conn that has already completed the Handshake
|
||||
func (d *Dialer) connectTLS(ctx context.Context, conn net.Conn, config *tls.Config) (tlsConn *tls.Conn, err error) {
|
||||
tlsConn = tls.Client(conn, config)
|
||||
errch := make(chan error)
|
||||
|
||||
go func() {
|
||||
defer close(errch)
|
||||
errch <- tlsConn.Handshake()
|
||||
}()
|
||||
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
conn.Close()
|
||||
tlsConn.Close()
|
||||
<-errch // ignore possible error from Handshake
|
||||
err = ctx.Err()
|
||||
|
||||
case err = <-errch:
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (d *Dialer) dialContext(ctx context.Context, network string, address string) (net.Conn, error) {
|
||||
if r := d.Resolver; r != nil {
|
||||
host, port := splitHostPort(address)
|
||||
addrs, err := r.LookupHost(ctx, host)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(addrs) != 0 {
|
||||
address = addrs[0]
|
||||
}
|
||||
if len(port) != 0 {
|
||||
address, _ = splitHostPort(address)
|
||||
address = net.JoinHostPort(address, port)
|
||||
}
|
||||
}
|
||||
|
||||
conn, err := (&net.Dialer{
|
||||
LocalAddr: d.LocalAddr,
|
||||
DualStack: d.DualStack,
|
||||
FallbackDelay: d.FallbackDelay,
|
||||
KeepAlive: d.KeepAlive,
|
||||
}).DialContext(ctx, network, address)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if d.TLS != nil {
|
||||
c := d.TLS
|
||||
// If no ServerName is set, infer the ServerName
|
||||
// from the hostname we're connecting to.
|
||||
if c.ServerName == "" {
|
||||
c = d.TLS.Clone()
|
||||
// Copied from tls.go in the standard library.
|
||||
colonPos := strings.LastIndex(address, ":")
|
||||
if colonPos == -1 {
|
||||
colonPos = len(address)
|
||||
}
|
||||
hostname := address[:colonPos]
|
||||
c.ServerName = hostname
|
||||
}
|
||||
return d.connectTLS(ctx, conn, c)
|
||||
}
|
||||
|
||||
return conn, nil
|
||||
}
|
||||
|
||||
// DefaultDialer is the default dialer used when none is specified.
|
||||
var DefaultDialer = &Dialer{
|
||||
Timeout: 10 * time.Second,
|
||||
DualStack: true,
|
||||
}
|
||||
|
||||
// Dial is a convenience wrapper for DefaultDialer.Dial.
|
||||
func Dial(network string, address string) (*Conn, error) {
|
||||
return DefaultDialer.Dial(network, address)
|
||||
}
|
||||
|
||||
// DialContext is a convenience wrapper for DefaultDialer.DialContext.
|
||||
func DialContext(ctx context.Context, network string, address string) (*Conn, error) {
|
||||
return DefaultDialer.DialContext(ctx, network, address)
|
||||
}
|
||||
|
||||
// DialLeader is a convenience wrapper for DefaultDialer.DialLeader.
|
||||
func DialLeader(ctx context.Context, network string, address string, topic string, partition int) (*Conn, error) {
|
||||
return DefaultDialer.DialLeader(ctx, network, address, topic, partition)
|
||||
}
|
||||
|
||||
// DialPartition is a convenience wrapper for DefaultDialer.DialPartition.
|
||||
func DialPartition(ctx context.Context, network string, address string, partition Partition) (*Conn, error) {
|
||||
return DefaultDialer.DialPartition(ctx, network, address, partition)
|
||||
}
|
||||
|
||||
// LookupPartition is a convenience wrapper for DefaultDialer.LookupPartition.
|
||||
func LookupPartition(ctx context.Context, network string, address string, topic string, partition int) (Partition, error) {
|
||||
return DefaultDialer.LookupPartition(ctx, network, address, topic, partition)
|
||||
}
|
||||
|
||||
// LookupPartitions is a convenience wrapper for DefaultDialer.LookupPartitions.
|
||||
func LookupPartitions(ctx context.Context, network string, address string, topic string) ([]Partition, error) {
|
||||
return DefaultDialer.LookupPartitions(ctx, network, address, topic)
|
||||
}
|
||||
|
||||
// The Resolver interface is used as an abstraction to provide service discovery
|
||||
// of the hosts of a kafka cluster.
|
||||
type Resolver interface {
|
||||
// LookupHost looks up the given host using the local resolver.
|
||||
// It returns a slice of that host's addresses.
|
||||
LookupHost(ctx context.Context, host string) (addrs []string, err error)
|
||||
}
|
||||
|
||||
func sleep(ctx context.Context, duration time.Duration) bool {
|
||||
if duration == 0 {
|
||||
select {
|
||||
default:
|
||||
return true
|
||||
case <-ctx.Done():
|
||||
return false
|
||||
}
|
||||
}
|
||||
timer := time.NewTimer(duration)
|
||||
defer timer.Stop()
|
||||
select {
|
||||
case <-timer.C:
|
||||
return true
|
||||
case <-ctx.Done():
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func backoff(attempt int, min time.Duration, max time.Duration) time.Duration {
|
||||
d := time.Duration(attempt*attempt) * min
|
||||
if d > max {
|
||||
d = max
|
||||
}
|
||||
return d
|
||||
}
|
||||
|
||||
func splitHostPort(s string) (host string, port string) {
|
||||
host, port, _ = net.SplitHostPort(s)
|
||||
if len(host) == 0 && len(port) == 0 {
|
||||
host = s
|
||||
}
|
||||
return
|
||||
}
|
||||
+50
@@ -0,0 +1,50 @@
|
||||
package kafka
|
||||
|
||||
import "bufio"
|
||||
|
||||
func discardN(r *bufio.Reader, sz int, n int) (int, error) {
|
||||
var err error
|
||||
if n <= sz {
|
||||
n, err = r.Discard(n)
|
||||
} else {
|
||||
n, err = r.Discard(sz)
|
||||
if err == nil {
|
||||
err = errShortRead
|
||||
}
|
||||
}
|
||||
return sz - n, err
|
||||
}
|
||||
|
||||
func discardInt8(r *bufio.Reader, sz int) (int, error) {
|
||||
return discardN(r, sz, 1)
|
||||
}
|
||||
|
||||
func discardInt16(r *bufio.Reader, sz int) (int, error) {
|
||||
return discardN(r, sz, 2)
|
||||
}
|
||||
|
||||
func discardInt32(r *bufio.Reader, sz int) (int, error) {
|
||||
return discardN(r, sz, 4)
|
||||
}
|
||||
|
||||
func discardInt64(r *bufio.Reader, sz int) (int, error) {
|
||||
return discardN(r, sz, 8)
|
||||
}
|
||||
|
||||
func discardString(r *bufio.Reader, sz int) (int, error) {
|
||||
return readStringWith(r, sz, func(r *bufio.Reader, sz int, n int) (int, error) {
|
||||
if n < 0 {
|
||||
return sz, nil
|
||||
}
|
||||
return discardN(r, sz, n)
|
||||
})
|
||||
}
|
||||
|
||||
func discardBytes(r *bufio.Reader, sz int) (int, error) {
|
||||
return readBytesWith(r, sz, func(r *bufio.Reader, sz int, n int) (int, error) {
|
||||
if n < 0 {
|
||||
return sz, nil
|
||||
}
|
||||
return discardN(r, sz, n)
|
||||
})
|
||||
}
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
version: "3"
|
||||
services:
|
||||
kafka:
|
||||
image: wurstmeister/kafka:0.11.0.1
|
||||
restart: on-failure:3
|
||||
links:
|
||||
- zookeeper
|
||||
ports:
|
||||
- "9092:9092"
|
||||
environment:
|
||||
KAFKA_VERSION: '0.11.0.1'
|
||||
KAFKA_BROKER_ID: 1
|
||||
KAFKA_CREATE_TOPICS: 'test-writer-0:3:1,test-writer-1:3:1'
|
||||
KAFKA_DELETE_TOPIC_ENABLE: 'true'
|
||||
KAFKA_ADVERTISED_HOST_NAME: 'localhost'
|
||||
KAFKA_ADVERTISED_PORT: '9092'
|
||||
KAFKA_ZOOKEEPER_CONNECT: 'zookeeper:2181'
|
||||
KAFKA_AUTO_CREATE_TOPICS_ENABLE: 'true'
|
||||
KAFKA_MESSAGE_MAX_BYTES: 200000000
|
||||
|
||||
zookeeper:
|
||||
image: wurstmeister/zookeeper
|
||||
ports:
|
||||
- "2181:2181"
|
||||
+360
@@ -0,0 +1,360 @@
|
||||
package kafka
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
)
|
||||
|
||||
// Error represents the different error codes that may be returned by kafka.
|
||||
type Error int
|
||||
|
||||
const (
|
||||
Unknown Error = -1
|
||||
OffsetOutOfRange Error = 1
|
||||
InvalidMessage Error = 2
|
||||
UnknownTopicOrPartition Error = 3
|
||||
InvalidMessageSize Error = 4
|
||||
LeaderNotAvailable Error = 5
|
||||
NotLeaderForPartition Error = 6
|
||||
RequestTimedOut Error = 7
|
||||
BrokerNotAvailable Error = 8
|
||||
ReplicaNotAvailable Error = 9
|
||||
MessageSizeTooLarge Error = 10
|
||||
StaleControllerEpoch Error = 11
|
||||
OffsetMetadataTooLarge Error = 12
|
||||
GroupLoadInProgress Error = 14
|
||||
GroupCoordinatorNotAvailable Error = 15
|
||||
NotCoordinatorForGroup Error = 16
|
||||
InvalidTopic Error = 17
|
||||
RecordListTooLarge Error = 18
|
||||
NotEnoughReplicas Error = 19
|
||||
NotEnoughReplicasAfterAppend Error = 20
|
||||
InvalidRequiredAcks Error = 21
|
||||
IllegalGeneration Error = 22
|
||||
InconsistentGroupProtocol Error = 23
|
||||
InvalidGroupId Error = 24
|
||||
UnknownMemberId Error = 25
|
||||
InvalidSessionTimeout Error = 26
|
||||
RebalanceInProgress Error = 27
|
||||
InvalidCommitOffsetSize Error = 28
|
||||
TopicAuthorizationFailed Error = 29
|
||||
GroupAuthorizationFailed Error = 30
|
||||
ClusterAuthorizationFailed Error = 31
|
||||
InvalidTimestamp Error = 32
|
||||
UnsupportedSASLMechanism Error = 33
|
||||
IllegalSASLState Error = 34
|
||||
UnsupportedVersion Error = 35
|
||||
TopicAlreadyExists Error = 36
|
||||
InvalidPartitionNumber Error = 37
|
||||
InvalidReplicationFactor Error = 38
|
||||
InvalidReplicaAssignment Error = 39
|
||||
InvalidConfiguration Error = 40
|
||||
NotController Error = 41
|
||||
InvalidRequest Error = 42
|
||||
UnsupportedForMessageFormat Error = 43
|
||||
PolicyViolation Error = 44
|
||||
OutOfOrderSequenceNumber Error = 45
|
||||
DuplicateSequenceNumber Error = 46
|
||||
InvalidProducerEpoch Error = 47
|
||||
InvalidTransactionState Error = 48
|
||||
InvalidProducerIDMapping Error = 49
|
||||
InvalidTransactionTimeout Error = 50
|
||||
ConcurrentTransactions Error = 51
|
||||
TransactionCoordinatorFenced Error = 52
|
||||
TransactionalIDAuthorizationFailed Error = 53
|
||||
SecurityDisabled Error = 54
|
||||
BrokerAuthorizationFailed Error = 55
|
||||
)
|
||||
|
||||
// Error satisfies the error interface.
|
||||
func (e Error) Error() string {
|
||||
return fmt.Sprintf("[%d] %s: %s", e, e.Title(), e.Description())
|
||||
}
|
||||
|
||||
// Timeout returns true if the error was due to a timeout.
|
||||
func (e Error) Timeout() bool {
|
||||
return e == RequestTimedOut
|
||||
}
|
||||
|
||||
// Temporary returns true if the operation that generated the error may succeed
|
||||
// if retried at a later time.
|
||||
func (e Error) Temporary() bool {
|
||||
return e == LeaderNotAvailable ||
|
||||
e == BrokerNotAvailable ||
|
||||
e == ReplicaNotAvailable ||
|
||||
e == GroupLoadInProgress ||
|
||||
e == GroupCoordinatorNotAvailable ||
|
||||
e == RebalanceInProgress ||
|
||||
e.Timeout()
|
||||
}
|
||||
|
||||
// Title returns a human readable title for the error.
|
||||
func (e Error) Title() string {
|
||||
switch e {
|
||||
case Unknown:
|
||||
return "Unknown"
|
||||
case OffsetOutOfRange:
|
||||
return "Offset Out Of Range"
|
||||
case InvalidMessage:
|
||||
return "Invalid Message"
|
||||
case UnknownTopicOrPartition:
|
||||
return "Unknown Topic Or Partition"
|
||||
case InvalidMessageSize:
|
||||
return "Invalid Message Size"
|
||||
case LeaderNotAvailable:
|
||||
return "Leader Not Available"
|
||||
case NotLeaderForPartition:
|
||||
return "Not Leader For Partition"
|
||||
case RequestTimedOut:
|
||||
return "Request Timed Out"
|
||||
case BrokerNotAvailable:
|
||||
return "Broker Not Available"
|
||||
case ReplicaNotAvailable:
|
||||
return "Replica Not Available"
|
||||
case MessageSizeTooLarge:
|
||||
return "Message Size Too Large"
|
||||
case StaleControllerEpoch:
|
||||
return "Stale Controller Epoch"
|
||||
case OffsetMetadataTooLarge:
|
||||
return "Offset Metadata Too Large"
|
||||
case GroupLoadInProgress:
|
||||
return "Group Load In Progress"
|
||||
case GroupCoordinatorNotAvailable:
|
||||
return "Group Coordinator Not Available"
|
||||
case NotCoordinatorForGroup:
|
||||
return "Not Coordinator For Group"
|
||||
case InvalidTopic:
|
||||
return "Invalid Topic"
|
||||
case RecordListTooLarge:
|
||||
return "Record List Too Large"
|
||||
case NotEnoughReplicas:
|
||||
return "Not Enough Replicas"
|
||||
case NotEnoughReplicasAfterAppend:
|
||||
return "Not Enough Replicas After Append"
|
||||
case InvalidRequiredAcks:
|
||||
return "Invalid Required Acks"
|
||||
case IllegalGeneration:
|
||||
return "Illegal Generation"
|
||||
case InconsistentGroupProtocol:
|
||||
return "Inconsistent Group Protocol"
|
||||
case InvalidGroupId:
|
||||
return "Invalid Group ID"
|
||||
case UnknownMemberId:
|
||||
return "Unknown Member ID"
|
||||
case InvalidSessionTimeout:
|
||||
return "Invalid Session Timeout"
|
||||
case RebalanceInProgress:
|
||||
return "Rebalance In Progress"
|
||||
case InvalidCommitOffsetSize:
|
||||
return "Invalid Commit Offset Size"
|
||||
case TopicAuthorizationFailed:
|
||||
return "Topic Authorization Failed"
|
||||
case GroupAuthorizationFailed:
|
||||
return "Group Authorization Failed"
|
||||
case ClusterAuthorizationFailed:
|
||||
return "Cluster Authorization Failed"
|
||||
case InvalidTimestamp:
|
||||
return "Invalid Timestamp"
|
||||
case UnsupportedSASLMechanism:
|
||||
return "Unsupported SASL Mechanism"
|
||||
case IllegalSASLState:
|
||||
return "Illegal SASL State"
|
||||
case UnsupportedVersion:
|
||||
return "Unsupported Version"
|
||||
case TopicAlreadyExists:
|
||||
return "Topic Already Exists"
|
||||
case InvalidPartitionNumber:
|
||||
return "Invalid Partition Number"
|
||||
case InvalidReplicationFactor:
|
||||
return "Invalid Replication Factor"
|
||||
case InvalidReplicaAssignment:
|
||||
return "Invalid Replica Assignment"
|
||||
case InvalidConfiguration:
|
||||
return "Invalid Configuration"
|
||||
case NotController:
|
||||
return "Not Controller"
|
||||
case InvalidRequest:
|
||||
return "Invalid Request"
|
||||
case UnsupportedForMessageFormat:
|
||||
return "Unsupported For Message Format"
|
||||
case PolicyViolation:
|
||||
return "Policy Violation"
|
||||
case OutOfOrderSequenceNumber:
|
||||
return "Out Of Order Sequence Number"
|
||||
case DuplicateSequenceNumber:
|
||||
return "Duplicate Sequence Number"
|
||||
case InvalidProducerEpoch:
|
||||
return "Invalid Producer Epoch"
|
||||
case InvalidTransactionState:
|
||||
return "Invalid Transaction State"
|
||||
case InvalidProducerIDMapping:
|
||||
return "Invalid Producer ID Mapping"
|
||||
case InvalidTransactionTimeout:
|
||||
return "Invalid Transaction Timeout"
|
||||
case ConcurrentTransactions:
|
||||
return "Concurrent Transactions"
|
||||
case TransactionCoordinatorFenced:
|
||||
return "Transaction Coordinator Fenced"
|
||||
case TransactionalIDAuthorizationFailed:
|
||||
return "Transactional ID Authorization Failed"
|
||||
case SecurityDisabled:
|
||||
return "Security Disabled"
|
||||
case BrokerAuthorizationFailed:
|
||||
return "Broker Authorization Failed"
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// Description returns a human readable description of cause of the error.
|
||||
func (e Error) Description() string {
|
||||
switch e {
|
||||
case Unknown:
|
||||
return "an unexpected server error occurred"
|
||||
case OffsetOutOfRange:
|
||||
return "the requested offset is outside the range of offsets maintained by the server for the given topic/partition"
|
||||
case InvalidMessage:
|
||||
return "the message contents does not match its CRC"
|
||||
case UnknownTopicOrPartition:
|
||||
return "the request is for a topic or partition that does not exist on this broker"
|
||||
case InvalidMessageSize:
|
||||
return "the message has a negative size"
|
||||
case LeaderNotAvailable:
|
||||
return "the cluster is in the middle of a leadership election and there is currently no leader for this partition and hence it is unavailable for writes"
|
||||
case NotLeaderForPartition:
|
||||
return "the client attempted to send messages to a replica that is not the leader for some partition, the client's metadata are likely out of date"
|
||||
case RequestTimedOut:
|
||||
return "the request exceeded the user-specified time limit in the request"
|
||||
case BrokerNotAvailable:
|
||||
return "not a client facing error and is used mostly by tools when a broker is not alive"
|
||||
case ReplicaNotAvailable:
|
||||
return "a replica is expected on a broker, but is not (this can be safely ignored)"
|
||||
case MessageSizeTooLarge:
|
||||
return "the server has a configurable maximum message size to avoid unbounded memory allocation and the client attempted to produce a message larger than this maximum"
|
||||
case StaleControllerEpoch:
|
||||
return "internal error code for broker-to-broker communication"
|
||||
case OffsetMetadataTooLarge:
|
||||
return "the client specified a string larger than configured maximum for offset metadata"
|
||||
case GroupLoadInProgress:
|
||||
return "the broker returns this error code for an offset fetch request if it is still loading offsets (after a leader change for that offsets topic partition), or in response to group membership requests (such as heartbeats) when group metadata is being loaded by the coordinator"
|
||||
case GroupCoordinatorNotAvailable:
|
||||
return "the broker returns this error code for group coordinator requests, offset commits, and most group management requests if the offsets topic has not yet been created, or if the group coordinator is not active"
|
||||
case NotCoordinatorForGroup:
|
||||
return "the broker returns this error code if it receives an offset fetch or commit request for a group that it is not a coordinator for"
|
||||
case InvalidTopic:
|
||||
return "a request which attempted to access an invalid topic (e.g. one which has an illegal name), or if an attempt was made to write to an internal topic (such as the consumer offsets topic)"
|
||||
case RecordListTooLarge:
|
||||
return "a message batch in a produce request exceeds the maximum configured segment size"
|
||||
case NotEnoughReplicas:
|
||||
return "the number of in-sync replicas is lower than the configured minimum and requiredAcks is -1"
|
||||
case NotEnoughReplicasAfterAppend:
|
||||
return "the message was written to the log, but with fewer in-sync replicas than required."
|
||||
case InvalidRequiredAcks:
|
||||
return "the requested requiredAcks is invalid (anything other than -1, 1, or 0)"
|
||||
case IllegalGeneration:
|
||||
return "the generation id provided in the request is not the current generation"
|
||||
case InconsistentGroupProtocol:
|
||||
return "the member provided a protocol type or set of protocols which is not compatible with the current group"
|
||||
case InvalidGroupId:
|
||||
return "the group id is empty or null"
|
||||
case UnknownMemberId:
|
||||
return "the member id is not in the current generation"
|
||||
case InvalidSessionTimeout:
|
||||
return "the requested session timeout is outside of the allowed range on the broker"
|
||||
case RebalanceInProgress:
|
||||
return "the coordinator has begun rebalancing the group, the client should rejoin the group"
|
||||
case InvalidCommitOffsetSize:
|
||||
return "an offset commit was rejected because of oversize metadata"
|
||||
case TopicAuthorizationFailed:
|
||||
return "the client is not authorized to access the requested topic"
|
||||
case GroupAuthorizationFailed:
|
||||
return "the client is not authorized to access a particular group id"
|
||||
case ClusterAuthorizationFailed:
|
||||
return "the client is not authorized to use an inter-broker or administrative API"
|
||||
case InvalidTimestamp:
|
||||
return "the timestamp of the message is out of acceptable range"
|
||||
case UnsupportedSASLMechanism:
|
||||
return "the broker does not support the requested SASL mechanism"
|
||||
case IllegalSASLState:
|
||||
return "the request is not valid given the current SASL state"
|
||||
case UnsupportedVersion:
|
||||
return "the version of API is not supported"
|
||||
case TopicAlreadyExists:
|
||||
return "a topic with this name already exists"
|
||||
case InvalidPartitionNumber:
|
||||
return "the number of partitions is invalid"
|
||||
case InvalidReplicationFactor:
|
||||
return "the replication-factor is invalid"
|
||||
case InvalidReplicaAssignment:
|
||||
return "the replica assignment is invalid"
|
||||
case InvalidConfiguration:
|
||||
return "the configuration is invalid"
|
||||
case NotController:
|
||||
return "this is not the correct controller for this cluster"
|
||||
case InvalidRequest:
|
||||
return "this most likely occurs because of a request being malformed by the client library or the message was sent to an incompatible broker, se the broker logs for more details"
|
||||
case UnsupportedForMessageFormat:
|
||||
return "the message format version on the broker does not support the request"
|
||||
case PolicyViolation:
|
||||
return "the request parameters do not satisfy the configured policy"
|
||||
case OutOfOrderSequenceNumber:
|
||||
return "the broker received an out of order sequence number"
|
||||
case DuplicateSequenceNumber:
|
||||
return "the broker received a duplicate sequence number"
|
||||
case InvalidProducerEpoch:
|
||||
return "the producer attempted an operation with an old epoch, either there is a newer producer with the same transactional ID, or the producer's transaction has been expired by the broker"
|
||||
case InvalidTransactionState:
|
||||
return "the producer attempted a transactional operation in an invalid state"
|
||||
case InvalidProducerIDMapping:
|
||||
return "the producer attempted to use a producer id which is not currently assigned to its transactional ID"
|
||||
case InvalidTransactionTimeout:
|
||||
return "the transaction timeout is larger than the maximum value allowed by the broker (as configured by max.transaction.timeout.ms)"
|
||||
case ConcurrentTransactions:
|
||||
return "the producer attempted to update a transaction while another concurrent operation on the same transaction was ongoing"
|
||||
case TransactionCoordinatorFenced:
|
||||
return "the transaction coordinator sending a WriteTxnMarker is no longer the current coordinator for a given producer"
|
||||
case TransactionalIDAuthorizationFailed:
|
||||
return "the transactional ID authorization failed"
|
||||
case SecurityDisabled:
|
||||
return "the security features are disabled"
|
||||
case BrokerAuthorizationFailed:
|
||||
return "the broker authorization failed"
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func isTimeout(err error) bool {
|
||||
e, ok := err.(interface {
|
||||
Timeout() bool
|
||||
})
|
||||
return ok && e.Timeout()
|
||||
}
|
||||
|
||||
func isTemporary(err error) bool {
|
||||
e, ok := err.(interface {
|
||||
Temporary() bool
|
||||
})
|
||||
return ok && e.Temporary()
|
||||
}
|
||||
|
||||
func silentEOF(err error) error {
|
||||
if err == io.EOF {
|
||||
err = nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func dontExpectEOF(err error) error {
|
||||
if err == io.EOF {
|
||||
err = io.ErrUnexpectedEOF
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func coalesceErrors(errs ...error) error {
|
||||
for _, err := range errs {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
+101
@@ -0,0 +1,101 @@
|
||||
package kafka
|
||||
|
||||
import "bufio"
|
||||
|
||||
type fetchRequestV2 struct {
|
||||
ReplicaID int32
|
||||
MaxWaitTime int32
|
||||
MinBytes int32
|
||||
Topics []fetchRequestTopicV2
|
||||
}
|
||||
|
||||
func (r fetchRequestV2) size() int32 {
|
||||
return 4 + 4 + 4 + sizeofArray(len(r.Topics), func(i int) int32 { return r.Topics[i].size() })
|
||||
}
|
||||
|
||||
func (r fetchRequestV2) writeTo(w *bufio.Writer) {
|
||||
writeInt32(w, r.ReplicaID)
|
||||
writeInt32(w, r.MaxWaitTime)
|
||||
writeInt32(w, r.MinBytes)
|
||||
writeArray(w, len(r.Topics), func(i int) { r.Topics[i].writeTo(w) })
|
||||
}
|
||||
|
||||
type fetchRequestTopicV2 struct {
|
||||
TopicName string
|
||||
Partitions []fetchRequestPartitionV2
|
||||
}
|
||||
|
||||
func (t fetchRequestTopicV2) size() int32 {
|
||||
return sizeofString(t.TopicName) +
|
||||
sizeofArray(len(t.Partitions), func(i int) int32 { return t.Partitions[i].size() })
|
||||
}
|
||||
|
||||
func (t fetchRequestTopicV2) writeTo(w *bufio.Writer) {
|
||||
writeString(w, t.TopicName)
|
||||
writeArray(w, len(t.Partitions), func(i int) { t.Partitions[i].writeTo(w) })
|
||||
}
|
||||
|
||||
type fetchRequestPartitionV2 struct {
|
||||
Partition int32
|
||||
FetchOffset int64
|
||||
MaxBytes int32
|
||||
}
|
||||
|
||||
func (p fetchRequestPartitionV2) size() int32 {
|
||||
return 4 + 8 + 4
|
||||
}
|
||||
|
||||
func (p fetchRequestPartitionV2) writeTo(w *bufio.Writer) {
|
||||
writeInt32(w, p.Partition)
|
||||
writeInt64(w, p.FetchOffset)
|
||||
writeInt32(w, p.MaxBytes)
|
||||
}
|
||||
|
||||
type fetchResponseV2 struct {
|
||||
ThrottleTime int32
|
||||
Topics []fetchResponseTopicV2
|
||||
}
|
||||
|
||||
func (r fetchResponseV2) size() int32 {
|
||||
return 4 + sizeofArray(len(r.Topics), func(i int) int32 { return r.Topics[i].size() })
|
||||
}
|
||||
|
||||
func (r fetchResponseV2) writeTo(w *bufio.Writer) {
|
||||
writeInt32(w, r.ThrottleTime)
|
||||
writeArray(w, len(r.Topics), func(i int) { r.Topics[i].writeTo(w) })
|
||||
}
|
||||
|
||||
type fetchResponseTopicV2 struct {
|
||||
TopicName string
|
||||
Partitions []fetchResponsePartitionV2
|
||||
}
|
||||
|
||||
func (t fetchResponseTopicV2) size() int32 {
|
||||
return sizeofString(t.TopicName) +
|
||||
sizeofArray(len(t.Partitions), func(i int) int32 { return t.Partitions[i].size() })
|
||||
}
|
||||
|
||||
func (t fetchResponseTopicV2) writeTo(w *bufio.Writer) {
|
||||
writeString(w, t.TopicName)
|
||||
writeArray(w, len(t.Partitions), func(i int) { t.Partitions[i].writeTo(w) })
|
||||
}
|
||||
|
||||
type fetchResponsePartitionV2 struct {
|
||||
Partition int32
|
||||
ErrorCode int16
|
||||
HighwaterMarkOffset int64
|
||||
MessageSetSize int32
|
||||
MessageSet messageSet
|
||||
}
|
||||
|
||||
func (p fetchResponsePartitionV2) size() int32 {
|
||||
return 4 + 2 + 8 + 4 + p.MessageSet.size()
|
||||
}
|
||||
|
||||
func (p fetchResponsePartitionV2) writeTo(w *bufio.Writer) {
|
||||
writeInt32(w, p.Partition)
|
||||
writeInt16(w, p.ErrorCode)
|
||||
writeInt64(w, p.HighwaterMarkOffset)
|
||||
writeInt32(w, p.MessageSetSize)
|
||||
p.MessageSet.writeTo(w)
|
||||
}
|
||||
+86
@@ -0,0 +1,86 @@
|
||||
package kafka
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
)
|
||||
|
||||
// FindCoordinatorRequestV0 requests the coordinator for the specified group or transaction
|
||||
//
|
||||
// See http://kafka.apache.org/protocol.html#The_Messages_FindCoordinator
|
||||
type findCoordinatorRequestV0 struct {
|
||||
// CoordinatorKey holds id to use for finding the coordinator (for groups, this is
|
||||
// the groupId, for transactional producers, this is the transactional id)
|
||||
CoordinatorKey string
|
||||
}
|
||||
|
||||
func (t findCoordinatorRequestV0) size() int32 {
|
||||
return sizeofString(t.CoordinatorKey)
|
||||
}
|
||||
|
||||
func (t findCoordinatorRequestV0) writeTo(w *bufio.Writer) {
|
||||
writeString(w, t.CoordinatorKey)
|
||||
}
|
||||
|
||||
type findCoordinatorResponseCoordinatorV0 struct {
|
||||
// NodeID holds the broker id.
|
||||
NodeID int32
|
||||
|
||||
// Host of the broker
|
||||
Host string
|
||||
|
||||
// Port on which broker accepts requests
|
||||
Port int32
|
||||
}
|
||||
|
||||
func (t findCoordinatorResponseCoordinatorV0) size() int32 {
|
||||
return sizeofInt32(t.NodeID) +
|
||||
sizeofString(t.Host) +
|
||||
sizeofInt32(t.Port)
|
||||
}
|
||||
|
||||
func (t findCoordinatorResponseCoordinatorV0) writeTo(w *bufio.Writer) {
|
||||
writeInt32(w, t.NodeID)
|
||||
writeString(w, t.Host)
|
||||
writeInt32(w, t.Port)
|
||||
}
|
||||
|
||||
func (t *findCoordinatorResponseCoordinatorV0) readFrom(r *bufio.Reader, size int) (remain int, err error) {
|
||||
if remain, err = readInt32(r, size, &t.NodeID); err != nil {
|
||||
return
|
||||
}
|
||||
if remain, err = readString(r, remain, &t.Host); err != nil {
|
||||
return
|
||||
}
|
||||
if remain, err = readInt32(r, remain, &t.Port); err != nil {
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
type findCoordinatorResponseV0 struct {
|
||||
// ErrorCode holds response error code
|
||||
ErrorCode int16
|
||||
|
||||
// Coordinator holds host and port information for the coordinator
|
||||
Coordinator findCoordinatorResponseCoordinatorV0
|
||||
}
|
||||
|
||||
func (t findCoordinatorResponseV0) size() int32 {
|
||||
return sizeofInt16(t.ErrorCode) +
|
||||
t.Coordinator.size()
|
||||
}
|
||||
|
||||
func (t findCoordinatorResponseV0) writeTo(w *bufio.Writer) {
|
||||
writeInt16(w, t.ErrorCode)
|
||||
t.Coordinator.writeTo(w)
|
||||
}
|
||||
|
||||
func (t *findCoordinatorResponseV0) readFrom(r *bufio.Reader, size int) (remain int, err error) {
|
||||
if remain, err = readInt16(r, size, &t.ErrorCode); err != nil {
|
||||
return
|
||||
}
|
||||
if remain, err = (&t.Coordinator).readFrom(r, remain); err != nil {
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
+187
@@ -0,0 +1,187 @@
|
||||
package kafka
|
||||
|
||||
import "sort"
|
||||
|
||||
// GroupMember describes a single participant in a consumer group.
|
||||
type GroupMember struct {
|
||||
// ID is the unique ID for this member as taken from the JoinGroup response.
|
||||
ID string
|
||||
|
||||
// Topics is a list of topics that this member is consuming.
|
||||
Topics []string
|
||||
|
||||
// UserData contains any information that the GroupBalancer sent to the
|
||||
// consumer group coordinator.
|
||||
UserData []byte
|
||||
}
|
||||
|
||||
// GroupMemberAssignments holds MemberID => topic => partitions
|
||||
type GroupMemberAssignments map[string]map[string][]int
|
||||
|
||||
// GroupBalancer encapsulates the client side rebalancing logic
|
||||
type GroupBalancer interface {
|
||||
// ProtocolName of the GroupBalancer
|
||||
ProtocolName() string
|
||||
|
||||
// UserData provides the GroupBalancer an opportunity to embed custom
|
||||
// UserData into the metadata.
|
||||
//
|
||||
// Will be used by JoinGroup to begin the consumer group handshake.
|
||||
//
|
||||
// See https://cwiki.apache.org/confluence/display/KAFKA/A+Guide+To+The+Kafka+Protocol#AGuideToTheKafkaProtocol-JoinGroupRequest
|
||||
UserData() ([]byte, error)
|
||||
|
||||
// DefineMemberships returns which members will be consuming
|
||||
// which topic partitions
|
||||
AssignGroups(members []GroupMember, partitions []Partition) GroupMemberAssignments
|
||||
}
|
||||
|
||||
// RangeGroupBalancer groups consumers by partition
|
||||
//
|
||||
// Example: 5 partitions, 2 consumers
|
||||
// C0: [0, 1, 2]
|
||||
// C1: [3, 4]
|
||||
//
|
||||
// Example: 6 partitions, 3 consumers
|
||||
// C0: [0, 1]
|
||||
// C1: [2, 3]
|
||||
// C2: [4, 5]
|
||||
//
|
||||
type RangeGroupBalancer struct{}
|
||||
|
||||
func (r RangeGroupBalancer) ProtocolName() string {
|
||||
return "range"
|
||||
}
|
||||
|
||||
func (r RangeGroupBalancer) UserData() ([]byte, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (r RangeGroupBalancer) AssignGroups(members []GroupMember, topicPartitions []Partition) GroupMemberAssignments {
|
||||
groupAssignments := GroupMemberAssignments{}
|
||||
membersByTopic := findMembersByTopic(members)
|
||||
|
||||
for topic, members := range membersByTopic {
|
||||
partitions := findPartitions(topic, topicPartitions)
|
||||
partitionCount := len(partitions)
|
||||
memberCount := len(members)
|
||||
|
||||
for memberIndex, member := range members {
|
||||
assignmentsByTopic, ok := groupAssignments[member.ID]
|
||||
if !ok {
|
||||
assignmentsByTopic = map[string][]int{}
|
||||
groupAssignments[member.ID] = assignmentsByTopic
|
||||
}
|
||||
|
||||
minIndex := memberIndex * partitionCount / memberCount
|
||||
maxIndex := (memberIndex + 1) * partitionCount / memberCount
|
||||
|
||||
for partitionIndex, partition := range partitions {
|
||||
if partitionIndex >= minIndex && partitionIndex < maxIndex {
|
||||
assignmentsByTopic[topic] = append(assignmentsByTopic[topic], partition)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return groupAssignments
|
||||
}
|
||||
|
||||
// RoundrobinGroupBalancer divides partitions evenly among consumers
|
||||
//
|
||||
// Example: 5 partitions, 2 consumers
|
||||
// C0: [0, 2, 4]
|
||||
// C1: [1, 3]
|
||||
//
|
||||
// Example: 6 partitions, 3 consumers
|
||||
// C0: [0, 3]
|
||||
// C1: [1, 4]
|
||||
// C2: [2, 5]
|
||||
//
|
||||
type RoundRobinGroupBalancer struct{}
|
||||
|
||||
func (r RoundRobinGroupBalancer) ProtocolName() string {
|
||||
return "roundrobin"
|
||||
}
|
||||
|
||||
func (r RoundRobinGroupBalancer) UserData() ([]byte, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (r RoundRobinGroupBalancer) AssignGroups(members []GroupMember, topicPartitions []Partition) GroupMemberAssignments {
|
||||
groupAssignments := GroupMemberAssignments{}
|
||||
membersByTopic := findMembersByTopic(members)
|
||||
for topic, members := range membersByTopic {
|
||||
partitionIDs := findPartitions(topic, topicPartitions)
|
||||
memberCount := len(members)
|
||||
|
||||
for memberIndex, member := range members {
|
||||
assignmentsByTopic, ok := groupAssignments[member.ID]
|
||||
if !ok {
|
||||
assignmentsByTopic = map[string][]int{}
|
||||
groupAssignments[member.ID] = assignmentsByTopic
|
||||
}
|
||||
|
||||
for partitionIndex, partition := range partitionIDs {
|
||||
if (partitionIndex % memberCount) == memberIndex {
|
||||
assignmentsByTopic[topic] = append(assignmentsByTopic[topic], partition)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return groupAssignments
|
||||
}
|
||||
|
||||
// findPartitions extracts the partition ids associated with the topic from the
|
||||
// list of Partitions provided
|
||||
func findPartitions(topic string, partitions []Partition) []int {
|
||||
var ids []int
|
||||
for _, partition := range partitions {
|
||||
if partition.Topic == topic {
|
||||
ids = append(ids, partition.ID)
|
||||
}
|
||||
}
|
||||
return ids
|
||||
}
|
||||
|
||||
// findMembersByTopic groups the memberGroupMetadata by topic
|
||||
func findMembersByTopic(members []GroupMember) map[string][]GroupMember {
|
||||
membersByTopic := map[string][]GroupMember{}
|
||||
for _, member := range members {
|
||||
for _, topic := range member.Topics {
|
||||
membersByTopic[topic] = append(membersByTopic[topic], member)
|
||||
}
|
||||
}
|
||||
|
||||
// normalize ordering of members to enabling grouping across topics by partitions
|
||||
//
|
||||
// Want:
|
||||
// C0 [T0/P0, T1/P0]
|
||||
// C1 [T0/P1, T1/P1]
|
||||
//
|
||||
// Not:
|
||||
// C0 [T0/P0, T1/P1]
|
||||
// C1 [T0/P1, T1/P0]
|
||||
//
|
||||
// Even though the later is still round robin, the partitions are crossed
|
||||
//
|
||||
for _, members := range membersByTopic {
|
||||
sort.Slice(members, func(i, j int) bool {
|
||||
return members[i].ID < members[j].ID
|
||||
})
|
||||
}
|
||||
|
||||
return membersByTopic
|
||||
}
|
||||
|
||||
// findGroupBalancer returns the GroupBalancer with the specified protocolName
|
||||
// from the slice provided
|
||||
func findGroupBalancer(protocolName string, balancers []GroupBalancer) (GroupBalancer, bool) {
|
||||
for _, balancer := range balancers {
|
||||
if balancer.ProtocolName() == protocolName {
|
||||
return balancer, true
|
||||
}
|
||||
}
|
||||
return nil, false
|
||||
}
|
||||
+46
@@ -0,0 +1,46 @@
|
||||
package kafka
|
||||
|
||||
import "bufio"
|
||||
|
||||
type heartbeatRequestV0 struct {
|
||||
// GroupID holds the unique group identifier
|
||||
GroupID string
|
||||
|
||||
// GenerationID holds the generation of the group.
|
||||
GenerationID int32
|
||||
|
||||
// MemberID assigned by the group coordinator
|
||||
MemberID string
|
||||
}
|
||||
|
||||
func (t heartbeatRequestV0) size() int32 {
|
||||
return sizeofString(t.GroupID) +
|
||||
sizeofInt32(t.GenerationID) +
|
||||
sizeofString(t.MemberID)
|
||||
}
|
||||
|
||||
func (t heartbeatRequestV0) writeTo(w *bufio.Writer) {
|
||||
writeString(w, t.GroupID)
|
||||
writeInt32(w, t.GenerationID)
|
||||
writeString(w, t.MemberID)
|
||||
}
|
||||
|
||||
type heartbeatResponseV0 struct {
|
||||
// ErrorCode holds response error code
|
||||
ErrorCode int16
|
||||
}
|
||||
|
||||
func (t heartbeatResponseV0) size() int32 {
|
||||
return sizeofInt16(t.ErrorCode)
|
||||
}
|
||||
|
||||
func (t heartbeatResponseV0) writeTo(w *bufio.Writer) {
|
||||
writeInt16(w, t.ErrorCode)
|
||||
}
|
||||
|
||||
func (t *heartbeatResponseV0) readFrom(r *bufio.Reader, sz int) (remain int, err error) {
|
||||
if remain, err = readInt16(r, sz, &t.ErrorCode); err != nil {
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
+202
@@ -0,0 +1,202 @@
|
||||
package kafka
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
)
|
||||
|
||||
type memberGroupMetadata struct {
|
||||
// MemberID assigned by the group coordinator or null if joining for the
|
||||
// first time.
|
||||
MemberID string
|
||||
Metadata groupMetadata
|
||||
}
|
||||
|
||||
type groupMetadata struct {
|
||||
Version int16
|
||||
Topics []string
|
||||
UserData []byte
|
||||
}
|
||||
|
||||
func (t groupMetadata) size() int32 {
|
||||
return sizeofInt16(t.Version) +
|
||||
sizeofStringArray(t.Topics) +
|
||||
sizeofBytes(t.UserData)
|
||||
}
|
||||
|
||||
func (t groupMetadata) writeTo(w *bufio.Writer) {
|
||||
writeInt16(w, t.Version)
|
||||
writeStringArray(w, t.Topics)
|
||||
writeBytes(w, t.UserData)
|
||||
}
|
||||
|
||||
func (t groupMetadata) bytes() []byte {
|
||||
buf := bytes.NewBuffer(nil)
|
||||
w := bufio.NewWriter(buf)
|
||||
t.writeTo(w)
|
||||
w.Flush()
|
||||
return buf.Bytes()
|
||||
}
|
||||
|
||||
func (t *groupMetadata) readFrom(r *bufio.Reader, size int) (remain int, err error) {
|
||||
if remain, err = readInt16(r, size, &t.Version); err != nil {
|
||||
return
|
||||
}
|
||||
if remain, err = readStringArray(r, remain, &t.Topics); err != nil {
|
||||
return
|
||||
}
|
||||
if remain, err = readBytes(r, remain, &t.UserData); err != nil {
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
type joinGroupRequestGroupProtocolV1 struct {
|
||||
ProtocolName string
|
||||
ProtocolMetadata []byte
|
||||
}
|
||||
|
||||
func (t joinGroupRequestGroupProtocolV1) size() int32 {
|
||||
return sizeofString(t.ProtocolName) +
|
||||
sizeofBytes(t.ProtocolMetadata)
|
||||
}
|
||||
|
||||
func (t joinGroupRequestGroupProtocolV1) writeTo(w *bufio.Writer) {
|
||||
writeString(w, t.ProtocolName)
|
||||
writeBytes(w, t.ProtocolMetadata)
|
||||
}
|
||||
|
||||
type joinGroupRequestV1 struct {
|
||||
// GroupID holds the unique group identifier
|
||||
GroupID string
|
||||
|
||||
// SessionTimeout holds the coordinator considers the consumer dead if it
|
||||
// receives no heartbeat after this timeout in ms.
|
||||
SessionTimeout int32
|
||||
|
||||
// RebalanceTimeout holds the maximum time that the coordinator will wait
|
||||
// for each member to rejoin when rebalancing the group in ms
|
||||
RebalanceTimeout int32
|
||||
|
||||
// MemberID assigned by the group coordinator or the zero string if joining
|
||||
// for the first time.
|
||||
MemberID string
|
||||
|
||||
// ProtocolType holds the unique name for class of protocols implemented by group
|
||||
ProtocolType string
|
||||
|
||||
// GroupProtocols holds the list of protocols that the member supports
|
||||
GroupProtocols []joinGroupRequestGroupProtocolV1
|
||||
}
|
||||
|
||||
func (t joinGroupRequestV1) size() int32 {
|
||||
return sizeofString(t.GroupID) +
|
||||
sizeofInt32(t.SessionTimeout) +
|
||||
sizeofInt32(t.RebalanceTimeout) +
|
||||
sizeofString(t.MemberID) +
|
||||
sizeofString(t.ProtocolType) +
|
||||
sizeofArray(len(t.GroupProtocols), func(i int) int32 { return t.GroupProtocols[i].size() })
|
||||
}
|
||||
|
||||
func (t joinGroupRequestV1) writeTo(w *bufio.Writer) {
|
||||
writeString(w, t.GroupID)
|
||||
writeInt32(w, t.SessionTimeout)
|
||||
writeInt32(w, t.RebalanceTimeout)
|
||||
writeString(w, t.MemberID)
|
||||
writeString(w, t.ProtocolType)
|
||||
writeArray(w, len(t.GroupProtocols), func(i int) { t.GroupProtocols[i].writeTo(w) })
|
||||
}
|
||||
|
||||
type joinGroupResponseMemberV1 struct {
|
||||
// MemberID assigned by the group coordinator
|
||||
MemberID string
|
||||
MemberMetadata []byte
|
||||
}
|
||||
|
||||
func (t joinGroupResponseMemberV1) size() int32 {
|
||||
return sizeofString(t.MemberID) +
|
||||
sizeofBytes(t.MemberMetadata)
|
||||
}
|
||||
|
||||
func (t joinGroupResponseMemberV1) writeTo(w *bufio.Writer) {
|
||||
writeString(w, t.MemberID)
|
||||
writeBytes(w, t.MemberMetadata)
|
||||
}
|
||||
|
||||
func (t *joinGroupResponseMemberV1) readFrom(r *bufio.Reader, size int) (remain int, err error) {
|
||||
if remain, err = readString(r, size, &t.MemberID); err != nil {
|
||||
return
|
||||
}
|
||||
if remain, err = readBytes(r, remain, &t.MemberMetadata); err != nil {
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
type joinGroupResponseV1 struct {
|
||||
// ErrorCode holds response error code
|
||||
ErrorCode int16
|
||||
|
||||
// GenerationID holds the generation of the group.
|
||||
GenerationID int32
|
||||
|
||||
// GroupProtocol holds the group protocol selected by the coordinator
|
||||
GroupProtocol string
|
||||
|
||||
// LeaderID holds the leader of the group
|
||||
LeaderID string
|
||||
|
||||
// MemberID assigned by the group coordinator
|
||||
MemberID string
|
||||
Members []joinGroupResponseMemberV1
|
||||
}
|
||||
|
||||
func (t joinGroupResponseV1) size() int32 {
|
||||
return sizeofInt16(t.ErrorCode) +
|
||||
sizeofInt32(t.GenerationID) +
|
||||
sizeofString(t.GroupProtocol) +
|
||||
sizeofString(t.LeaderID) +
|
||||
sizeofString(t.MemberID) +
|
||||
sizeofArray(len(t.MemberID), func(i int) int32 { return t.Members[i].size() })
|
||||
}
|
||||
|
||||
func (t joinGroupResponseV1) writeTo(w *bufio.Writer) {
|
||||
writeInt16(w, t.ErrorCode)
|
||||
writeInt32(w, t.GenerationID)
|
||||
writeString(w, t.GroupProtocol)
|
||||
writeString(w, t.LeaderID)
|
||||
writeString(w, t.MemberID)
|
||||
writeArray(w, len(t.Members), func(i int) { t.Members[i].writeTo(w) })
|
||||
}
|
||||
|
||||
func (t *joinGroupResponseV1) readFrom(r *bufio.Reader, size int) (remain int, err error) {
|
||||
if remain, err = readInt16(r, size, &t.ErrorCode); err != nil {
|
||||
return
|
||||
}
|
||||
if remain, err = readInt32(r, remain, &t.GenerationID); err != nil {
|
||||
return
|
||||
}
|
||||
if remain, err = readString(r, remain, &t.GroupProtocol); err != nil {
|
||||
return
|
||||
}
|
||||
if remain, err = readString(r, remain, &t.LeaderID); err != nil {
|
||||
return
|
||||
}
|
||||
if remain, err = readString(r, remain, &t.MemberID); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
fn := func(r *bufio.Reader, size int) (fnRemain int, fnErr error) {
|
||||
var item joinGroupResponseMemberV1
|
||||
if fnRemain, fnErr = (&item).readFrom(r, size); fnErr != nil {
|
||||
return
|
||||
}
|
||||
t.Members = append(t.Members, item)
|
||||
return
|
||||
}
|
||||
if remain, err = readArrayWith(r, remain, fn); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
+42
@@ -0,0 +1,42 @@
|
||||
package kafka
|
||||
|
||||
import "bufio"
|
||||
|
||||
type leaveGroupRequestV0 struct {
|
||||
// GroupID holds the unique group identifier
|
||||
GroupID string
|
||||
|
||||
// MemberID assigned by the group coordinator or the zero string if joining
|
||||
// for the first time.
|
||||
MemberID string
|
||||
}
|
||||
|
||||
func (t leaveGroupRequestV0) size() int32 {
|
||||
return sizeofString(t.GroupID) +
|
||||
sizeofString(t.MemberID)
|
||||
}
|
||||
|
||||
func (t leaveGroupRequestV0) writeTo(w *bufio.Writer) {
|
||||
writeString(w, t.GroupID)
|
||||
writeString(w, t.MemberID)
|
||||
}
|
||||
|
||||
type leaveGroupResponseV0 struct {
|
||||
// ErrorCode holds response error code
|
||||
ErrorCode int16
|
||||
}
|
||||
|
||||
func (t leaveGroupResponseV0) size() int32 {
|
||||
return sizeofInt16(t.ErrorCode)
|
||||
}
|
||||
|
||||
func (t leaveGroupResponseV0) writeTo(w *bufio.Writer) {
|
||||
writeInt16(w, t.ErrorCode)
|
||||
}
|
||||
|
||||
func (t *leaveGroupResponseV0) readFrom(r *bufio.Reader, size int) (remain int, err error) {
|
||||
if remain, err = readInt16(r, size, &t.ErrorCode); err != nil {
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
+87
@@ -0,0 +1,87 @@
|
||||
package kafka
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
)
|
||||
|
||||
type listGroupsRequestV1 struct {
|
||||
}
|
||||
|
||||
func (t listGroupsRequestV1) size() int32 {
|
||||
return 0
|
||||
}
|
||||
|
||||
func (t listGroupsRequestV1) writeTo(w *bufio.Writer) {
|
||||
}
|
||||
|
||||
type ListGroupsResponseGroupV1 struct {
|
||||
// GroupID holds the unique group identifier
|
||||
GroupID string
|
||||
ProtocolType string
|
||||
}
|
||||
|
||||
func (t ListGroupsResponseGroupV1) size() int32 {
|
||||
return sizeofString(t.GroupID) +
|
||||
sizeofString(t.ProtocolType)
|
||||
}
|
||||
|
||||
func (t ListGroupsResponseGroupV1) writeTo(w *bufio.Writer) {
|
||||
writeString(w, t.GroupID)
|
||||
writeString(w, t.ProtocolType)
|
||||
}
|
||||
|
||||
func (t *ListGroupsResponseGroupV1) readFrom(r *bufio.Reader, size int) (remain int, err error) {
|
||||
if remain, err = readString(r, size, &t.GroupID); err != nil {
|
||||
return
|
||||
}
|
||||
if remain, err = readString(r, remain, &t.ProtocolType); err != nil {
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
type listGroupsResponseV1 struct {
|
||||
// ThrottleTimeMS holds the duration in milliseconds for which the request
|
||||
// was throttled due to quota violation (Zero if the request did not violate
|
||||
// any quota)
|
||||
ThrottleTimeMS int32
|
||||
|
||||
// ErrorCode holds response error code
|
||||
ErrorCode int16
|
||||
Groups []ListGroupsResponseGroupV1
|
||||
}
|
||||
|
||||
func (t listGroupsResponseV1) size() int32 {
|
||||
return sizeofInt32(t.ThrottleTimeMS) +
|
||||
sizeofInt16(t.ErrorCode) +
|
||||
sizeofArray(len(t.Groups), func(i int) int32 { return t.Groups[i].size() })
|
||||
}
|
||||
|
||||
func (t listGroupsResponseV1) writeTo(w *bufio.Writer) {
|
||||
writeInt32(w, t.ThrottleTimeMS)
|
||||
writeInt16(w, t.ErrorCode)
|
||||
writeArray(w, len(t.Groups), func(i int) { t.Groups[i].writeTo(w) })
|
||||
}
|
||||
|
||||
func (t *listGroupsResponseV1) readFrom(r *bufio.Reader, size int) (remain int, err error) {
|
||||
if remain, err = readInt32(r, size, &t.ThrottleTimeMS); err != nil {
|
||||
return
|
||||
}
|
||||
if remain, err = readInt16(r, remain, &t.ErrorCode); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
fn := func(withReader *bufio.Reader, withSize int) (fnRemain int, fnErr error) {
|
||||
var item ListGroupsResponseGroupV1
|
||||
if fnRemain, fnErr = (&item).readFrom(withReader, withSize); err != nil {
|
||||
return
|
||||
}
|
||||
t.Groups = append(t.Groups, item)
|
||||
return
|
||||
}
|
||||
if remain, err = readArrayWith(r, remain, fn); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
+105
@@ -0,0 +1,105 @@
|
||||
package kafka
|
||||
|
||||
import "bufio"
|
||||
|
||||
type listOffsetRequestV1 struct {
|
||||
ReplicaID int32
|
||||
Topics []listOffsetRequestTopicV1
|
||||
}
|
||||
|
||||
func (r listOffsetRequestV1) size() int32 {
|
||||
return 4 + sizeofArray(len(r.Topics), func(i int) int32 { return r.Topics[i].size() })
|
||||
}
|
||||
|
||||
func (r listOffsetRequestV1) writeTo(w *bufio.Writer) {
|
||||
writeInt32(w, r.ReplicaID)
|
||||
writeArray(w, len(r.Topics), func(i int) { r.Topics[i].writeTo(w) })
|
||||
}
|
||||
|
||||
type listOffsetRequestTopicV1 struct {
|
||||
TopicName string
|
||||
Partitions []listOffsetRequestPartitionV1
|
||||
}
|
||||
|
||||
func (t listOffsetRequestTopicV1) size() int32 {
|
||||
return sizeofString(t.TopicName) +
|
||||
sizeofArray(len(t.Partitions), func(i int) int32 { return t.Partitions[i].size() })
|
||||
}
|
||||
|
||||
func (t listOffsetRequestTopicV1) writeTo(w *bufio.Writer) {
|
||||
writeString(w, t.TopicName)
|
||||
writeArray(w, len(t.Partitions), func(i int) { t.Partitions[i].writeTo(w) })
|
||||
}
|
||||
|
||||
type listOffsetRequestPartitionV1 struct {
|
||||
Partition int32
|
||||
Time int64
|
||||
}
|
||||
|
||||
func (p listOffsetRequestPartitionV1) size() int32 {
|
||||
return 4 + 8
|
||||
}
|
||||
|
||||
func (p listOffsetRequestPartitionV1) writeTo(w *bufio.Writer) {
|
||||
writeInt32(w, p.Partition)
|
||||
writeInt64(w, p.Time)
|
||||
}
|
||||
|
||||
type listOffsetResponseV1 []listOffsetResponseTopicV1
|
||||
|
||||
func (r listOffsetResponseV1) size() int32 {
|
||||
return sizeofArray(len(r), func(i int) int32 { return r[i].size() })
|
||||
}
|
||||
|
||||
func (r listOffsetResponseV1) writeTo(w *bufio.Writer) {
|
||||
writeArray(w, len(r), func(i int) { r[i].writeTo(w) })
|
||||
}
|
||||
|
||||
type listOffsetResponseTopicV1 struct {
|
||||
TopicName string
|
||||
PartitionOffsets []partitionOffsetV1
|
||||
}
|
||||
|
||||
func (t listOffsetResponseTopicV1) size() int32 {
|
||||
return sizeofString(t.TopicName) +
|
||||
sizeofArray(len(t.PartitionOffsets), func(i int) int32 { return t.PartitionOffsets[i].size() })
|
||||
}
|
||||
|
||||
func (t listOffsetResponseTopicV1) writeTo(w *bufio.Writer) {
|
||||
writeString(w, t.TopicName)
|
||||
writeArray(w, len(t.PartitionOffsets), func(i int) { t.PartitionOffsets[i].writeTo(w) })
|
||||
}
|
||||
|
||||
type partitionOffsetV1 struct {
|
||||
Partition int32
|
||||
ErrorCode int16
|
||||
Timestamp int64
|
||||
Offset int64
|
||||
}
|
||||
|
||||
func (p partitionOffsetV1) size() int32 {
|
||||
return 4 + 2 + 8 + 8
|
||||
}
|
||||
|
||||
func (p partitionOffsetV1) writeTo(w *bufio.Writer) {
|
||||
writeInt32(w, p.Partition)
|
||||
writeInt16(w, p.ErrorCode)
|
||||
writeInt64(w, p.Timestamp)
|
||||
writeInt64(w, p.Offset)
|
||||
}
|
||||
|
||||
func (p *partitionOffsetV1) readFrom(r *bufio.Reader, sz int) (remain int, err error) {
|
||||
if remain, err = readInt32(r, sz, &p.Partition); err != nil {
|
||||
return
|
||||
}
|
||||
if remain, err = readInt16(r, remain, &p.ErrorCode); err != nil {
|
||||
return
|
||||
}
|
||||
if remain, err = readInt64(r, remain, &p.Timestamp); err != nil {
|
||||
return
|
||||
}
|
||||
if remain, err = readInt64(r, remain, &p.Offset); err != nil {
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
+253
@@ -0,0 +1,253 @@
|
||||
package kafka
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Message is a data structure representing kafka messages.
|
||||
type Message struct {
|
||||
// Topic is reads only and MUST NOT be set when writing messages
|
||||
Topic string
|
||||
|
||||
// Partition is reads only and MUST NOT be set when writing messages
|
||||
Partition int
|
||||
Offset int64
|
||||
Key []byte
|
||||
Value []byte
|
||||
|
||||
// If not set at the creation, Time will be automatically set when
|
||||
// writing the message.
|
||||
Time time.Time
|
||||
}
|
||||
|
||||
func (msg Message) item() messageSetItem {
|
||||
item := messageSetItem{
|
||||
Offset: msg.Offset,
|
||||
Message: msg.message(),
|
||||
}
|
||||
item.MessageSize = item.Message.size()
|
||||
return item
|
||||
}
|
||||
|
||||
func (msg Message) message() message {
|
||||
m := message{
|
||||
MagicByte: 1,
|
||||
Key: msg.Key,
|
||||
Value: msg.Value,
|
||||
Timestamp: timestamp(msg.Time),
|
||||
}
|
||||
m.CRC = m.crc32()
|
||||
return m
|
||||
}
|
||||
|
||||
type message struct {
|
||||
CRC int32
|
||||
MagicByte int8
|
||||
Attributes int8
|
||||
Timestamp int64
|
||||
Key []byte
|
||||
Value []byte
|
||||
}
|
||||
|
||||
func (m message) crc32() int32 {
|
||||
return int32(crc32OfMessage(m.MagicByte, m.Attributes, m.Timestamp, m.Key, m.Value))
|
||||
}
|
||||
|
||||
func (m message) size() int32 {
|
||||
size := 4 + 1 + 1 + sizeofBytes(m.Key) + sizeofBytes(m.Value)
|
||||
if m.MagicByte != 0 {
|
||||
size += 8 // Timestamp
|
||||
}
|
||||
return size
|
||||
}
|
||||
|
||||
func (m message) writeTo(w *bufio.Writer) {
|
||||
writeInt32(w, m.CRC)
|
||||
writeInt8(w, m.MagicByte)
|
||||
writeInt8(w, m.Attributes)
|
||||
if m.MagicByte != 0 {
|
||||
writeInt64(w, m.Timestamp)
|
||||
}
|
||||
writeBytes(w, m.Key)
|
||||
writeBytes(w, m.Value)
|
||||
}
|
||||
|
||||
type messageSetItem struct {
|
||||
Offset int64
|
||||
MessageSize int32
|
||||
Message message
|
||||
}
|
||||
|
||||
func (m messageSetItem) size() int32 {
|
||||
return 8 + 4 + m.Message.size()
|
||||
}
|
||||
|
||||
func (m messageSetItem) writeTo(w *bufio.Writer) {
|
||||
writeInt64(w, m.Offset)
|
||||
writeInt32(w, m.MessageSize)
|
||||
m.Message.writeTo(w)
|
||||
}
|
||||
|
||||
type messageSet []messageSetItem
|
||||
|
||||
func (s messageSet) size() (size int32) {
|
||||
for _, m := range s {
|
||||
size += m.size()
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (s messageSet) writeTo(w *bufio.Writer) {
|
||||
for _, m := range s {
|
||||
m.writeTo(w)
|
||||
}
|
||||
}
|
||||
|
||||
type messageSetReader struct {
|
||||
*readerStack
|
||||
}
|
||||
|
||||
type readerStack struct {
|
||||
reader *bufio.Reader
|
||||
remain int
|
||||
base int64
|
||||
parent *readerStack
|
||||
}
|
||||
|
||||
func newMessageSetReader(reader *bufio.Reader, remain int) *messageSetReader {
|
||||
return &messageSetReader{&readerStack{
|
||||
reader: reader,
|
||||
remain: remain,
|
||||
}}
|
||||
}
|
||||
|
||||
func (r *messageSetReader) readMessage(min int64,
|
||||
key func(*bufio.Reader, int, int) (int, error),
|
||||
val func(*bufio.Reader, int, int) (int, error),
|
||||
) (offset int64, timestamp int64, err error) {
|
||||
for r.readerStack != nil {
|
||||
if r.remain == 0 {
|
||||
r.readerStack = r.parent
|
||||
continue
|
||||
}
|
||||
|
||||
var attributes int8
|
||||
if offset, attributes, timestamp, r.remain, err = readMessageHeader(r.reader, r.remain); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// if the message is compressed, decompress it and push a new reader
|
||||
// onto the stack.
|
||||
code := attributes & compressionCodecMask
|
||||
if code != 0 {
|
||||
var codec CompressionCodec
|
||||
if codec, err = resolveCodec(attributes); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// discard next four bytes...will be -1 to indicate null key
|
||||
if r.remain, err = discardN(r.reader, r.remain, 4); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// read and decompress the contained message set.
|
||||
var decompressed []byte
|
||||
if r.remain, err = readBytesWith(r.reader, r.remain, func(r *bufio.Reader, sz, n int) (remain int, err error) {
|
||||
var value []byte
|
||||
if value, remain, err = readNewBytes(r, sz, n); err != nil {
|
||||
return
|
||||
}
|
||||
decompressed, err = codec.Decode(value)
|
||||
return
|
||||
}); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// the compressed message's offset will be equal to the offset of
|
||||
// the last message in the set. within the compressed set, the
|
||||
// offsets will be relative, so we have to scan through them to
|
||||
// get the base offset. for example, if there are four compressed
|
||||
// messages at offsets 10-13, then the container message will have
|
||||
// offset 13 and the contained messages will be 0,1,2,3. the base
|
||||
// offset for the container, then is 13-3=10.
|
||||
if offset, err = extractOffset(offset, decompressed); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
r.readerStack = &readerStack{
|
||||
reader: bufio.NewReader(bytes.NewReader(decompressed)),
|
||||
remain: len(decompressed),
|
||||
base: offset,
|
||||
parent: r.readerStack,
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
// adjust the offset in case we're reading compressed messages. the
|
||||
// base will be zero otherwise.
|
||||
offset += r.base
|
||||
|
||||
// When the messages are compressed kafka may return messages at an
|
||||
// earlier offset than the one that was requested, it's the client's
|
||||
// responsibility to ignore those.
|
||||
if offset < min {
|
||||
if r.remain, err = discardBytes(r.reader, r.remain); err != nil {
|
||||
return
|
||||
}
|
||||
if r.remain, err = discardBytes(r.reader, r.remain); err != nil {
|
||||
return
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
if r.remain, err = readBytesWith(r.reader, r.remain, key); err != nil {
|
||||
return
|
||||
}
|
||||
r.remain, err = readBytesWith(r.reader, r.remain, val)
|
||||
return
|
||||
}
|
||||
|
||||
err = errShortRead
|
||||
return
|
||||
}
|
||||
|
||||
func (r *messageSetReader) remaining() (remain int) {
|
||||
for s := r.readerStack; s != nil; s = s.parent {
|
||||
remain += s.remain
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (r *messageSetReader) discard() (err error) {
|
||||
if r.readerStack == nil {
|
||||
return
|
||||
}
|
||||
// rewind up to the top-most reader b/c it's the only one that's doing
|
||||
// actual i/o. the rest are byte buffers that have been pushed on the stack
|
||||
// while reading compressed message sets.
|
||||
for r.parent != nil {
|
||||
r.readerStack = r.parent
|
||||
}
|
||||
r.remain, err = discardN(r.reader, r.remain, r.remain)
|
||||
return
|
||||
}
|
||||
|
||||
func extractOffset(base int64, msgSet []byte) (offset int64, err error) {
|
||||
r, remain := bufio.NewReader(bytes.NewReader(msgSet)), len(msgSet)
|
||||
for remain > 0 {
|
||||
if remain, err = readInt64(r, remain, &offset); err != nil {
|
||||
return
|
||||
}
|
||||
var sz int32
|
||||
if remain, err = readInt32(r, remain, &sz); err != nil {
|
||||
return
|
||||
}
|
||||
if remain, err = discardN(r, remain, int(sz)); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
offset = base - offset
|
||||
return
|
||||
}
|
||||
+89
@@ -0,0 +1,89 @@
|
||||
package kafka
|
||||
|
||||
import "bufio"
|
||||
|
||||
type topicMetadataRequestV1 []string
|
||||
|
||||
func (r topicMetadataRequestV1) size() int32 {
|
||||
return sizeofStringArray([]string(r))
|
||||
}
|
||||
|
||||
func (r topicMetadataRequestV1) writeTo(w *bufio.Writer) {
|
||||
writeStringArray(w, []string(r))
|
||||
}
|
||||
|
||||
type metadataResponseV1 struct {
|
||||
Brokers []brokerMetadataV1
|
||||
ControllerID int32
|
||||
Topics []topicMetadataV1
|
||||
}
|
||||
|
||||
func (r metadataResponseV1) size() int32 {
|
||||
n1 := sizeofArray(len(r.Brokers), func(i int) int32 { return r.Brokers[i].size() })
|
||||
n2 := sizeofArray(len(r.Topics), func(i int) int32 { return r.Topics[i].size() })
|
||||
return 4 + n1 + n2
|
||||
}
|
||||
|
||||
func (r metadataResponseV1) writeTo(w *bufio.Writer) {
|
||||
writeArray(w, len(r.Brokers), func(i int) { r.Brokers[i].writeTo(w) })
|
||||
writeInt32(w, r.ControllerID)
|
||||
writeArray(w, len(r.Topics), func(i int) { r.Topics[i].writeTo(w) })
|
||||
}
|
||||
|
||||
type brokerMetadataV1 struct {
|
||||
NodeID int32
|
||||
Host string
|
||||
Port int32
|
||||
Rack string
|
||||
}
|
||||
|
||||
func (b brokerMetadataV1) size() int32 {
|
||||
return 4 + 4 + sizeofString(b.Host) + sizeofString(b.Rack)
|
||||
}
|
||||
|
||||
func (b brokerMetadataV1) writeTo(w *bufio.Writer) {
|
||||
writeInt32(w, b.NodeID)
|
||||
writeString(w, b.Host)
|
||||
writeInt32(w, b.Port)
|
||||
writeString(w, b.Rack)
|
||||
}
|
||||
|
||||
type topicMetadataV1 struct {
|
||||
TopicErrorCode int16
|
||||
TopicName string
|
||||
Internal bool
|
||||
Partitions []partitionMetadataV1
|
||||
}
|
||||
|
||||
func (t topicMetadataV1) size() int32 {
|
||||
return 2 + 1 +
|
||||
sizeofString(t.TopicName) +
|
||||
sizeofArray(len(t.Partitions), func(i int) int32 { return t.Partitions[i].size() })
|
||||
}
|
||||
|
||||
func (t topicMetadataV1) writeTo(w *bufio.Writer) {
|
||||
writeInt16(w, t.TopicErrorCode)
|
||||
writeString(w, t.TopicName)
|
||||
writeBool(w, t.Internal)
|
||||
writeArray(w, len(t.Partitions), func(i int) { t.Partitions[i].writeTo(w) })
|
||||
}
|
||||
|
||||
type partitionMetadataV1 struct {
|
||||
PartitionErrorCode int16
|
||||
PartitionID int32
|
||||
Leader int32
|
||||
Replicas []int32
|
||||
Isr []int32
|
||||
}
|
||||
|
||||
func (p partitionMetadataV1) size() int32 {
|
||||
return 2 + 4 + 4 + sizeofInt32Array(p.Replicas) + sizeofInt32Array(p.Isr)
|
||||
}
|
||||
|
||||
func (p partitionMetadataV1) writeTo(w *bufio.Writer) {
|
||||
writeInt16(w, p.PartitionErrorCode)
|
||||
writeInt32(w, p.PartitionID)
|
||||
writeInt32(w, p.Leader)
|
||||
writeInt32Array(w, p.Replicas)
|
||||
writeInt32Array(w, p.Isr)
|
||||
}
|
||||
+167
@@ -0,0 +1,167 @@
|
||||
package kafka
|
||||
|
||||
import "bufio"
|
||||
|
||||
type offsetCommitRequestV2Partition struct {
|
||||
// Partition ID
|
||||
Partition int32
|
||||
|
||||
// Offset to be committed
|
||||
Offset int64
|
||||
|
||||
// Metadata holds any associated metadata the client wants to keep
|
||||
Metadata string
|
||||
}
|
||||
|
||||
func (t offsetCommitRequestV2Partition) size() int32 {
|
||||
return sizeofInt32(t.Partition) +
|
||||
sizeofInt64(t.Offset) +
|
||||
sizeofString(t.Metadata)
|
||||
}
|
||||
|
||||
func (t offsetCommitRequestV2Partition) writeTo(w *bufio.Writer) {
|
||||
writeInt32(w, t.Partition)
|
||||
writeInt64(w, t.Offset)
|
||||
writeString(w, t.Metadata)
|
||||
}
|
||||
|
||||
type offsetCommitRequestV2Topic struct {
|
||||
// Topic name
|
||||
Topic string
|
||||
|
||||
// Partitions to commit offsets
|
||||
Partitions []offsetCommitRequestV2Partition
|
||||
}
|
||||
|
||||
func (t offsetCommitRequestV2Topic) size() int32 {
|
||||
return sizeofString(t.Topic) +
|
||||
sizeofArray(len(t.Partitions), func(i int) int32 { return t.Partitions[i].size() })
|
||||
}
|
||||
|
||||
func (t offsetCommitRequestV2Topic) writeTo(w *bufio.Writer) {
|
||||
writeString(w, t.Topic)
|
||||
writeArray(w, len(t.Partitions), func(i int) { t.Partitions[i].writeTo(w) })
|
||||
}
|
||||
|
||||
type offsetCommitRequestV2 struct {
|
||||
// GroupID holds the unique group identifier
|
||||
GroupID string
|
||||
|
||||
// GenerationID holds the generation of the group.
|
||||
GenerationID int32
|
||||
|
||||
// MemberID assigned by the group coordinator
|
||||
MemberID string
|
||||
|
||||
// RetentionTime holds the time period in ms to retain the offset.
|
||||
RetentionTime int64
|
||||
|
||||
// Topics to commit offsets
|
||||
Topics []offsetCommitRequestV2Topic
|
||||
}
|
||||
|
||||
func (t offsetCommitRequestV2) size() int32 {
|
||||
return sizeofString(t.GroupID) +
|
||||
sizeofInt32(t.GenerationID) +
|
||||
sizeofString(t.MemberID) +
|
||||
sizeofInt64(t.RetentionTime) +
|
||||
sizeofArray(len(t.Topics), func(i int) int32 { return t.Topics[i].size() })
|
||||
}
|
||||
|
||||
func (t offsetCommitRequestV2) writeTo(w *bufio.Writer) {
|
||||
writeString(w, t.GroupID)
|
||||
writeInt32(w, t.GenerationID)
|
||||
writeString(w, t.MemberID)
|
||||
writeInt64(w, t.RetentionTime)
|
||||
writeArray(w, len(t.Topics), func(i int) { t.Topics[i].writeTo(w) })
|
||||
}
|
||||
|
||||
type offsetCommitResponseV2PartitionResponse struct {
|
||||
Partition int32
|
||||
|
||||
// ErrorCode holds response error code
|
||||
ErrorCode int16
|
||||
}
|
||||
|
||||
func (t offsetCommitResponseV2PartitionResponse) size() int32 {
|
||||
return sizeofInt32(t.Partition) +
|
||||
sizeofInt16(t.ErrorCode)
|
||||
}
|
||||
|
||||
func (t offsetCommitResponseV2PartitionResponse) writeTo(w *bufio.Writer) {
|
||||
writeInt32(w, t.Partition)
|
||||
writeInt16(w, t.ErrorCode)
|
||||
}
|
||||
|
||||
func (t *offsetCommitResponseV2PartitionResponse) readFrom(r *bufio.Reader, size int) (remain int, err error) {
|
||||
if remain, err = readInt32(r, size, &t.Partition); err != nil {
|
||||
return
|
||||
}
|
||||
if remain, err = readInt16(r, remain, &t.ErrorCode); err != nil {
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
type offsetCommitResponseV2Response struct {
|
||||
Topic string
|
||||
PartitionResponses []offsetCommitResponseV2PartitionResponse
|
||||
}
|
||||
|
||||
func (t offsetCommitResponseV2Response) size() int32 {
|
||||
return sizeofString(t.Topic) +
|
||||
sizeofArray(len(t.PartitionResponses), func(i int) int32 { return t.PartitionResponses[i].size() })
|
||||
}
|
||||
|
||||
func (t offsetCommitResponseV2Response) writeTo(w *bufio.Writer) {
|
||||
writeString(w, t.Topic)
|
||||
writeArray(w, len(t.PartitionResponses), func(i int) { t.PartitionResponses[i].writeTo(w) })
|
||||
}
|
||||
|
||||
func (t *offsetCommitResponseV2Response) readFrom(r *bufio.Reader, size int) (remain int, err error) {
|
||||
if remain, err = readString(r, size, &t.Topic); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
fn := func(r *bufio.Reader, withSize int) (fnRemain int, fnErr error) {
|
||||
item := offsetCommitResponseV2PartitionResponse{}
|
||||
if fnRemain, fnErr = (&item).readFrom(r, withSize); fnErr != nil {
|
||||
return
|
||||
}
|
||||
t.PartitionResponses = append(t.PartitionResponses, item)
|
||||
return
|
||||
}
|
||||
if remain, err = readArrayWith(r, remain, fn); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
type offsetCommitResponseV2 struct {
|
||||
Responses []offsetCommitResponseV2Response
|
||||
}
|
||||
|
||||
func (t offsetCommitResponseV2) size() int32 {
|
||||
return sizeofArray(len(t.Responses), func(i int) int32 { return t.Responses[i].size() })
|
||||
}
|
||||
|
||||
func (t offsetCommitResponseV2) writeTo(w *bufio.Writer) {
|
||||
writeArray(w, len(t.Responses), func(i int) { t.Responses[i].writeTo(w) })
|
||||
}
|
||||
|
||||
func (t *offsetCommitResponseV2) readFrom(r *bufio.Reader, size int) (remain int, err error) {
|
||||
fn := func(r *bufio.Reader, withSize int) (fnRemain int, fnErr error) {
|
||||
item := offsetCommitResponseV2Response{}
|
||||
if fnRemain, fnErr = (&item).readFrom(r, withSize); fnErr != nil {
|
||||
return
|
||||
}
|
||||
t.Responses = append(t.Responses, item)
|
||||
return
|
||||
}
|
||||
if remain, err = readArrayWith(r, size, fn); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user