mirror of
https://gitlab.com/ric_harvey/MailHog.git
synced 2024-11-23 14:24:03 +00:00
add vendor back in (again!)
This commit is contained in:
parent
8dc1b9d86e
commit
1bd561aa34
251 changed files with 41538 additions and 0 deletions
27
vendor/github.com/gorilla/context/LICENSE
generated
vendored
Normal file
27
vendor/github.com/gorilla/context/LICENSE
generated
vendored
Normal file
|
@ -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.
|
10
vendor/github.com/gorilla/context/README.md
generated
vendored
Normal file
10
vendor/github.com/gorilla/context/README.md
generated
vendored
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
context
|
||||||
|
=======
|
||||||
|
[![Build Status](https://travis-ci.org/gorilla/context.png?branch=master)](https://travis-ci.org/gorilla/context)
|
||||||
|
|
||||||
|
gorilla/context is a general purpose registry for global request variables.
|
||||||
|
|
||||||
|
> 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()`.
|
||||||
|
|
||||||
|
Read the full documentation here: http://www.gorillatoolkit.org/pkg/context
|
143
vendor/github.com/gorilla/context/context.go
generated
vendored
Normal file
143
vendor/github.com/gorilla/context/context.go
generated
vendored
Normal file
|
@ -0,0 +1,143 @@
|
||||||
|
// 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
|
||||||
|
|
||||||
|
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
vendor/github.com/gorilla/context/doc.go
generated
vendored
Normal file
88
vendor/github.com/gorilla/context/doc.go
generated
vendored
Normal file
|
@ -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
|
27
vendor/github.com/gorilla/mux/LICENSE
generated
vendored
Normal file
27
vendor/github.com/gorilla/mux/LICENSE
generated
vendored
Normal file
|
@ -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.
|
340
vendor/github.com/gorilla/mux/README.md
generated
vendored
Normal file
340
vendor/github.com/gorilla/mux/README.md
generated
vendored
Normal file
|
@ -0,0 +1,340 @@
|
||||||
|
gorilla/mux
|
||||||
|
===
|
||||||
|
[![GoDoc](https://godoc.org/github.com/gorilla/mux?status.svg)](https://godoc.org/github.com/gorilla/mux)
|
||||||
|
[![Build Status](https://travis-ci.org/gorilla/mux.svg?branch=master)](https://travis-ci.org/gorilla/mux)
|
||||||
|
[![Sourcegraph](https://sourcegraph.com/github.com/gorilla/mux/-/badge.svg)](https://sourcegraph.com/github.com/gorilla/mux?badge)
|
||||||
|
|
||||||
|
![Gorilla Logo](http://www.gorillatoolkit.org/static/images/gorilla-icon-64.png)
|
||||||
|
|
||||||
|
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 and paths 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)
|
||||||
|
* [Listing Routes](#listing-routes)
|
||||||
|
* [Static Files](#static-files)
|
||||||
|
* [Registered URLs](#registered-urls)
|
||||||
|
* [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")
|
||||||
|
```
|
||||||
|
|
||||||
|
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)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Listing Routes
|
||||||
|
|
||||||
|
Routes on a mux can be listed using the Router.Walk method—useful for generating documentation:
|
||||||
|
|
||||||
|
```go
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"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)
|
||||||
|
r.HandleFunc("/articles", handler)
|
||||||
|
r.HandleFunc("/articles/{id}", handler)
|
||||||
|
r.Walk(func(route *mux.Route, router *mux.Router, ancestors []*mux.Route) error {
|
||||||
|
t, err := route.GetPathTemplate()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
fmt.Println(t)
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
http.Handle("/", r)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 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 variables:
|
||||||
|
|
||||||
|
```go
|
||||||
|
r := mux.NewRouter()
|
||||||
|
r.Host("{subdomain}.domain.com").
|
||||||
|
Path("/articles/{category}/{id:[0-9]+}").
|
||||||
|
HandlerFunc(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. 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")
|
||||||
|
```
|
||||||
|
|
||||||
|
## 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
vendor/github.com/gorilla/mux/context_gorilla.go
generated
vendored
Normal file
26
vendor/github.com/gorilla/mux/context_gorilla.go
generated
vendored
Normal file
|
@ -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
vendor/github.com/gorilla/mux/context_native.go
generated
vendored
Normal file
24
vendor/github.com/gorilla/mux/context_native.go
generated
vendored
Normal file
|
@ -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
|
||||||
|
}
|
240
vendor/github.com/gorilla/mux/doc.go
generated
vendored
Normal file
240
vendor/github.com/gorilla/mux/doc.go
generated
vendored
Normal file
|
@ -0,0 +1,240 @@
|
||||||
|
// 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 and paths 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 variables:
|
||||||
|
|
||||||
|
r := mux.NewRouter()
|
||||||
|
r.Host("{subdomain}.domain.com").
|
||||||
|
Path("/articles/{category}/{id:[0-9]+}").
|
||||||
|
HandlerFunc(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. 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")
|
||||||
|
*/
|
||||||
|
package mux
|
542
vendor/github.com/gorilla/mux/mux.go
generated
vendored
Normal file
542
vendor/github.com/gorilla/mux/mux.go
generated
vendored
Normal file
|
@ -0,0 +1,542 @@
|
||||||
|
// 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"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// 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
|
||||||
|
// 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
|
||||||
|
}
|
||||||
|
|
||||||
|
// Match matches registered routes against the request.
|
||||||
|
func (r *Router) Match(req *http.Request, match *RouteMatch) bool {
|
||||||
|
for _, route := range r.routes {
|
||||||
|
if route.Match(req, match) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Closest match for a router (includes sub-routers)
|
||||||
|
if r.NotFoundHandler != nil {
|
||||||
|
match.Handler = r.NotFoundHandler
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
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 = getPath(req)
|
||||||
|
}
|
||||||
|
// 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 {
|
||||||
|
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 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.
|
||||||
|
//
|
||||||
|
// 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".
|
||||||
|
// This behavior has the drawback of needing to match routes against
|
||||||
|
// r.RequestURI instead of r.URL.Path. Any modifications (such as http.StripPrefix)
|
||||||
|
// to r.URL.Path will not affect routing when this flag is on and thus may
|
||||||
|
// induce unintended behavior.
|
||||||
|
//
|
||||||
|
// 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
|
||||||
|
// ----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
// 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 {
|
||||||
|
if t.regexp == nil || t.regexp.path == nil || t.regexp.path.template == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
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 {
|
||||||
|
err := h.walk(walkFn, ancestors)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
// ----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
// getPath returns the escaped path if possible; doing what URL.EscapedPath()
|
||||||
|
// which was added in go1.5 does
|
||||||
|
func getPath(req *http.Request) string {
|
||||||
|
if req.RequestURI != "" {
|
||||||
|
// Extract the path from RequestURI (which is escaped unlike URL.Path)
|
||||||
|
// as detailed here as detailed in https://golang.org/pkg/net/url/#URL
|
||||||
|
// for < 1.5 server side workaround
|
||||||
|
// http://localhost/path/here?v=1 -> /path/here
|
||||||
|
path := req.RequestURI
|
||||||
|
path = strings.TrimPrefix(path, req.URL.Scheme+`://`)
|
||||||
|
path = strings.TrimPrefix(path, req.URL.Host)
|
||||||
|
if i := strings.LastIndex(path, "?"); i > -1 {
|
||||||
|
path = path[:i]
|
||||||
|
}
|
||||||
|
if i := strings.LastIndex(path, "#"); i > -1 {
|
||||||
|
path = path[:i]
|
||||||
|
}
|
||||||
|
return path
|
||||||
|
}
|
||||||
|
return req.URL.Path
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 paramers 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
|
||||||
|
}
|
323
vendor/github.com/gorilla/mux/regexp.go
generated
vendored
Normal file
323
vendor/github.com/gorilla/mux/regexp.go
generated
vendored
Normal file
|
@ -0,0 +1,323 @@
|
||||||
|
// 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"
|
||||||
|
)
|
||||||
|
|
||||||
|
// 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, matchHost, matchPrefix, matchQuery, strictSlash, useEncodedPath bool) (*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 matchQuery {
|
||||||
|
defaultPattern = "[^?&]*"
|
||||||
|
} else if matchHost {
|
||||||
|
defaultPattern = "[^.]+"
|
||||||
|
matchPrefix = false
|
||||||
|
}
|
||||||
|
// Only match strict slash if not matching
|
||||||
|
if matchPrefix || matchHost || matchQuery {
|
||||||
|
strictSlash = false
|
||||||
|
}
|
||||||
|
// Set a flag for strictSlash.
|
||||||
|
endSlash := false
|
||||||
|
if 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 strictSlash {
|
||||||
|
pattern.WriteString("[/]?")
|
||||||
|
}
|
||||||
|
if matchQuery {
|
||||||
|
// Add the default pattern if the query value is empty
|
||||||
|
if queryVal := strings.SplitN(template, "=", 2)[1]; queryVal == "" {
|
||||||
|
pattern.WriteString(defaultPattern)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !matchPrefix {
|
||||||
|
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,
|
||||||
|
matchHost: matchHost,
|
||||||
|
matchQuery: matchQuery,
|
||||||
|
strictSlash: strictSlash,
|
||||||
|
useEncodedPath: useEncodedPath,
|
||||||
|
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
|
||||||
|
// True for host match, false for path or query string match.
|
||||||
|
matchHost bool
|
||||||
|
// True for query string match, false for path and host match.
|
||||||
|
matchQuery bool
|
||||||
|
// The strictSlash value defined on the route, but disabled if PathPrefix was used.
|
||||||
|
strictSlash bool
|
||||||
|
// Determines whether to use encoded path from getPath function or unencoded
|
||||||
|
// req.URL.Path for path matching
|
||||||
|
useEncodedPath bool
|
||||||
|
// 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.matchHost {
|
||||||
|
if r.matchQuery {
|
||||||
|
return r.matchQueryString(req)
|
||||||
|
}
|
||||||
|
path := req.URL.Path
|
||||||
|
if r.useEncodedPath {
|
||||||
|
path = getPath(req)
|
||||||
|
}
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
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.matchQuery {
|
||||||
|
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 = getPath(req)
|
||||||
|
}
|
||||||
|
// 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.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]]
|
||||||
|
}
|
||||||
|
}
|
636
vendor/github.com/gorilla/mux/route.go
generated
vendored
Normal file
636
vendor/github.com/gorilla/mux/route.go
generated
vendored
Normal file
|
@ -0,0 +1,636 @@
|
||||||
|
// 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
|
||||||
|
// 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
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
// Match everything.
|
||||||
|
for _, m := range r.matchers {
|
||||||
|
if matched := m.Match(req, match); !matched {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 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, matchHost, matchPrefix, matchQuery bool) error {
|
||||||
|
if r.err != nil {
|
||||||
|
return r.err
|
||||||
|
}
|
||||||
|
r.regexp = r.getRegexpGroup()
|
||||||
|
if !matchHost && !matchQuery {
|
||||||
|
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, matchHost, matchPrefix, matchQuery, r.strictSlash, 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 matchHost {
|
||||||
|
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 matchQuery {
|
||||||
|
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.
|
||||||
|
// It the value is an empty string, it will match any value if the key is set.
|
||||||
|
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, true, false, false)
|
||||||
|
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, false, false, false)
|
||||||
|
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, false, true, false)
|
||||||
|
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], false, false, true); 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)
|
||||||
|
}
|
||||||
|
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
|
||||||
|
if r.regexp.host != nil {
|
||||||
|
// Set a default scheme.
|
||||||
|
scheme = "http"
|
||||||
|
if host, err = r.regexp.host.url(values); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if r.regexp.path != nil {
|
||||||
|
if path, err = r.regexp.path.url(values); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return &url.URL{
|
||||||
|
Scheme: scheme,
|
||||||
|
Host: host,
|
||||||
|
Path: path,
|
||||||
|
}, 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
|
||||||
|
}
|
||||||
|
return &url.URL{
|
||||||
|
Scheme: "http",
|
||||||
|
Host: host,
|
||||||
|
}, 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
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 {
|
||||||
|
getNamedRoutes() map[string]*Route
|
||||||
|
getRegexpGroup() *routeRegexpGroup
|
||||||
|
buildVars(map[string]string) map[string]string
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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
|
||||||
|
}
|
27
vendor/github.com/gorilla/pat/LICENSE
generated
vendored
Normal file
27
vendor/github.com/gorilla/pat/LICENSE
generated
vendored
Normal file
|
@ -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.
|
70
vendor/github.com/gorilla/pat/README.md
generated
vendored
Normal file
70
vendor/github.com/gorilla/pat/README.md
generated
vendored
Normal file
|
@ -0,0 +1,70 @@
|
||||||
|
pat
|
||||||
|
===
|
||||||
|
[![GoDoc](https://godoc.org/github.com/gorilla/pat?status.svg)](https://godoc.org/github.com/gorilla/pat)
|
||||||
|
[![Build Status](https://travis-ci.org/gorilla/pat.svg?branch=master)](https://travis-ci.org/gorilla/pat)
|
||||||
|
|
||||||
|
### How to use?
|
||||||
|
|
||||||
|
pat is pretty simple. The documentation lives [here](http://www.gorillatoolkit.org/pkg/pat).
|
||||||
|
|
||||||
|
### Install
|
||||||
|
With a properly configured Go toolchain:
|
||||||
|
```sh
|
||||||
|
go get github.com/gorilla/pat
|
||||||
|
```
|
||||||
|
|
||||||
|
### Example
|
||||||
|
|
||||||
|
Here's an example of a RESTful api:
|
||||||
|
|
||||||
|
```go
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/gorilla/pat"
|
||||||
|
)
|
||||||
|
|
||||||
|
func homeHandler(wr http.ResponseWriter, req *http.Request) {
|
||||||
|
wr.WriteHeader(http.StatusOK)
|
||||||
|
wr.Write([]byte("Yay! We're home, Jim!"))
|
||||||
|
}
|
||||||
|
|
||||||
|
func getAllTheThings(wr http.ResponseWriter, req *http.Request) {
|
||||||
|
wr.WriteHeader(http.StatusOK)
|
||||||
|
wr.Write([]byte("Look, Jim! Get all the things!"))
|
||||||
|
}
|
||||||
|
|
||||||
|
func putOneThing(wr http.ResponseWriter, req *http.Request) {
|
||||||
|
wr.WriteHeader(http.StatusOK)
|
||||||
|
wr.Write([]byte("Look, Jim! Put one thing!"))
|
||||||
|
}
|
||||||
|
|
||||||
|
func deleteOneThing(wr http.ResponseWriter, req *http.Request) {
|
||||||
|
wr.WriteHeader(http.StatusOK)
|
||||||
|
wr.Write([]byte("Look, Jim! Delete one thing!"))
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
router := pat.New()
|
||||||
|
|
||||||
|
router.Get("/things", getAllTheThings)
|
||||||
|
|
||||||
|
router.Put("/things/{id}", putOneThing)
|
||||||
|
router.Delete("/things/{id}", deleteOneThing)
|
||||||
|
|
||||||
|
router.Get("/", homeHandler)
|
||||||
|
|
||||||
|
http.Handle("/", router)
|
||||||
|
|
||||||
|
log.Print("Listening on 127.0.0.1:8000...")
|
||||||
|
log.Fatal(http.ListenAndServe(":8000", nil))
|
||||||
|
}
|
||||||
|
```
|
||||||
|
Notice how the routes descend? That's because Pat will take the first route
|
||||||
|
that matches. For your own testing, take the line ```router.Get("/",
|
||||||
|
homeHandler)``` and put it above the other routes and run the example. When you
|
||||||
|
try to curl any of the routes, you'll only get what the homeHandler returns.
|
||||||
|
Design your routes carefully.
|
67
vendor/github.com/gorilla/pat/doc.go
generated
vendored
Normal file
67
vendor/github.com/gorilla/pat/doc.go
generated
vendored
Normal file
|
@ -0,0 +1,67 @@
|
||||||
|
// 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 gorilla/pat is a request router and dispatcher with a pat-like
|
||||||
|
interface. It is an alternative to gorilla/mux that showcases how it can
|
||||||
|
be used as a base for different API flavors. Package pat is documented at:
|
||||||
|
|
||||||
|
http://godoc.org/github.com/bmizerany/pat
|
||||||
|
|
||||||
|
Let's start registering a couple of URL paths and handlers:
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
r := pat.New()
|
||||||
|
r.Get("/products", ProductsHandler)
|
||||||
|
r.Get("/articles", ArticlesHandler)
|
||||||
|
r.Get("/", HomeHandler)
|
||||||
|
http.Handle("/", r)
|
||||||
|
}
|
||||||
|
|
||||||
|
Here we register three routes mapping URL paths to handlers. This is
|
||||||
|
equivalent to how http.HandleFunc() works: if an incoming GET request matches
|
||||||
|
one of the paths, the corresponding handler is called passing
|
||||||
|
(http.ResponseWriter, *http.Request) as parameters.
|
||||||
|
|
||||||
|
Note: gorilla/pat matches path prefixes, so you must register the most
|
||||||
|
specific paths first.
|
||||||
|
|
||||||
|
Note: differently from pat, these methods accept a handler function, and not an
|
||||||
|
http.Handler. We think this is shorter and more convenient. To set an
|
||||||
|
http.Handler, use the Add() method.
|
||||||
|
|
||||||
|
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 := pat.New()
|
||||||
|
r.Get("/articles/{category}/{id:[0-9]+}", ArticleHandler)
|
||||||
|
r.Get("/articles/{category}/", ArticlesCategoryHandler)
|
||||||
|
r.Get("/products/{key}", ProductHandler)
|
||||||
|
|
||||||
|
The names are used to create a map of route variables which are stored in the
|
||||||
|
URL query, prefixed by a colon:
|
||||||
|
|
||||||
|
category := req.URL.Query().Get(":category")
|
||||||
|
|
||||||
|
As in the gorilla/mux package, other matchers can be added to the registered
|
||||||
|
routes and URLs can be reversed as well. To build a URL for a route, first
|
||||||
|
add a name to it:
|
||||||
|
|
||||||
|
r.Get("/products/{key}", ProductHandler).Name("product")
|
||||||
|
|
||||||
|
Then you can get it using the name and generate a URL:
|
||||||
|
|
||||||
|
url, err := r.GetRoute("product").URL("key", "transmogrifier")
|
||||||
|
|
||||||
|
...and the result will be a url.URL with the following path:
|
||||||
|
|
||||||
|
"/products/transmogrifier"
|
||||||
|
|
||||||
|
Check the mux documentation for more details about URL building and extra
|
||||||
|
matchers:
|
||||||
|
|
||||||
|
http://gorilla-web.appspot.com/pkg/mux/
|
||||||
|
*/
|
||||||
|
package pat
|
126
vendor/github.com/gorilla/pat/pat.go
generated
vendored
Normal file
126
vendor/github.com/gorilla/pat/pat.go
generated
vendored
Normal file
|
@ -0,0 +1,126 @@
|
||||||
|
// 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 pat
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"path"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/gorilla/context"
|
||||||
|
"github.com/gorilla/mux"
|
||||||
|
)
|
||||||
|
|
||||||
|
// New returns a new router.
|
||||||
|
func New() *Router {
|
||||||
|
return &Router{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Router is a request router that implements a pat-like API.
|
||||||
|
//
|
||||||
|
// pat docs: http://godoc.org/github.com/bmizerany/pat
|
||||||
|
type Router struct {
|
||||||
|
mux.Router
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add registers a pattern with a handler for the given request method.
|
||||||
|
func (r *Router) Add(meth, pat string, h http.Handler) *mux.Route {
|
||||||
|
return r.NewRoute().PathPrefix(pat).Handler(h).Methods(meth)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Options registers a pattern with a handler for OPTIONS requests.
|
||||||
|
func (r *Router) Options(pat string, h http.HandlerFunc) *mux.Route {
|
||||||
|
return r.Add("OPTIONS", pat, h)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete registers a pattern with a handler for DELETE requests.
|
||||||
|
func (r *Router) Delete(pat string, h http.HandlerFunc) *mux.Route {
|
||||||
|
return r.Add("DELETE", pat, h)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Head registers a pattern with a handler for HEAD requests.
|
||||||
|
func (r *Router) Head(pat string, h http.HandlerFunc) *mux.Route {
|
||||||
|
return r.Add("HEAD", pat, h)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get registers a pattern with a handler for GET requests.
|
||||||
|
func (r *Router) Get(pat string, h http.HandlerFunc) *mux.Route {
|
||||||
|
return r.Add("GET", pat, h)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Post registers a pattern with a handler for POST requests.
|
||||||
|
func (r *Router) Post(pat string, h http.HandlerFunc) *mux.Route {
|
||||||
|
return r.Add("POST", pat, h)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Put registers a pattern with a handler for PUT requests.
|
||||||
|
func (r *Router) Put(pat string, h http.HandlerFunc) *mux.Route {
|
||||||
|
return r.Add("PUT", pat, h)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Patch registers a pattern with a handler for PATCH requests.
|
||||||
|
func (r *Router) Patch(pat string, h http.HandlerFunc) *mux.Route {
|
||||||
|
return r.Add("PATCH", pat, h)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ServeHTTP dispatches the handler registered in the matched route.
|
||||||
|
func (r *Router) ServeHTTP(w http.ResponseWriter, req *http.Request) {
|
||||||
|
// Clean path to canonical form and redirect.
|
||||||
|
if p := cleanPath(req.URL.Path); p != req.URL.Path {
|
||||||
|
w.Header().Set("Location", p)
|
||||||
|
w.WriteHeader(http.StatusMovedPermanently)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var match mux.RouteMatch
|
||||||
|
var handler http.Handler
|
||||||
|
if matched := r.Match(req, &match); matched {
|
||||||
|
handler = match.Handler
|
||||||
|
registerVars(req, match.Vars)
|
||||||
|
}
|
||||||
|
if handler == nil {
|
||||||
|
if r.NotFoundHandler == nil {
|
||||||
|
r.NotFoundHandler = http.NotFoundHandler()
|
||||||
|
}
|
||||||
|
handler = r.NotFoundHandler
|
||||||
|
}
|
||||||
|
if !r.KeepContext {
|
||||||
|
defer context.Clear(req)
|
||||||
|
}
|
||||||
|
handler.ServeHTTP(w, req)
|
||||||
|
}
|
||||||
|
|
||||||
|
// registerVars adds the matched route variables to the URL query.
|
||||||
|
func registerVars(r *http.Request, vars map[string]string) {
|
||||||
|
parts, i := make([]string, len(vars)), 0
|
||||||
|
for key, value := range vars {
|
||||||
|
parts[i] = url.QueryEscape(":"+key) + "=" + url.QueryEscape(value)
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
q := strings.Join(parts, "&")
|
||||||
|
if r.URL.RawQuery == "" {
|
||||||
|
r.URL.RawQuery = q
|
||||||
|
} else {
|
||||||
|
r.URL.RawQuery += "&" + q
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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
|
||||||
|
}
|
8
vendor/github.com/gorilla/websocket/AUTHORS
generated
vendored
Normal file
8
vendor/github.com/gorilla/websocket/AUTHORS
generated
vendored
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
# This is the official list of Gorilla WebSocket authors for copyright
|
||||||
|
# purposes.
|
||||||
|
#
|
||||||
|
# Please keep the list sorted.
|
||||||
|
|
||||||
|
Gary Burd <gary@beagledreams.com>
|
||||||
|
Joachim Bauch <mail@joachim-bauch.de>
|
||||||
|
|
22
vendor/github.com/gorilla/websocket/LICENSE
generated
vendored
Normal file
22
vendor/github.com/gorilla/websocket/LICENSE
generated
vendored
Normal file
|
@ -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
vendor/github.com/gorilla/websocket/README.md
generated
vendored
Normal file
64
vendor/github.com/gorilla/websocket/README.md
generated
vendored
Normal file
|
@ -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.
|
||||||
|
|
||||||
|
[![Build Status](https://travis-ci.org/gorilla/websocket.svg?branch=master)](https://travis-ci.org/gorilla/websocket)
|
||||||
|
[![GoDoc](https://godoc.org/github.com/gorilla/websocket?status.svg)](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.
|
||||||
|
|
392
vendor/github.com/gorilla/websocket/client.go
generated
vendored
Normal file
392
vendor/github.com/gorilla/websocket/client.go
generated
vendored
Normal file
|
@ -0,0 +1,392 @@
|
||||||
|
// 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"
|
||||||
|
"bytes"
|
||||||
|
"crypto/tls"
|
||||||
|
"encoding/base64"
|
||||||
|
"errors"
|
||||||
|
"io"
|
||||||
|
"io/ioutil"
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
"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)
|
||||||
|
|
||||||
|
// 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
|
||||||
|
|
||||||
|
// 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
|
||||||
|
}
|
||||||
|
|
||||||
|
var errMalformedURL = errors.New("malformed ws or wss URL")
|
||||||
|
|
||||||
|
// parseURL parses the URL.
|
||||||
|
//
|
||||||
|
// This function is a replacement for the standard library url.Parse function.
|
||||||
|
// In Go 1.4 and earlier, url.Parse loses information from the path.
|
||||||
|
func parseURL(s string) (*url.URL, error) {
|
||||||
|
// From the RFC:
|
||||||
|
//
|
||||||
|
// ws-URI = "ws:" "//" host [ ":" port ] path [ "?" query ]
|
||||||
|
// wss-URI = "wss:" "//" host [ ":" port ] path [ "?" query ]
|
||||||
|
var u url.URL
|
||||||
|
switch {
|
||||||
|
case strings.HasPrefix(s, "ws://"):
|
||||||
|
u.Scheme = "ws"
|
||||||
|
s = s[len("ws://"):]
|
||||||
|
case strings.HasPrefix(s, "wss://"):
|
||||||
|
u.Scheme = "wss"
|
||||||
|
s = s[len("wss://"):]
|
||||||
|
default:
|
||||||
|
return nil, errMalformedURL
|
||||||
|
}
|
||||||
|
|
||||||
|
if i := strings.Index(s, "?"); i >= 0 {
|
||||||
|
u.RawQuery = s[i+1:]
|
||||||
|
s = s[:i]
|
||||||
|
}
|
||||||
|
|
||||||
|
if i := strings.Index(s, "/"); i >= 0 {
|
||||||
|
u.Opaque = s[i:]
|
||||||
|
s = s[:i]
|
||||||
|
} else {
|
||||||
|
u.Opaque = "/"
|
||||||
|
}
|
||||||
|
|
||||||
|
u.Host = s
|
||||||
|
|
||||||
|
if strings.Contains(u.Host, "@") {
|
||||||
|
// Don't bother parsing user information because user information is
|
||||||
|
// not allowed in websocket URIs.
|
||||||
|
return nil, errMalformedURL
|
||||||
|
}
|
||||||
|
|
||||||
|
return &u, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
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 zero values.
|
||||||
|
var DefaultDialer = &Dialer{
|
||||||
|
Proxy: http.ProxyFromEnvironment,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dial 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).
|
||||||
|
//
|
||||||
|
// 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) Dial(urlStr string, requestHeader http.Header) (*Conn, *http.Response, error) {
|
||||||
|
|
||||||
|
if d == nil {
|
||||||
|
d = &Dialer{
|
||||||
|
Proxy: http.ProxyFromEnvironment,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
challengeKey, err := generateChallengeKey()
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
u, err := parseURL(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,
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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)
|
||||||
|
default:
|
||||||
|
req.Header[k] = vs
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if d.EnableCompression {
|
||||||
|
req.Header.Set("Sec-Websocket-Extensions", "permessage-deflate; server_no_context_takeover; client_no_context_takeover")
|
||||||
|
}
|
||||||
|
|
||||||
|
hostPort, hostNoPort := hostPortNoPort(u)
|
||||||
|
|
||||||
|
var proxyURL *url.URL
|
||||||
|
// Check wether the proxy method has been configured
|
||||||
|
if d.Proxy != nil {
|
||||||
|
proxyURL, err = d.Proxy(req)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var targetHostPort string
|
||||||
|
if proxyURL != nil {
|
||||||
|
targetHostPort, _ = hostPortNoPort(proxyURL)
|
||||||
|
} else {
|
||||||
|
targetHostPort = hostPort
|
||||||
|
}
|
||||||
|
|
||||||
|
var deadline time.Time
|
||||||
|
if d.HandshakeTimeout != 0 {
|
||||||
|
deadline = time.Now().Add(d.HandshakeTimeout)
|
||||||
|
}
|
||||||
|
|
||||||
|
netDial := d.NetDial
|
||||||
|
if netDial == nil {
|
||||||
|
netDialer := &net.Dialer{Deadline: deadline}
|
||||||
|
netDial = netDialer.Dial
|
||||||
|
}
|
||||||
|
|
||||||
|
netConn, err := netDial("tcp", targetHostPort)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
if netConn != nil {
|
||||||
|
netConn.Close()
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
if err := netConn.SetDeadline(deadline); err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if proxyURL != nil {
|
||||||
|
connectHeader := make(http.Header)
|
||||||
|
if user := 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: hostPort},
|
||||||
|
Host: hostPort,
|
||||||
|
Header: connectHeader,
|
||||||
|
}
|
||||||
|
|
||||||
|
connectReq.Write(netConn)
|
||||||
|
|
||||||
|
// Read response.
|
||||||
|
// Okay to use and discard buffered reader here, because
|
||||||
|
// TLS server will not speak until spoken to.
|
||||||
|
br := bufio.NewReader(netConn)
|
||||||
|
resp, err := http.ReadResponse(br, connectReq)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
if resp.StatusCode != 200 {
|
||||||
|
f := strings.SplitN(resp.Status, " ", 2)
|
||||||
|
return nil, nil, errors.New(f[1])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if u.Scheme == "https" {
|
||||||
|
cfg := cloneTLSConfig(d.TLSClientConfig)
|
||||||
|
if cfg.ServerName == "" {
|
||||||
|
cfg.ServerName = hostNoPort
|
||||||
|
}
|
||||||
|
tlsConn := tls.Client(netConn, cfg)
|
||||||
|
netConn = tlsConn
|
||||||
|
if err := tlsConn.Handshake(); err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
if !cfg.InsecureSkipVerify {
|
||||||
|
if err := tlsConn.VerifyHostname(cfg.ServerName); err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
conn := newConn(netConn, false, d.ReadBufferSize, d.WriteBufferSize)
|
||||||
|
|
||||||
|
if err := req.Write(netConn); err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
16
vendor/github.com/gorilla/websocket/client_clone.go
generated
vendored
Normal file
16
vendor/github.com/gorilla/websocket/client_clone.go
generated
vendored
Normal file
|
@ -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
vendor/github.com/gorilla/websocket/client_clone_legacy.go
generated
vendored
Normal file
38
vendor/github.com/gorilla/websocket/client_clone_legacy.go
generated
vendored
Normal file
|
@ -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
vendor/github.com/gorilla/websocket/compression.go
generated
vendored
Normal file
148
vendor/github.com/gorilla/websocket/compression.go
generated
vendored
Normal file
|
@ -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
|
||||||
|
}
|
1149
vendor/github.com/gorilla/websocket/conn.go
generated
vendored
Normal file
1149
vendor/github.com/gorilla/websocket/conn.go
generated
vendored
Normal file
File diff suppressed because it is too large
Load diff
18
vendor/github.com/gorilla/websocket/conn_read.go
generated
vendored
Normal file
18
vendor/github.com/gorilla/websocket/conn_read.go
generated
vendored
Normal file
|
@ -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.5
|
||||||
|
|
||||||
|
package websocket
|
||||||
|
|
||||||
|
import "io"
|
||||||
|
|
||||||
|
func (c *Conn) read(n int) ([]byte, error) {
|
||||||
|
p, err := c.br.Peek(n)
|
||||||
|
if err == io.EOF {
|
||||||
|
err = errUnexpectedEOF
|
||||||
|
}
|
||||||
|
c.br.Discard(len(p))
|
||||||
|
return p, err
|
||||||
|
}
|
21
vendor/github.com/gorilla/websocket/conn_read_legacy.go
generated
vendored
Normal file
21
vendor/github.com/gorilla/websocket/conn_read_legacy.go
generated
vendored
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
// 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.5
|
||||||
|
|
||||||
|
package websocket
|
||||||
|
|
||||||
|
import "io"
|
||||||
|
|
||||||
|
func (c *Conn) read(n int) ([]byte, error) {
|
||||||
|
p, err := c.br.Peek(n)
|
||||||
|
if err == io.EOF {
|
||||||
|
err = errUnexpectedEOF
|
||||||
|
}
|
||||||
|
if len(p) > 0 {
|
||||||
|
// advance over the bytes just read
|
||||||
|
io.ReadFull(c.br, p)
|
||||||
|
}
|
||||||
|
return p, err
|
||||||
|
}
|
180
vendor/github.com/gorilla/websocket/doc.go
generated
vendored
Normal file
180
vendor/github.com/gorilla/websocket/doc.go
generated
vendored
Normal file
|
@ -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 uses
|
||||||
|
// the Upgrade function from an Upgrader object with a HTTP request handler
|
||||||
|
// to get a pointer to 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 {
|
||||||
|
// return
|
||||||
|
// }
|
||||||
|
// if err = conn.WriteMessage(messageType, p); err != nil {
|
||||||
|
// return err
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// 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 sending a close message to the
|
||||||
|
// peer and returning a *CloseError from the the NextReader, ReadMessage or the
|
||||||
|
// message Read method.
|
||||||
|
//
|
||||||
|
// Connections handle received ping and pong messages by invoking callback
|
||||||
|
// functions set with SetPingHandler and SetPongHandler methods. The callback
|
||||||
|
// functions are called from the NextReader, ReadMessage and the message Read
|
||||||
|
// methods.
|
||||||
|
//
|
||||||
|
// The default ping handler sends a pong to the peer. The application's reading
|
||||||
|
// goroutine can block for a short time while the handler writes the pong data
|
||||||
|
// to the connection.
|
||||||
|
//
|
||||||
|
// The application must read the connection to process ping, pong and close
|
||||||
|
// 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 not equal to the
|
||||||
|
// Host request header.
|
||||||
|
//
|
||||||
|
// An application can allow connections from any origin by specifying a
|
||||||
|
// function that always returns true:
|
||||||
|
//
|
||||||
|
// var upgrader = websocket.Upgrader{
|
||||||
|
// CheckOrigin: func(r *http.Request) bool { return true },
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// The deprecated Upgrade function does not enforce an origin policy. It's the
|
||||||
|
// application's responsibility to check the Origin header before calling
|
||||||
|
// Upgrade.
|
||||||
|
//
|
||||||
|
// 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
|
55
vendor/github.com/gorilla/websocket/json.go
generated
vendored
Normal file
55
vendor/github.com/gorilla/websocket/json.go
generated
vendored
Normal file
|
@ -0,0 +1,55 @@
|
||||||
|
// 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 is deprecated, use c.WriteJSON instead.
|
||||||
|
func WriteJSON(c *Conn, v interface{}) error {
|
||||||
|
return c.WriteJSON(v)
|
||||||
|
}
|
||||||
|
|
||||||
|
// WriteJSON writes the JSON encoding of v to the connection.
|
||||||
|
//
|
||||||
|
// 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 is 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
|
||||||
|
}
|
55
vendor/github.com/gorilla/websocket/mask.go
generated
vendored
Normal file
55
vendor/github.com/gorilla/websocket/mask.go
generated
vendored
Normal file
|
@ -0,0 +1,55 @@
|
||||||
|
// 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
vendor/github.com/gorilla/websocket/mask_safe.go
generated
vendored
Normal file
15
vendor/github.com/gorilla/websocket/mask_safe.go
generated
vendored
Normal file
|
@ -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
|
||||||
|
}
|
103
vendor/github.com/gorilla/websocket/prepared.go
generated
vendored
Normal file
103
vendor/github.com/gorilla/websocket/prepared.go
generated
vendored
Normal file
|
@ -0,0 +1,103 @@
|
||||||
|
// 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
|
||||||
|
err error
|
||||||
|
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 }
|
291
vendor/github.com/gorilla/websocket/server.go
generated
vendored
Normal file
291
vendor/github.com/gorilla/websocket/server.go
generated
vendored
Normal file
|
@ -0,0 +1,291 @@
|
||||||
|
// 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"
|
||||||
|
"net"
|
||||||
|
"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
|
||||||
|
|
||||||
|
// Subprotocols specifies the server's supported protocols in order of
|
||||||
|
// preference. If this field is set, then the Upgrade method negotiates a
|
||||||
|
// subprotocol by selecting the first match in this list with a protocol
|
||||||
|
// requested by the client.
|
||||||
|
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, the host in the Origin header must not be set or
|
||||||
|
// must match the host of the request.
|
||||||
|
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 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) {
|
||||||
|
if r.Method != "GET" {
|
||||||
|
return u.returnError(w, r, http.StatusMethodNotAllowed, "websocket: not a websocket handshake: request method is not GET")
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, ok := responseHeader["Sec-Websocket-Extensions"]; ok {
|
||||||
|
return u.returnError(w, r, http.StatusInternalServerError, "websocket: application specific 'Sec-Websocket-Extensions' headers are unsupported")
|
||||||
|
}
|
||||||
|
|
||||||
|
if !tokenListContainsValue(r.Header, "Connection", "upgrade") {
|
||||||
|
return u.returnError(w, r, http.StatusBadRequest, "websocket: not a websocket handshake: 'upgrade' token not found in 'Connection' header")
|
||||||
|
}
|
||||||
|
|
||||||
|
if !tokenListContainsValue(r.Header, "Upgrade", "websocket") {
|
||||||
|
return u.returnError(w, r, http.StatusBadRequest, "websocket: not a websocket handshake: 'websocket' token not found in 'Upgrade' header")
|
||||||
|
}
|
||||||
|
|
||||||
|
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")
|
||||||
|
}
|
||||||
|
|
||||||
|
checkOrigin := u.CheckOrigin
|
||||||
|
if checkOrigin == nil {
|
||||||
|
checkOrigin = checkSameOrigin
|
||||||
|
}
|
||||||
|
if !checkOrigin(r) {
|
||||||
|
return u.returnError(w, r, http.StatusForbidden, "websocket: 'Origin' header value not allowed")
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
netConn net.Conn
|
||||||
|
err error
|
||||||
|
)
|
||||||
|
|
||||||
|
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")
|
||||||
|
}
|
||||||
|
|
||||||
|
c := newConnBRW(netConn, true, u.ReadBufferSize, u.WriteBufferSize, brw)
|
||||||
|
c.subprotocol = subprotocol
|
||||||
|
|
||||||
|
if compress {
|
||||||
|
c.newCompressionWriter = compressNoContextTakeover
|
||||||
|
c.newDecompressionReader = decompressNoContextTakeover
|
||||||
|
}
|
||||||
|
|
||||||
|
p := c.writeBuf[: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.
|
||||||
|
//
|
||||||
|
// This function is deprecated, use websocket.Upgrader instead.
|
||||||
|
//
|
||||||
|
// The application is responsible for checking the request origin before
|
||||||
|
// calling Upgrade. An example implementation of the same origin policy is:
|
||||||
|
//
|
||||||
|
// if req.Header.Get("Origin") != "http://"+req.Host {
|
||||||
|
// http.Error(w, "Origin not allowed", 403)
|
||||||
|
// 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")
|
||||||
|
}
|
214
vendor/github.com/gorilla/websocket/util.go
generated
vendored
Normal file
214
vendor/github.com/gorilla/websocket/util.go
generated
vendored
Normal file
|
@ -0,0 +1,214 @@
|
||||||
|
// 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"
|
||||||
|
)
|
||||||
|
|
||||||
|
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 += 1
|
||||||
|
case b == '\\':
|
||||||
|
escape = true
|
||||||
|
case b == '"':
|
||||||
|
return string(p[:j]), s[i+1:]
|
||||||
|
default:
|
||||||
|
p[j] = b
|
||||||
|
j += 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return "", ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return "", ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// tokenListContainsValue returns true if the 1#token header with the given
|
||||||
|
// name contains token.
|
||||||
|
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 strings.EqualFold(t, value) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if s == "" {
|
||||||
|
continue headers
|
||||||
|
}
|
||||||
|
s = s[1:]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseExtensiosn 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
|
||||||
|
}
|
21
vendor/github.com/ian-kent/envconf/LICENSE.md
generated
vendored
Normal file
21
vendor/github.com/ian-kent/envconf/LICENSE.md
generated
vendored
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
The MIT License (MIT)
|
||||||
|
|
||||||
|
Copyright (c) 2014 Ian Kent
|
||||||
|
|
||||||
|
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.
|
30
vendor/github.com/ian-kent/envconf/README.md
generated
vendored
Normal file
30
vendor/github.com/ian-kent/envconf/README.md
generated
vendored
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
envconf
|
||||||
|
=======
|
||||||
|
|
||||||
|
Configure your Go application from the environment.
|
||||||
|
|
||||||
|
Supports most basic Go types and works nicely with the built in `flag` package.
|
||||||
|
|
||||||
|
```go
|
||||||
|
package main
|
||||||
|
|
||||||
|
import(
|
||||||
|
"flag"
|
||||||
|
"fmt"
|
||||||
|
. "github.com/ian-kent/envconf"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
count := flag.Int("count", FromEnvP("COUNT", 15).(int), "Count target")
|
||||||
|
flag.Parse()
|
||||||
|
for i := 1; i <= *count; i++ {
|
||||||
|
fmt.Printf("%d\n", i)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Licence
|
||||||
|
|
||||||
|
Copyright © 2014, Ian Kent (http://iankent.uk).
|
||||||
|
|
||||||
|
Released under MIT license, see [LICENSE](LICENSE.md) for details.
|
88
vendor/github.com/ian-kent/envconf/envconf.go
generated
vendored
Normal file
88
vendor/github.com/ian-kent/envconf/envconf.go
generated
vendored
Normal file
|
@ -0,0 +1,88 @@
|
||||||
|
package envconf
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"os"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
// ErrUnsupportedType is returned if the type passed in is unsupported
|
||||||
|
ErrUnsupportedType = errors.New("Unsupported type")
|
||||||
|
)
|
||||||
|
|
||||||
|
// FromEnvP is the same as FromEnv, but panics on error
|
||||||
|
func FromEnvP(env string, value interface{}) interface{} {
|
||||||
|
ev, err := FromEnv(env, value)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
return ev
|
||||||
|
}
|
||||||
|
|
||||||
|
// FromEnv returns the environment variable specified by env
|
||||||
|
// using the type of value
|
||||||
|
func FromEnv(env string, value interface{}) (interface{}, error) {
|
||||||
|
envs := os.Environ()
|
||||||
|
found := false
|
||||||
|
for _, e := range envs {
|
||||||
|
if strings.HasPrefix(e, env+"=") {
|
||||||
|
found = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !found {
|
||||||
|
return value, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
ev := os.Getenv(env)
|
||||||
|
|
||||||
|
switch value.(type) {
|
||||||
|
case string:
|
||||||
|
vt := interface{}(ev)
|
||||||
|
return vt, nil
|
||||||
|
case int:
|
||||||
|
i, e := strconv.ParseInt(ev, 10, 64)
|
||||||
|
return int(i), e
|
||||||
|
case int8:
|
||||||
|
i, e := strconv.ParseInt(ev, 10, 8)
|
||||||
|
return int8(i), e
|
||||||
|
case int16:
|
||||||
|
i, e := strconv.ParseInt(ev, 10, 16)
|
||||||
|
return int16(i), e
|
||||||
|
case int32:
|
||||||
|
i, e := strconv.ParseInt(ev, 10, 32)
|
||||||
|
return int32(i), e
|
||||||
|
case int64:
|
||||||
|
i, e := strconv.ParseInt(ev, 10, 64)
|
||||||
|
return i, e
|
||||||
|
case uint:
|
||||||
|
i, e := strconv.ParseUint(ev, 10, 64)
|
||||||
|
return uint(i), e
|
||||||
|
case uint8:
|
||||||
|
i, e := strconv.ParseUint(ev, 10, 8)
|
||||||
|
return uint8(i), e
|
||||||
|
case uint16:
|
||||||
|
i, e := strconv.ParseUint(ev, 10, 16)
|
||||||
|
return uint16(i), e
|
||||||
|
case uint32:
|
||||||
|
i, e := strconv.ParseUint(ev, 10, 32)
|
||||||
|
return uint32(i), e
|
||||||
|
case uint64:
|
||||||
|
i, e := strconv.ParseUint(ev, 10, 64)
|
||||||
|
return i, e
|
||||||
|
case float32:
|
||||||
|
i, e := strconv.ParseFloat(ev, 32)
|
||||||
|
return float32(i), e
|
||||||
|
case float64:
|
||||||
|
i, e := strconv.ParseFloat(ev, 64)
|
||||||
|
return float64(i), e
|
||||||
|
case bool:
|
||||||
|
i, e := strconv.ParseBool(ev)
|
||||||
|
return i, e
|
||||||
|
default:
|
||||||
|
return value, ErrUnsupportedType
|
||||||
|
}
|
||||||
|
}
|
21
vendor/github.com/ian-kent/go-log/LICENSE.md
generated
vendored
Normal file
21
vendor/github.com/ian-kent/go-log/LICENSE.md
generated
vendored
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
The MIT License (MIT)
|
||||||
|
|
||||||
|
Copyright (c) 2014 Ian Kent
|
||||||
|
|
||||||
|
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.
|
22
vendor/github.com/ian-kent/go-log/appenders/appender.go
generated
vendored
Normal file
22
vendor/github.com/ian-kent/go-log/appenders/appender.go
generated
vendored
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
package appenders
|
||||||
|
|
||||||
|
/*
|
||||||
|
|
||||||
|
Appenders control the flow of data from a logger to an output.
|
||||||
|
|
||||||
|
For example, a Console appender outputs log data to stdout.
|
||||||
|
|
||||||
|
Satisfy the Appender interface to implement your own log appender.
|
||||||
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/ian-kent/go-log/layout"
|
||||||
|
"github.com/ian-kent/go-log/levels"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Appender interface {
|
||||||
|
Write(level levels.LogLevel, message string, args ...interface{})
|
||||||
|
Layout() layout.Layout
|
||||||
|
SetLayout(layout.Layout)
|
||||||
|
}
|
31
vendor/github.com/ian-kent/go-log/appenders/console.go
generated
vendored
Normal file
31
vendor/github.com/ian-kent/go-log/appenders/console.go
generated
vendored
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
package appenders
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"github.com/ian-kent/go-log/layout"
|
||||||
|
"github.com/ian-kent/go-log/levels"
|
||||||
|
)
|
||||||
|
|
||||||
|
type consoleAppender struct {
|
||||||
|
Appender
|
||||||
|
layout layout.Layout
|
||||||
|
}
|
||||||
|
|
||||||
|
func Console() *consoleAppender {
|
||||||
|
a := &consoleAppender{
|
||||||
|
layout: layout.Default(),
|
||||||
|
}
|
||||||
|
return a
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *consoleAppender) Write(level levels.LogLevel, message string, args ...interface{}) {
|
||||||
|
fmt.Println(a.Layout().Format(level, message, args...))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *consoleAppender) Layout() layout.Layout {
|
||||||
|
return a.layout
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *consoleAppender) SetLayout(layout layout.Layout) {
|
||||||
|
a.layout = layout
|
||||||
|
}
|
64
vendor/github.com/ian-kent/go-log/appenders/fluentd.go
generated
vendored
Normal file
64
vendor/github.com/ian-kent/go-log/appenders/fluentd.go
generated
vendored
Normal file
|
@ -0,0 +1,64 @@
|
||||||
|
package appenders
|
||||||
|
|
||||||
|
// TODO add tests
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/ian-kent/go-log/layout"
|
||||||
|
"github.com/ian-kent/go-log/levels"
|
||||||
|
"github.com/t-k/fluent-logger-golang/fluent"
|
||||||
|
)
|
||||||
|
|
||||||
|
type fluentdAppender struct {
|
||||||
|
Appender
|
||||||
|
layout layout.Layout
|
||||||
|
fluent *fluent.Fluent
|
||||||
|
fluentConfig fluent.Config
|
||||||
|
}
|
||||||
|
|
||||||
|
func SafeFluentd(config fluent.Config) (*fluentdAppender, error) {
|
||||||
|
a := &fluentdAppender{
|
||||||
|
layout: layout.Default(),
|
||||||
|
fluentConfig: config,
|
||||||
|
}
|
||||||
|
if err := a.Open(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return a, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func Fluentd(config fluent.Config) *fluentdAppender {
|
||||||
|
a, _ := SafeFluentd(config)
|
||||||
|
return a
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *fluentdAppender) Close() {
|
||||||
|
a.fluent.Close()
|
||||||
|
a.fluent = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *fluentdAppender) Open() error {
|
||||||
|
f, err := fluent.New(a.fluentConfig)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
a.fluent = f
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *fluentdAppender) Write(level levels.LogLevel, message string, args ...interface{}) {
|
||||||
|
// FIXME
|
||||||
|
// - use tag instead of "go-log"
|
||||||
|
// - get layout to return the map
|
||||||
|
var data = map[string]string{
|
||||||
|
"message": a.Layout().Format(level, message, args...),
|
||||||
|
}
|
||||||
|
a.fluent.Post("go-log", data)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *fluentdAppender) Layout() layout.Layout {
|
||||||
|
return a.layout
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *fluentdAppender) SetLayout(layout layout.Layout) {
|
||||||
|
a.layout = layout
|
||||||
|
}
|
32
vendor/github.com/ian-kent/go-log/appenders/multiple_appender.go
generated
vendored
Normal file
32
vendor/github.com/ian-kent/go-log/appenders/multiple_appender.go
generated
vendored
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
package appenders
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/ian-kent/go-log/layout"
|
||||||
|
"github.com/ian-kent/go-log/levels"
|
||||||
|
)
|
||||||
|
|
||||||
|
type multipleAppender struct {
|
||||||
|
currentLayout layout.Layout
|
||||||
|
listOfAppenders []Appender
|
||||||
|
}
|
||||||
|
|
||||||
|
func Multiple(layout layout.Layout, appenders ...Appender) Appender {
|
||||||
|
return &multipleAppender{
|
||||||
|
listOfAppenders: appenders,
|
||||||
|
currentLayout: layout,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (this *multipleAppender) Layout() layout.Layout {
|
||||||
|
return this.currentLayout
|
||||||
|
}
|
||||||
|
|
||||||
|
func (this *multipleAppender) SetLayout(l layout.Layout) {
|
||||||
|
this.currentLayout = l
|
||||||
|
}
|
||||||
|
|
||||||
|
func (this *multipleAppender) Write(level levels.LogLevel, message string, args ...interface{}) {
|
||||||
|
for _, appender := range this.listOfAppenders {
|
||||||
|
appender.Write(level, message, args...)
|
||||||
|
}
|
||||||
|
}
|
122
vendor/github.com/ian-kent/go-log/appenders/rollingfile.go
generated
vendored
Normal file
122
vendor/github.com/ian-kent/go-log/appenders/rollingfile.go
generated
vendored
Normal file
|
@ -0,0 +1,122 @@
|
||||||
|
package appenders
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"github.com/ian-kent/go-log/layout"
|
||||||
|
"github.com/ian-kent/go-log/levels"
|
||||||
|
"os"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
)
|
||||||
|
|
||||||
|
type rollingFileAppender struct {
|
||||||
|
Appender
|
||||||
|
layout layout.Layout
|
||||||
|
MaxFileSize int64
|
||||||
|
MaxBackupIndex int
|
||||||
|
|
||||||
|
filename string
|
||||||
|
file *os.File
|
||||||
|
append bool
|
||||||
|
writeMutex sync.Mutex
|
||||||
|
|
||||||
|
bytesWritten int64
|
||||||
|
}
|
||||||
|
|
||||||
|
func RollingFile(filename string, append bool) *rollingFileAppender {
|
||||||
|
a := &rollingFileAppender{
|
||||||
|
layout: layout.Default(),
|
||||||
|
MaxFileSize: 104857600,
|
||||||
|
MaxBackupIndex: 1,
|
||||||
|
append: append,
|
||||||
|
bytesWritten: 0,
|
||||||
|
}
|
||||||
|
err := a.SetFilename(filename)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("Error opening file: %s\n", err)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return a
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *rollingFileAppender) Close() {
|
||||||
|
if a.file != nil {
|
||||||
|
a.file.Close()
|
||||||
|
a.file = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *rollingFileAppender) Write(level levels.LogLevel, message string, args ...interface{}) {
|
||||||
|
m := a.Layout().Format(level, message, args...)
|
||||||
|
if !strings.HasSuffix(m, "\n") {
|
||||||
|
m += "\n"
|
||||||
|
}
|
||||||
|
|
||||||
|
a.writeMutex.Lock()
|
||||||
|
a.file.Write([]byte(m))
|
||||||
|
|
||||||
|
a.bytesWritten += int64(len(m))
|
||||||
|
if a.bytesWritten >= a.MaxFileSize {
|
||||||
|
a.bytesWritten = 0
|
||||||
|
a.rotateFile()
|
||||||
|
}
|
||||||
|
|
||||||
|
a.writeMutex.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *rollingFileAppender) Layout() layout.Layout {
|
||||||
|
return a.layout
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *rollingFileAppender) SetLayout(layout layout.Layout) {
|
||||||
|
a.layout = layout
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *rollingFileAppender) Filename() string {
|
||||||
|
return a.filename
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *rollingFileAppender) SetFilename(filename string) error {
|
||||||
|
if a.filename != filename || a.file == nil {
|
||||||
|
a.closeFile()
|
||||||
|
a.filename = filename
|
||||||
|
err := a.openFile()
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *rollingFileAppender) rotateFile() {
|
||||||
|
a.closeFile()
|
||||||
|
|
||||||
|
lastFile := a.filename + "." + strconv.Itoa(a.MaxBackupIndex)
|
||||||
|
if _, err := os.Stat(lastFile); err == nil {
|
||||||
|
os.Remove(lastFile)
|
||||||
|
}
|
||||||
|
|
||||||
|
for n := a.MaxBackupIndex; n > 0; n-- {
|
||||||
|
f1 := a.filename + "." + strconv.Itoa(n)
|
||||||
|
f2 := a.filename + "." + strconv.Itoa(n+1)
|
||||||
|
os.Rename(f1, f2)
|
||||||
|
}
|
||||||
|
|
||||||
|
os.Rename(a.filename, a.filename+".1")
|
||||||
|
|
||||||
|
a.openFile()
|
||||||
|
}
|
||||||
|
func (a *rollingFileAppender) closeFile() {
|
||||||
|
if a.file != nil {
|
||||||
|
a.file.Close()
|
||||||
|
a.file = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
func (a *rollingFileAppender) openFile() error {
|
||||||
|
mode := os.O_WRONLY | os.O_APPEND | os.O_CREATE
|
||||||
|
if !a.append {
|
||||||
|
mode = os.O_WRONLY | os.O_CREATE
|
||||||
|
}
|
||||||
|
f, err := os.OpenFile(a.filename, mode, 0666)
|
||||||
|
a.file = f
|
||||||
|
return err
|
||||||
|
}
|
1
vendor/github.com/ian-kent/go-log/appenders/rollingfile_test.log
generated
vendored
Normal file
1
vendor/github.com/ian-kent/go-log/appenders/rollingfile_test.log
generated
vendored
Normal file
|
@ -0,0 +1 @@
|
||||||
|
Yet another test
|
2
vendor/github.com/ian-kent/go-log/appenders/rollingfile_test.log.1
generated
vendored
Normal file
2
vendor/github.com/ian-kent/go-log/appenders/rollingfile_test.log.1
generated
vendored
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
Test message
|
||||||
|
Another test
|
18
vendor/github.com/ian-kent/go-log/layout/basic.go
generated
vendored
Normal file
18
vendor/github.com/ian-kent/go-log/layout/basic.go
generated
vendored
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
package layout
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"github.com/ian-kent/go-log/levels"
|
||||||
|
)
|
||||||
|
|
||||||
|
type basicLayout struct {
|
||||||
|
Layout
|
||||||
|
}
|
||||||
|
|
||||||
|
func Basic() *basicLayout {
|
||||||
|
return &basicLayout{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *basicLayout) Format(level levels.LogLevel, message string, args ...interface{}) string {
|
||||||
|
return fmt.Sprintf(message, args...)
|
||||||
|
}
|
24
vendor/github.com/ian-kent/go-log/layout/layout.go
generated
vendored
Normal file
24
vendor/github.com/ian-kent/go-log/layout/layout.go
generated
vendored
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
package layout
|
||||||
|
|
||||||
|
/*
|
||||||
|
|
||||||
|
Layouts control the formatting of data into a printable log string.
|
||||||
|
|
||||||
|
For example, the Basic layout passes the log message and arguments
|
||||||
|
through fmt.Sprintf.
|
||||||
|
|
||||||
|
Satisfy the Layout interface to implement your own log layout.
|
||||||
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/ian-kent/go-log/levels"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Layout interface {
|
||||||
|
Format(level levels.LogLevel, message string, args ...interface{}) string
|
||||||
|
}
|
||||||
|
|
||||||
|
func Default() Layout {
|
||||||
|
return Basic()
|
||||||
|
}
|
112
vendor/github.com/ian-kent/go-log/layout/pattern.go
generated
vendored
Normal file
112
vendor/github.com/ian-kent/go-log/layout/pattern.go
generated
vendored
Normal file
|
@ -0,0 +1,112 @@
|
||||||
|
package layout
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"path/filepath"
|
||||||
|
"regexp"
|
||||||
|
"runtime"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/ian-kent/go-log/levels"
|
||||||
|
)
|
||||||
|
|
||||||
|
// http://logging.apache.org/log4j/1.2/apidocs/org/apache/log4j/PatternLayout.html
|
||||||
|
|
||||||
|
// DefaultTimeLayout is the default layout used by %d
|
||||||
|
var DefaultTimeLayout = "2006-01-02 15:04:05.000000000 -0700 MST"
|
||||||
|
|
||||||
|
// LegacyDefaultTimeLayout is the legacy (non-zero padded) time layout.
|
||||||
|
// Set layout.DefaultTimeLayout = layout.LegacyDefaultTimeLayout to revert behaviour.
|
||||||
|
var LegacyDefaultTimeLayout = "2006-01-02 15:04:05.999999999 -0700 MST"
|
||||||
|
|
||||||
|
type patternLayout struct {
|
||||||
|
Layout
|
||||||
|
Pattern string
|
||||||
|
created int64
|
||||||
|
re *regexp.Regexp
|
||||||
|
}
|
||||||
|
|
||||||
|
type caller struct {
|
||||||
|
pc uintptr
|
||||||
|
file string
|
||||||
|
line int
|
||||||
|
ok bool
|
||||||
|
pkg string
|
||||||
|
fullpkg string
|
||||||
|
filename string
|
||||||
|
}
|
||||||
|
|
||||||
|
func Pattern(pattern string) *patternLayout {
|
||||||
|
return &patternLayout{
|
||||||
|
Pattern: pattern,
|
||||||
|
re: regexp.MustCompile("%(\\w|%)(?:{([^}]+)})?"),
|
||||||
|
created: time.Now().UnixNano(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func getCaller() *caller {
|
||||||
|
pc, file, line, ok := runtime.Caller(2)
|
||||||
|
|
||||||
|
// TODO feels nasty?
|
||||||
|
dir, fn := filepath.Split(file)
|
||||||
|
bits := strings.Split(dir, "/")
|
||||||
|
pkg := bits[len(bits)-2]
|
||||||
|
|
||||||
|
if ok {
|
||||||
|
return &caller{pc, file, line, ok, pkg, pkg, fn}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *patternLayout) Format(level levels.LogLevel, message string, args ...interface{}) string {
|
||||||
|
|
||||||
|
// TODO
|
||||||
|
// padding, e.g. %20c, %-20c, %.30c, %20.30c, %-20.30c
|
||||||
|
// %t - thread name
|
||||||
|
// %M - function name
|
||||||
|
|
||||||
|
caller := getCaller()
|
||||||
|
r := time.Now().UnixNano()
|
||||||
|
|
||||||
|
msg := a.re.ReplaceAllStringFunc(a.Pattern, func(m string) string {
|
||||||
|
parts := a.re.FindStringSubmatch(m)
|
||||||
|
switch parts[1] {
|
||||||
|
// FIXME
|
||||||
|
// %c and %C should probably return the logger name, not the package
|
||||||
|
// name, since that's how the logger is created in the first place!
|
||||||
|
case "c":
|
||||||
|
return caller.pkg
|
||||||
|
case "C":
|
||||||
|
return caller.pkg
|
||||||
|
case "d":
|
||||||
|
// FIXME specifier, e.g. %d{HH:mm:ss,SSS}
|
||||||
|
return time.Now().Format(DefaultTimeLayout)
|
||||||
|
case "F":
|
||||||
|
return caller.file
|
||||||
|
case "l":
|
||||||
|
return fmt.Sprintf("%s/%s:%d", caller.pkg, caller.filename, caller.line)
|
||||||
|
case "L":
|
||||||
|
return strconv.Itoa(caller.line)
|
||||||
|
case "m":
|
||||||
|
return fmt.Sprintf(message, args...)
|
||||||
|
case "n":
|
||||||
|
// FIXME platform-specific?
|
||||||
|
return "\n"
|
||||||
|
case "p":
|
||||||
|
return levels.LogLevelsToString[level]
|
||||||
|
case "r":
|
||||||
|
return strconv.FormatInt((r-a.created)/100000, 10)
|
||||||
|
case "x":
|
||||||
|
return "" // NDC
|
||||||
|
case "X":
|
||||||
|
return "" // MDC (must specify key)
|
||||||
|
case "%":
|
||||||
|
return "%"
|
||||||
|
}
|
||||||
|
return m
|
||||||
|
})
|
||||||
|
|
||||||
|
return msg
|
||||||
|
}
|
33
vendor/github.com/ian-kent/go-log/levels/levels.go
generated
vendored
Normal file
33
vendor/github.com/ian-kent/go-log/levels/levels.go
generated
vendored
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
package levels
|
||||||
|
|
||||||
|
type LogLevel int
|
||||||
|
|
||||||
|
const (
|
||||||
|
FATAL LogLevel = iota
|
||||||
|
ERROR
|
||||||
|
INFO
|
||||||
|
WARN
|
||||||
|
DEBUG
|
||||||
|
TRACE
|
||||||
|
INHERIT
|
||||||
|
)
|
||||||
|
|
||||||
|
var StringToLogLevels = map[string]LogLevel{
|
||||||
|
"TRACE": TRACE,
|
||||||
|
"DEBUG": DEBUG,
|
||||||
|
"WARN": WARN,
|
||||||
|
"INFO": INFO,
|
||||||
|
"ERROR": ERROR,
|
||||||
|
"FATAL": FATAL,
|
||||||
|
"INHERIT": INHERIT,
|
||||||
|
}
|
||||||
|
|
||||||
|
var LogLevelsToString = map[LogLevel]string{
|
||||||
|
TRACE: "TRACE",
|
||||||
|
DEBUG: "DEBUG",
|
||||||
|
WARN: "WARN",
|
||||||
|
INFO: "INFO",
|
||||||
|
ERROR: "ERROR",
|
||||||
|
FATAL: "FATAL",
|
||||||
|
INHERIT: "INHERIT",
|
||||||
|
}
|
54
vendor/github.com/ian-kent/go-log/log/log.go
generated
vendored
Normal file
54
vendor/github.com/ian-kent/go-log/log/log.go
generated
vendored
Normal file
|
@ -0,0 +1,54 @@
|
||||||
|
package log
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/ian-kent/go-log/levels"
|
||||||
|
"github.com/ian-kent/go-log/logger"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
var global logger.Logger
|
||||||
|
|
||||||
|
// Converts a string level (e.g. DEBUG) to a LogLevel
|
||||||
|
func Stol(level string) levels.LogLevel {
|
||||||
|
return levels.StringToLogLevels[strings.ToUpper(level)]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns a Logger instance
|
||||||
|
//
|
||||||
|
// If no arguments are given, the global/root logger
|
||||||
|
// instance will be returned.
|
||||||
|
//
|
||||||
|
// If at least one argument is given, the logger instance
|
||||||
|
// for that namespace will be returned.
|
||||||
|
func Logger(args ...string) logger.Logger {
|
||||||
|
var name string
|
||||||
|
if len(args) > 0 {
|
||||||
|
name = args[0]
|
||||||
|
} else {
|
||||||
|
name = ""
|
||||||
|
}
|
||||||
|
|
||||||
|
if global == nil {
|
||||||
|
global = logger.New("")
|
||||||
|
global.SetLevel(levels.DEBUG)
|
||||||
|
}
|
||||||
|
|
||||||
|
l := global.GetLogger(name)
|
||||||
|
|
||||||
|
return l
|
||||||
|
}
|
||||||
|
|
||||||
|
func Log(level levels.LogLevel, params ...interface{}) {
|
||||||
|
Logger().Log(level, params...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func Level(level levels.LogLevel) { Logger().Level() }
|
||||||
|
func Debug(params ...interface{}) { Log(levels.DEBUG, params...) }
|
||||||
|
func Info(params ...interface{}) { Log(levels.INFO, params...) }
|
||||||
|
func Warn(params ...interface{}) { Log(levels.WARN, params...) }
|
||||||
|
func Error(params ...interface{}) { Log(levels.ERROR, params...) }
|
||||||
|
func Trace(params ...interface{}) { Log(levels.TRACE, params...) }
|
||||||
|
func Fatal(params ...interface{}) { Log(levels.FATAL, params...) }
|
||||||
|
func Printf(params ...interface{}) { Log(levels.INFO, params...) }
|
||||||
|
func Println(params ...interface{}) { Log(levels.INFO, params...) }
|
||||||
|
func Fatalf(params ...interface{}) { Log(levels.FATAL, params...) }
|
221
vendor/github.com/ian-kent/go-log/logger/logger.go
generated
vendored
Normal file
221
vendor/github.com/ian-kent/go-log/logger/logger.go
generated
vendored
Normal file
|
@ -0,0 +1,221 @@
|
||||||
|
package logger
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/ian-kent/go-log/appenders"
|
||||||
|
"github.com/ian-kent/go-log/layout"
|
||||||
|
"github.com/ian-kent/go-log/levels"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Logger represents a logger
|
||||||
|
type Logger interface {
|
||||||
|
Level() levels.LogLevel
|
||||||
|
Name() string
|
||||||
|
FullName() string
|
||||||
|
Enabled() map[levels.LogLevel]bool
|
||||||
|
Appender() Appender
|
||||||
|
Children() []Logger
|
||||||
|
Parent() Logger
|
||||||
|
GetLogger(string) Logger
|
||||||
|
SetLevel(levels.LogLevel)
|
||||||
|
Log(levels.LogLevel, ...interface{})
|
||||||
|
SetAppender(appender Appender)
|
||||||
|
|
||||||
|
Debug(params ...interface{})
|
||||||
|
Info(params ...interface{})
|
||||||
|
Warn(params ...interface{})
|
||||||
|
Error(params ...interface{})
|
||||||
|
Trace(params ...interface{})
|
||||||
|
Printf(params ...interface{})
|
||||||
|
Println(params ...interface{})
|
||||||
|
Fatal(params ...interface{})
|
||||||
|
Fatalf(params ...interface{})
|
||||||
|
}
|
||||||
|
|
||||||
|
type logger struct {
|
||||||
|
level levels.LogLevel
|
||||||
|
name string
|
||||||
|
enabled map[levels.LogLevel]bool
|
||||||
|
appender Appender
|
||||||
|
children []Logger
|
||||||
|
parent Logger
|
||||||
|
ExitOnFatal bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// Appender represents a log appender
|
||||||
|
type Appender interface {
|
||||||
|
Write(level levels.LogLevel, message string, args ...interface{})
|
||||||
|
SetLayout(layout layout.Layout)
|
||||||
|
Layout() layout.Layout
|
||||||
|
}
|
||||||
|
|
||||||
|
// New returns a new Logger
|
||||||
|
func New(name string) Logger {
|
||||||
|
l := Logger(&logger{
|
||||||
|
level: levels.DEBUG,
|
||||||
|
name: name,
|
||||||
|
enabled: make(map[levels.LogLevel]bool),
|
||||||
|
appender: appenders.Console(),
|
||||||
|
children: make([]Logger, 0),
|
||||||
|
parent: nil,
|
||||||
|
ExitOnFatal: true,
|
||||||
|
})
|
||||||
|
l.SetLevel(levels.DEBUG)
|
||||||
|
return l
|
||||||
|
}
|
||||||
|
|
||||||
|
func unwrap(args ...interface{}) []interface{} {
|
||||||
|
head := args[0]
|
||||||
|
switch head.(type) {
|
||||||
|
case func() (string, []interface{}):
|
||||||
|
msg, args := head.(func() (string, []interface{}))()
|
||||||
|
args = unwrap(args...)
|
||||||
|
return append([]interface{}{msg}, args...)
|
||||||
|
case func() []interface{}:
|
||||||
|
args = unwrap(head.(func() []interface{})()...)
|
||||||
|
case func(...interface{}) []interface{}:
|
||||||
|
args = unwrap(head.(func(...interface{}) []interface{})(args[1:]...)...)
|
||||||
|
}
|
||||||
|
return args
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *logger) New(name string) Logger {
|
||||||
|
lg := Logger(&logger{
|
||||||
|
level: levels.INHERIT,
|
||||||
|
name: name,
|
||||||
|
enabled: make(map[levels.LogLevel]bool),
|
||||||
|
appender: nil,
|
||||||
|
children: make([]Logger, 0),
|
||||||
|
parent: l,
|
||||||
|
})
|
||||||
|
l.children = append(l.children, lg)
|
||||||
|
return lg
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *logger) GetLogger(name string) Logger {
|
||||||
|
bits := strings.Split(name, ".")
|
||||||
|
|
||||||
|
if l.name == bits[0] {
|
||||||
|
if len(bits) == 1 {
|
||||||
|
return l
|
||||||
|
}
|
||||||
|
|
||||||
|
child := bits[1]
|
||||||
|
n := strings.Join(bits[1:], ".")
|
||||||
|
for _, c := range l.children {
|
||||||
|
if c.Name() == child {
|
||||||
|
return c.GetLogger(n)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
lg := l.New(child)
|
||||||
|
return lg.GetLogger(n)
|
||||||
|
}
|
||||||
|
lg := l.New(bits[0])
|
||||||
|
return lg.GetLogger(name)
|
||||||
|
}
|
||||||
|
|
||||||
|
type stringer interface {
|
||||||
|
String() string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *logger) write(level levels.LogLevel, params ...interface{}) {
|
||||||
|
a := l.Appender()
|
||||||
|
if a != nil {
|
||||||
|
if s, ok := params[0].(string); ok {
|
||||||
|
a.Write(level, s, params[1:]...)
|
||||||
|
} else if s, ok := params[0].(stringer); ok {
|
||||||
|
a.Write(level, s.String(), params[1:]...)
|
||||||
|
} else {
|
||||||
|
a.Write(level, fmt.Sprintf("%s", params[0]), params[1:]...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *logger) Appender() Appender {
|
||||||
|
if a := l.appender; a != nil {
|
||||||
|
return a
|
||||||
|
}
|
||||||
|
if l.parent != nil {
|
||||||
|
if a := l.parent.Appender(); a != nil {
|
||||||
|
return a
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *logger) Log(level levels.LogLevel, params ...interface{}) {
|
||||||
|
if !l.Enabled()[level] {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
l.write(level, unwrap(params...)...)
|
||||||
|
|
||||||
|
if l.ExitOnFatal && level == levels.FATAL {
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *logger) Level() levels.LogLevel {
|
||||||
|
if l.level == levels.INHERIT {
|
||||||
|
return l.parent.Level()
|
||||||
|
}
|
||||||
|
return l.level
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *logger) Enabled() map[levels.LogLevel]bool {
|
||||||
|
if l.level == levels.INHERIT {
|
||||||
|
return l.parent.Enabled()
|
||||||
|
}
|
||||||
|
return l.enabled
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *logger) Name() string {
|
||||||
|
return l.name
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *logger) FullName() string {
|
||||||
|
n := l.name
|
||||||
|
if l.parent != nil {
|
||||||
|
p := l.parent.FullName()
|
||||||
|
if len(p) > 0 {
|
||||||
|
n = l.parent.FullName() + "." + n
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return n
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *logger) Children() []Logger {
|
||||||
|
return l.children
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *logger) Parent() Logger {
|
||||||
|
return l.parent
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *logger) SetLevel(level levels.LogLevel) {
|
||||||
|
l.level = level
|
||||||
|
for k := range levels.LogLevelsToString {
|
||||||
|
if k <= level {
|
||||||
|
l.enabled[k] = true
|
||||||
|
} else {
|
||||||
|
l.enabled[k] = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *logger) SetAppender(appender Appender) {
|
||||||
|
l.appender = appender
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *logger) Debug(params ...interface{}) { l.Log(levels.DEBUG, params...) }
|
||||||
|
func (l *logger) Info(params ...interface{}) { l.Log(levels.INFO, params...) }
|
||||||
|
func (l *logger) Warn(params ...interface{}) { l.Log(levels.WARN, params...) }
|
||||||
|
func (l *logger) Error(params ...interface{}) { l.Log(levels.ERROR, params...) }
|
||||||
|
func (l *logger) Trace(params ...interface{}) { l.Log(levels.TRACE, params...) }
|
||||||
|
func (l *logger) Printf(params ...interface{}) { l.Log(levels.INFO, params...) }
|
||||||
|
func (l *logger) Println(params ...interface{}) { l.Log(levels.INFO, params...) }
|
||||||
|
func (l *logger) Fatal(params ...interface{}) { l.Log(levels.FATAL, params...) }
|
||||||
|
func (l *logger) Fatalf(params ...interface{}) { l.Log(levels.FATAL, params...) }
|
21
vendor/github.com/ian-kent/goose/LICENSE.md
generated
vendored
Normal file
21
vendor/github.com/ian-kent/goose/LICENSE.md
generated
vendored
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
The MIT License (MIT)
|
||||||
|
|
||||||
|
Copyright (c) 2014 Ian Kent
|
||||||
|
|
||||||
|
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.
|
12
vendor/github.com/ian-kent/goose/README.md
generated
vendored
Normal file
12
vendor/github.com/ian-kent/goose/README.md
generated
vendored
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
Goose - Go Server-Sent Events [![GoDoc](https://godoc.org/github.com/ian-kent/goose?status.svg)](https://godoc.org/github.com/ian-kent/goose)
|
||||||
|
=============================
|
||||||
|
|
||||||
|
Goose implements Server-Sent Events in Go.
|
||||||
|
|
||||||
|
See [this example](example/main.go).
|
||||||
|
|
||||||
|
### Licence
|
||||||
|
|
||||||
|
Copyright © 2014, Ian Kent (http://www.iankent.eu).
|
||||||
|
|
||||||
|
Released under MIT license, see [LICENSE](LICENSE.md) for details.
|
122
vendor/github.com/ian-kent/goose/goose.go
generated
vendored
Normal file
122
vendor/github.com/ian-kent/goose/goose.go
generated
vendored
Normal file
|
@ -0,0 +1,122 @@
|
||||||
|
package goose
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
// ErrUnableToHijackRequest is returned by AddReceiver if the type
|
||||||
|
// conversion to http.Hijacker is unsuccessful
|
||||||
|
ErrUnableToHijackRequest = errors.New("Unable to hijack request")
|
||||||
|
)
|
||||||
|
|
||||||
|
// EventStream represents a collection of receivers
|
||||||
|
type EventStream struct {
|
||||||
|
mutex *sync.Mutex
|
||||||
|
receivers map[net.Conn]*EventReceiver
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewEventStream creates a new event stream
|
||||||
|
func NewEventStream() *EventStream {
|
||||||
|
return &EventStream{
|
||||||
|
mutex: new(sync.Mutex),
|
||||||
|
receivers: make(map[net.Conn]*EventReceiver),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// EventReceiver represents a hijacked HTTP connection
|
||||||
|
type EventReceiver struct {
|
||||||
|
stream *EventStream
|
||||||
|
conn net.Conn
|
||||||
|
bufrw *bufio.ReadWriter
|
||||||
|
}
|
||||||
|
|
||||||
|
// Notify sends the event to all event stream receivers
|
||||||
|
func (es *EventStream) Notify(event string, bytes []byte) {
|
||||||
|
// TODO reader?
|
||||||
|
|
||||||
|
lines := strings.Split(string(bytes), "\n")
|
||||||
|
|
||||||
|
data := ""
|
||||||
|
for _, l := range lines {
|
||||||
|
data += event + ": " + l + "\n"
|
||||||
|
}
|
||||||
|
|
||||||
|
sz := len(data) + 1
|
||||||
|
size := fmt.Sprintf("%X", sz)
|
||||||
|
|
||||||
|
for _, er := range es.receivers {
|
||||||
|
go er.send(size, data)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (er *EventReceiver) send(size, data string) {
|
||||||
|
_, err := er.write([]byte(size + "\r\n"))
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
lines := strings.Split(data, "\n")
|
||||||
|
for _, ln := range lines {
|
||||||
|
_, err = er.write([]byte(ln + "\n"))
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
er.write([]byte("\r\n"))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (er *EventReceiver) write(bytes []byte) (int, error) {
|
||||||
|
n, err := er.bufrw.Write(bytes)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
er.stream.mutex.Lock()
|
||||||
|
delete(er.stream.receivers, er.conn)
|
||||||
|
er.stream.mutex.Unlock()
|
||||||
|
er.conn.Close()
|
||||||
|
return n, err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = er.bufrw.Flush()
|
||||||
|
if err != nil {
|
||||||
|
er.stream.mutex.Lock()
|
||||||
|
delete(er.stream.receivers, er.conn)
|
||||||
|
er.stream.mutex.Unlock()
|
||||||
|
er.conn.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
return n, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddReceiver hijacks a http.ResponseWriter and attaches it to the event stream
|
||||||
|
func (es *EventStream) AddReceiver(w http.ResponseWriter) (*EventReceiver, error) {
|
||||||
|
w.Header().Set("Content-Type", "text/event-stream")
|
||||||
|
w.Header().Set("Cache-Control", "no-cache")
|
||||||
|
w.Header().Set("Connection", "keep-alive")
|
||||||
|
|
||||||
|
w.WriteHeader(200)
|
||||||
|
|
||||||
|
hj, ok := w.(http.Hijacker)
|
||||||
|
if !ok {
|
||||||
|
return nil, ErrUnableToHijackRequest
|
||||||
|
}
|
||||||
|
|
||||||
|
hjConn, hjBufrw, err := hj.Hijack()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
rec := &EventReceiver{es, hjConn, hjBufrw}
|
||||||
|
|
||||||
|
es.mutex.Lock()
|
||||||
|
es.receivers[hjConn] = rec
|
||||||
|
es.mutex.Unlock()
|
||||||
|
|
||||||
|
return rec, nil
|
||||||
|
}
|
41
vendor/github.com/ian-kent/linkio/README.md
generated
vendored
Normal file
41
vendor/github.com/ian-kent/linkio/README.md
generated
vendored
Normal file
|
@ -0,0 +1,41 @@
|
||||||
|
linkio [![GoDoc](https://godoc.org/github.com/ian-kent/linkio?status.svg)](https://godoc.org/github.com/ian-kent/linkio) [![Build Status](https://travis-ci.org/ian-kent/linkio.svg?branch=master)](https://travis-ci.org/ian-kent/linkio)
|
||||||
|
======
|
||||||
|
|
||||||
|
linkio provides an io.Reader and io.Writer that simulate a network connection of a certain speed, e.g. to simulate a mobile connection.
|
||||||
|
|
||||||
|
### Quick start
|
||||||
|
|
||||||
|
You can use `linkio` to wrap existing io.Reader and io.Writer interfaces:
|
||||||
|
|
||||||
|
```go
|
||||||
|
// Create a new link at 512kbps
|
||||||
|
link = linkio.NewLink(512 * linkio.KilobitPerSecond)
|
||||||
|
|
||||||
|
// Open a connection
|
||||||
|
conn, err := net.Dial("tcp", "google.com:80")
|
||||||
|
if err != nil {
|
||||||
|
// handle error
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a link reader/writer
|
||||||
|
linkReader := link.NewLinkReader(io.Reader(conn))
|
||||||
|
linkWriter := link.NewLinkWriter(io.Writer(conn))
|
||||||
|
|
||||||
|
// Use them as you would normally...
|
||||||
|
fmt.Fprintf(linkWriter, "GET / HTTP/1.0\r\n\r\n")
|
||||||
|
status, err := bufio.NewReader(linkReader).ReadString('\n')
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
### LICENSE
|
||||||
|
|
||||||
|
This code is originally a fork of [code.google.com/p/jra-go/linkio](https://code.google.com/p/jra-go/source/browse/#hg%2Flinkio).
|
||||||
|
|
||||||
|
The source contained this license text:
|
||||||
|
|
||||||
|
Use of this source code is governed by a BSD-style
|
||||||
|
license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
There is no LICENSE file, but it [may be referring to this](http://opensource.org/licenses/BSD-3-Clause).
|
||||||
|
|
||||||
|
Any modifications since the initial commit are Copyright © 2014, Ian Kent (http://iankent.uk), and are released under the terms of the [MIT License](http://opensource.org/licenses/MIT).
|
169
vendor/github.com/ian-kent/linkio/linkio.go
generated
vendored
Normal file
169
vendor/github.com/ian-kent/linkio/linkio.go
generated
vendored
Normal file
|
@ -0,0 +1,169 @@
|
||||||
|
// Copyright 2011 The Go 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 linkio provides an io.Reader and io.Writer that
|
||||||
|
// simulate a network connection of a certain speed.
|
||||||
|
package linkio
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Throughput represents the link speed as an int64 bits per second
|
||||||
|
// count. The representation limits the largest representable throughput
|
||||||
|
// to approximately 9223 petabits per second.
|
||||||
|
type Throughput int64
|
||||||
|
|
||||||
|
// Common throughputs.
|
||||||
|
//
|
||||||
|
// To count the number of units in a Duration, divide:
|
||||||
|
// kilobit := linkio.KilobitPerSecond
|
||||||
|
// fmt.Print(int64(kilobit/linkio.BitPerSecond)) // prints 1024
|
||||||
|
//
|
||||||
|
// To convert an integer number of units to a Throughput, multiply:
|
||||||
|
// megabits := 10
|
||||||
|
// fmt.Print(linkio.Throughput(megabits)*time.BitPerSecond) // prints 10s
|
||||||
|
//
|
||||||
|
const (
|
||||||
|
BitPerSecond Throughput = 1
|
||||||
|
BytePerSecond = 8 * BitPerSecond
|
||||||
|
KilobitPerSecond = 1024 * BitPerSecond
|
||||||
|
KilobytePerSecond = 1024 * BytePerSecond
|
||||||
|
MegabitPerSecond = 1024 * KilobitPerSecond
|
||||||
|
MegabytePerSecond = 1024 * KilobytePerSecond
|
||||||
|
GigabitPerSecond = 1024 * MegabitPerSecond
|
||||||
|
GigabytePerSecond = 1024 * MegabytePerSecond
|
||||||
|
)
|
||||||
|
|
||||||
|
// A LinkReader wraps an io.Reader, simulating reading from a
|
||||||
|
// shared access link with a fixed maximum speed.
|
||||||
|
type LinkReader struct {
|
||||||
|
r io.Reader
|
||||||
|
link *Link
|
||||||
|
}
|
||||||
|
|
||||||
|
// A LinkWriter wraps an io.Writer, simulating writer to a
|
||||||
|
// shared access link with a fixed maximum speed.
|
||||||
|
type LinkWriter struct {
|
||||||
|
w io.Writer
|
||||||
|
link *Link
|
||||||
|
}
|
||||||
|
|
||||||
|
// A Link serializes requests to sleep, simulating the way data travels
|
||||||
|
// across a link which is running at a certain kbps (kilo = 1024).
|
||||||
|
// Multiple LinkReaders can share a link (simulating multiple apps
|
||||||
|
// sharing a link). The sharing behavior is approximately fair, as implemented
|
||||||
|
// by Go when scheduling reads from a contested blocking channel.
|
||||||
|
type Link struct {
|
||||||
|
in chan linkRequest
|
||||||
|
out chan linkRequest
|
||||||
|
speed int64 // nanosec per bit
|
||||||
|
}
|
||||||
|
|
||||||
|
// A linkRequest asks the link to simulate sending that much data
|
||||||
|
// and return a true on the channel when it has accomplished the request.
|
||||||
|
type linkRequest struct {
|
||||||
|
bytes int
|
||||||
|
done chan bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewLinkReader returns a LinkReader that returns bytes from r,
|
||||||
|
// simulating that they arrived from a shared link.
|
||||||
|
func (link *Link) NewLinkReader(r io.Reader) (s *LinkReader) {
|
||||||
|
s = &LinkReader{r: r, link: link}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewLinkWriter returns a LinkWriter that writes bytes to r,
|
||||||
|
// simulating that they arrived from a shared link.
|
||||||
|
func (link *Link) NewLinkWriter(w io.Writer) (s *LinkWriter) {
|
||||||
|
s = &LinkWriter{w: w, link: link}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewLink returns a new Link running at kbps.
|
||||||
|
func NewLink(throughput Throughput) (l *Link) {
|
||||||
|
// allow up to 100 outstanding requests
|
||||||
|
l = &Link{in: make(chan linkRequest, 100), out: make(chan linkRequest, 100)}
|
||||||
|
l.SetThroughput(throughput)
|
||||||
|
|
||||||
|
// This goroutine serializes the requests. He could calculate
|
||||||
|
// link utilization by comparing the time he sleeps waiting for
|
||||||
|
// linkRequests to arrive and the time he spends sleeping to simulate
|
||||||
|
// traffic flowing.
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
for lr := range l.in {
|
||||||
|
// bits * nanosec/bit = nano to wait
|
||||||
|
delay := time.Duration(int64(lr.bytes*8) * l.speed)
|
||||||
|
time.Sleep(delay)
|
||||||
|
lr.done <- true
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
go func() {
|
||||||
|
for lr := range l.out {
|
||||||
|
// bits * nanosec/bit = nano to wait
|
||||||
|
delay := time.Duration(int64(lr.bytes*8) * l.speed)
|
||||||
|
time.Sleep(delay)
|
||||||
|
lr.done <- true
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetThroughput sets the current link throughput
|
||||||
|
func (link *Link) SetThroughput(throughput Throughput) {
|
||||||
|
// link.speed is stored in ns/bit
|
||||||
|
link.speed = 1e9 / int64(throughput)
|
||||||
|
}
|
||||||
|
|
||||||
|
// why isn't this in package math? hmm.
|
||||||
|
func min(a, b int) int {
|
||||||
|
if a < b {
|
||||||
|
return a
|
||||||
|
}
|
||||||
|
return b
|
||||||
|
}
|
||||||
|
|
||||||
|
// Satisfies interface io.Reader.
|
||||||
|
func (l *LinkReader) Read(buf []byte) (n int, err error) {
|
||||||
|
// Read small chunks at a time, even if they ask for more,
|
||||||
|
// preventing one LinkReader from saturating the simulated link.
|
||||||
|
// 1500 is the MTU for Ethernet, i.e. a likely maximum packet
|
||||||
|
// size.
|
||||||
|
toRead := min(len(buf), 1500)
|
||||||
|
n, err = l.r.Read(buf[0:toRead])
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// send in the request to sleep to the Link and sleep
|
||||||
|
lr := linkRequest{bytes: n, done: make(chan bool)}
|
||||||
|
l.link.in <- lr
|
||||||
|
_ = <-lr.done
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Satisfies interface io.Writer.
|
||||||
|
func (l *LinkWriter) Write(buf []byte) (n int, err error) {
|
||||||
|
// Write small chunks at a time, even if they attempt more,
|
||||||
|
// preventing one LinkReader from saturating the simulated link.
|
||||||
|
// 1500 is the MTU for Ethernet, i.e. a likely maximum packet
|
||||||
|
// size.
|
||||||
|
toWrite := min(len(buf), 1500)
|
||||||
|
n, err = l.w.Write(buf[0:toWrite])
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// send in the request to sleep to the Link and sleep
|
||||||
|
lr := linkRequest{bytes: n, done: make(chan bool)}
|
||||||
|
l.link.in <- lr
|
||||||
|
_ = <-lr.done
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
18
vendor/github.com/jtolds/gls/LICENSE
generated
vendored
Normal file
18
vendor/github.com/jtolds/gls/LICENSE
generated
vendored
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
Copyright (c) 2013, Space Monkey, Inc.
|
||||||
|
|
||||||
|
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.
|
89
vendor/github.com/jtolds/gls/README.md
generated
vendored
Normal file
89
vendor/github.com/jtolds/gls/README.md
generated
vendored
Normal file
|
@ -0,0 +1,89 @@
|
||||||
|
gls
|
||||||
|
===
|
||||||
|
|
||||||
|
Goroutine local storage
|
||||||
|
|
||||||
|
### IMPORTANT NOTE ###
|
||||||
|
|
||||||
|
It is my duty to point you to https://blog.golang.org/context, which is how
|
||||||
|
Google solves all of the problems you'd perhaps consider using this package
|
||||||
|
for at scale.
|
||||||
|
|
||||||
|
One downside to Google's approach is that *all* of your functions must have
|
||||||
|
a new first argument, but after clearing that hurdle everything else is much
|
||||||
|
better.
|
||||||
|
|
||||||
|
If you aren't interested in this warning, read on.
|
||||||
|
|
||||||
|
### Huhwaht? Why? ###
|
||||||
|
|
||||||
|
Every so often, a thread shows up on the
|
||||||
|
[golang-nuts](https://groups.google.com/d/forum/golang-nuts) asking for some
|
||||||
|
form of goroutine-local-storage, or some kind of goroutine id, or some kind of
|
||||||
|
context. There are a few valid use cases for goroutine-local-storage, one of
|
||||||
|
the most prominent being log line context. One poster was interested in being
|
||||||
|
able to log an HTTP request context id in every log line in the same goroutine
|
||||||
|
as the incoming HTTP request, without having to change every library and
|
||||||
|
function call he was interested in logging.
|
||||||
|
|
||||||
|
This would be pretty useful. Provided that you could get some kind of
|
||||||
|
goroutine-local-storage, you could call
|
||||||
|
[log.SetOutput](http://golang.org/pkg/log/#SetOutput) with your own logging
|
||||||
|
writer that checks goroutine-local-storage for some context information and
|
||||||
|
adds that context to your log lines.
|
||||||
|
|
||||||
|
But alas, Andrew Gerrand's typically diplomatic answer to the question of
|
||||||
|
goroutine-local variables was:
|
||||||
|
|
||||||
|
> We wouldn't even be having this discussion if thread local storage wasn't
|
||||||
|
> useful. But every feature comes at a cost, and in my opinion the cost of
|
||||||
|
> threadlocals far outweighs their benefits. They're just not a good fit for
|
||||||
|
> Go.
|
||||||
|
|
||||||
|
So, yeah, that makes sense. That's a pretty good reason for why the language
|
||||||
|
won't support a specific and (relatively) unuseful feature that requires some
|
||||||
|
runtime changes, just for the sake of a little bit of log improvement.
|
||||||
|
|
||||||
|
But does Go require runtime changes?
|
||||||
|
|
||||||
|
### How it works ###
|
||||||
|
|
||||||
|
Go has pretty fantastic introspective and reflective features, but one thing Go
|
||||||
|
doesn't give you is any kind of access to the stack pointer, or frame pointer,
|
||||||
|
or goroutine id, or anything contextual about your current stack. It gives you
|
||||||
|
access to your list of callers, but only along with program counters, which are
|
||||||
|
fixed at compile time.
|
||||||
|
|
||||||
|
But it does give you the stack.
|
||||||
|
|
||||||
|
So, we define 16 special functions and embed base-16 tags into the stack using
|
||||||
|
the call order of those 16 functions. Then, we can read our tags back out of
|
||||||
|
the stack looking at the callers list.
|
||||||
|
|
||||||
|
We then use these tags as an index into a traditional map for implementing
|
||||||
|
this library.
|
||||||
|
|
||||||
|
### What are people saying? ###
|
||||||
|
|
||||||
|
"Wow, that's horrifying."
|
||||||
|
|
||||||
|
"This is the most terrible thing I have seen in a very long time."
|
||||||
|
|
||||||
|
"Where is it getting a context from? Is this serializing all the requests?
|
||||||
|
What the heck is the client being bound to? What are these tags? Why does he
|
||||||
|
need callers? Oh god no. No no no."
|
||||||
|
|
||||||
|
### Docs ###
|
||||||
|
|
||||||
|
Please see the docs at http://godoc.org/github.com/jtolds/gls
|
||||||
|
|
||||||
|
### Related ###
|
||||||
|
|
||||||
|
If you're okay relying on the string format of the current runtime stacktrace
|
||||||
|
including a unique goroutine id (not guaranteed by the spec or anything, but
|
||||||
|
very unlikely to change within a Go release), you might be able to squeeze
|
||||||
|
out a bit more performance by using this similar library, inspired by some
|
||||||
|
code Brad Fitzpatrick wrote for debugging his HTTP/2 library:
|
||||||
|
https://github.com/tylerb/gls (in contrast, jtolds/gls doesn't require
|
||||||
|
any knowledge of the string format of the runtime stacktrace, which
|
||||||
|
probably adds unnecessary overhead).
|
153
vendor/github.com/jtolds/gls/context.go
generated
vendored
Normal file
153
vendor/github.com/jtolds/gls/context.go
generated
vendored
Normal file
|
@ -0,0 +1,153 @@
|
||||||
|
// Package gls implements goroutine-local storage.
|
||||||
|
package gls
|
||||||
|
|
||||||
|
import (
|
||||||
|
"sync"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
mgrRegistry = make(map[*ContextManager]bool)
|
||||||
|
mgrRegistryMtx sync.RWMutex
|
||||||
|
)
|
||||||
|
|
||||||
|
// Values is simply a map of key types to value types. Used by SetValues to
|
||||||
|
// set multiple values at once.
|
||||||
|
type Values map[interface{}]interface{}
|
||||||
|
|
||||||
|
// ContextManager is the main entrypoint for interacting with
|
||||||
|
// Goroutine-local-storage. You can have multiple independent ContextManagers
|
||||||
|
// at any given time. ContextManagers are usually declared globally for a given
|
||||||
|
// class of context variables. You should use NewContextManager for
|
||||||
|
// construction.
|
||||||
|
type ContextManager struct {
|
||||||
|
mtx sync.Mutex
|
||||||
|
values map[uint]Values
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewContextManager returns a brand new ContextManager. It also registers the
|
||||||
|
// new ContextManager in the ContextManager registry which is used by the Go
|
||||||
|
// method. ContextManagers are typically defined globally at package scope.
|
||||||
|
func NewContextManager() *ContextManager {
|
||||||
|
mgr := &ContextManager{values: make(map[uint]Values)}
|
||||||
|
mgrRegistryMtx.Lock()
|
||||||
|
defer mgrRegistryMtx.Unlock()
|
||||||
|
mgrRegistry[mgr] = true
|
||||||
|
return mgr
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unregister removes a ContextManager from the global registry, used by the
|
||||||
|
// Go method. Only intended for use when you're completely done with a
|
||||||
|
// ContextManager. Use of Unregister at all is rare.
|
||||||
|
func (m *ContextManager) Unregister() {
|
||||||
|
mgrRegistryMtx.Lock()
|
||||||
|
defer mgrRegistryMtx.Unlock()
|
||||||
|
delete(mgrRegistry, m)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetValues takes a collection of values and a function to call for those
|
||||||
|
// values to be set in. Anything further down the stack will have the set
|
||||||
|
// values available through GetValue. SetValues will add new values or replace
|
||||||
|
// existing values of the same key and will not mutate or change values for
|
||||||
|
// previous stack frames.
|
||||||
|
// SetValues is slow (makes a copy of all current and new values for the new
|
||||||
|
// gls-context) in order to reduce the amount of lookups GetValue requires.
|
||||||
|
func (m *ContextManager) SetValues(new_values Values, context_call func()) {
|
||||||
|
if len(new_values) == 0 {
|
||||||
|
context_call()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
mutated_keys := make([]interface{}, 0, len(new_values))
|
||||||
|
mutated_vals := make(Values, len(new_values))
|
||||||
|
|
||||||
|
EnsureGoroutineId(func(gid uint) {
|
||||||
|
m.mtx.Lock()
|
||||||
|
state, found := m.values[gid]
|
||||||
|
if !found {
|
||||||
|
state = make(Values, len(new_values))
|
||||||
|
m.values[gid] = state
|
||||||
|
}
|
||||||
|
m.mtx.Unlock()
|
||||||
|
|
||||||
|
for key, new_val := range new_values {
|
||||||
|
mutated_keys = append(mutated_keys, key)
|
||||||
|
if old_val, ok := state[key]; ok {
|
||||||
|
mutated_vals[key] = old_val
|
||||||
|
}
|
||||||
|
state[key] = new_val
|
||||||
|
}
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
if !found {
|
||||||
|
m.mtx.Lock()
|
||||||
|
delete(m.values, gid)
|
||||||
|
m.mtx.Unlock()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, key := range mutated_keys {
|
||||||
|
if val, ok := mutated_vals[key]; ok {
|
||||||
|
state[key] = val
|
||||||
|
} else {
|
||||||
|
delete(state, key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
context_call()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetValue will return a previously set value, provided that the value was set
|
||||||
|
// by SetValues somewhere higher up the stack. If the value is not found, ok
|
||||||
|
// will be false.
|
||||||
|
func (m *ContextManager) GetValue(key interface{}) (
|
||||||
|
value interface{}, ok bool) {
|
||||||
|
gid, ok := GetGoroutineId()
|
||||||
|
if !ok {
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
|
||||||
|
m.mtx.Lock()
|
||||||
|
state, found := m.values[gid]
|
||||||
|
m.mtx.Unlock()
|
||||||
|
|
||||||
|
if !found {
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
value, ok = state[key]
|
||||||
|
return value, ok
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *ContextManager) getValues() Values {
|
||||||
|
gid, ok := GetGoroutineId()
|
||||||
|
if !ok {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
m.mtx.Lock()
|
||||||
|
state, _ := m.values[gid]
|
||||||
|
m.mtx.Unlock()
|
||||||
|
return state
|
||||||
|
}
|
||||||
|
|
||||||
|
// Go preserves ContextManager values and Goroutine-local-storage across new
|
||||||
|
// goroutine invocations. The Go method makes a copy of all existing values on
|
||||||
|
// all registered context managers and makes sure they are still set after
|
||||||
|
// kicking off the provided function in a new goroutine. If you don't use this
|
||||||
|
// Go method instead of the standard 'go' keyword, you will lose values in
|
||||||
|
// ContextManagers, as goroutines have brand new stacks.
|
||||||
|
func Go(cb func()) {
|
||||||
|
mgrRegistryMtx.RLock()
|
||||||
|
defer mgrRegistryMtx.RUnlock()
|
||||||
|
|
||||||
|
for mgr := range mgrRegistry {
|
||||||
|
values := mgr.getValues()
|
||||||
|
if len(values) > 0 {
|
||||||
|
cb = func(mgr *ContextManager, cb func()) func() {
|
||||||
|
return func() { mgr.SetValues(values, cb) }
|
||||||
|
}(mgr, cb)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
go cb()
|
||||||
|
}
|
21
vendor/github.com/jtolds/gls/gen_sym.go
generated
vendored
Normal file
21
vendor/github.com/jtolds/gls/gen_sym.go
generated
vendored
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
package gls
|
||||||
|
|
||||||
|
import (
|
||||||
|
"sync"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
keyMtx sync.Mutex
|
||||||
|
keyCounter uint64
|
||||||
|
)
|
||||||
|
|
||||||
|
// ContextKey is a throwaway value you can use as a key to a ContextManager
|
||||||
|
type ContextKey struct{ id uint64 }
|
||||||
|
|
||||||
|
// GenSym will return a brand new, never-before-used ContextKey
|
||||||
|
func GenSym() ContextKey {
|
||||||
|
keyMtx.Lock()
|
||||||
|
defer keyMtx.Unlock()
|
||||||
|
keyCounter += 1
|
||||||
|
return ContextKey{id: keyCounter}
|
||||||
|
}
|
25
vendor/github.com/jtolds/gls/gid.go
generated
vendored
Normal file
25
vendor/github.com/jtolds/gls/gid.go
generated
vendored
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
package gls
|
||||||
|
|
||||||
|
var (
|
||||||
|
stackTagPool = &idPool{}
|
||||||
|
)
|
||||||
|
|
||||||
|
// Will return this goroutine's identifier if set. If you always need a
|
||||||
|
// goroutine identifier, you should use EnsureGoroutineId which will make one
|
||||||
|
// if there isn't one already.
|
||||||
|
func GetGoroutineId() (gid uint, ok bool) {
|
||||||
|
return readStackTag()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Will call cb with the current goroutine identifier. If one hasn't already
|
||||||
|
// been generated, one will be created and set first. The goroutine identifier
|
||||||
|
// might be invalid after cb returns.
|
||||||
|
func EnsureGoroutineId(cb func(gid uint)) {
|
||||||
|
if gid, ok := readStackTag(); ok {
|
||||||
|
cb(gid)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
gid := stackTagPool.Acquire()
|
||||||
|
defer stackTagPool.Release(gid)
|
||||||
|
addStackTag(gid, func() { cb(gid) })
|
||||||
|
}
|
34
vendor/github.com/jtolds/gls/id_pool.go
generated
vendored
Normal file
34
vendor/github.com/jtolds/gls/id_pool.go
generated
vendored
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
package gls
|
||||||
|
|
||||||
|
// though this could probably be better at keeping ids smaller, the goal of
|
||||||
|
// this class is to keep a registry of the smallest unique integer ids
|
||||||
|
// per-process possible
|
||||||
|
|
||||||
|
import (
|
||||||
|
"sync"
|
||||||
|
)
|
||||||
|
|
||||||
|
type idPool struct {
|
||||||
|
mtx sync.Mutex
|
||||||
|
released []uint
|
||||||
|
max_id uint
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *idPool) Acquire() (id uint) {
|
||||||
|
p.mtx.Lock()
|
||||||
|
defer p.mtx.Unlock()
|
||||||
|
if len(p.released) > 0 {
|
||||||
|
id = p.released[len(p.released)-1]
|
||||||
|
p.released = p.released[:len(p.released)-1]
|
||||||
|
return id
|
||||||
|
}
|
||||||
|
id = p.max_id
|
||||||
|
p.max_id++
|
||||||
|
return id
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *idPool) Release(id uint) {
|
||||||
|
p.mtx.Lock()
|
||||||
|
defer p.mtx.Unlock()
|
||||||
|
p.released = append(p.released, id)
|
||||||
|
}
|
108
vendor/github.com/jtolds/gls/stack_tags.go
generated
vendored
Normal file
108
vendor/github.com/jtolds/gls/stack_tags.go
generated
vendored
Normal file
|
@ -0,0 +1,108 @@
|
||||||
|
package gls
|
||||||
|
|
||||||
|
// so, basically, we're going to encode integer tags in base-16 on the stack
|
||||||
|
|
||||||
|
const (
|
||||||
|
bitWidth = 4
|
||||||
|
stackBatchSize = 16
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
pc_lookup = make(map[uintptr]int8, 17)
|
||||||
|
mark_lookup [16]func(uint, func())
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
setEntries := func(f func(uint, func()), v int8) {
|
||||||
|
var ptr uintptr
|
||||||
|
f(0, func() {
|
||||||
|
ptr = findPtr()
|
||||||
|
})
|
||||||
|
pc_lookup[ptr] = v
|
||||||
|
if v >= 0 {
|
||||||
|
mark_lookup[v] = f
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setEntries(github_com_jtolds_gls_markS, -0x1)
|
||||||
|
setEntries(github_com_jtolds_gls_mark0, 0x0)
|
||||||
|
setEntries(github_com_jtolds_gls_mark1, 0x1)
|
||||||
|
setEntries(github_com_jtolds_gls_mark2, 0x2)
|
||||||
|
setEntries(github_com_jtolds_gls_mark3, 0x3)
|
||||||
|
setEntries(github_com_jtolds_gls_mark4, 0x4)
|
||||||
|
setEntries(github_com_jtolds_gls_mark5, 0x5)
|
||||||
|
setEntries(github_com_jtolds_gls_mark6, 0x6)
|
||||||
|
setEntries(github_com_jtolds_gls_mark7, 0x7)
|
||||||
|
setEntries(github_com_jtolds_gls_mark8, 0x8)
|
||||||
|
setEntries(github_com_jtolds_gls_mark9, 0x9)
|
||||||
|
setEntries(github_com_jtolds_gls_markA, 0xa)
|
||||||
|
setEntries(github_com_jtolds_gls_markB, 0xb)
|
||||||
|
setEntries(github_com_jtolds_gls_markC, 0xc)
|
||||||
|
setEntries(github_com_jtolds_gls_markD, 0xd)
|
||||||
|
setEntries(github_com_jtolds_gls_markE, 0xe)
|
||||||
|
setEntries(github_com_jtolds_gls_markF, 0xf)
|
||||||
|
}
|
||||||
|
|
||||||
|
func addStackTag(tag uint, context_call func()) {
|
||||||
|
if context_call == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
github_com_jtolds_gls_markS(tag, context_call)
|
||||||
|
}
|
||||||
|
|
||||||
|
// these private methods are named this horrendous name so gopherjs support
|
||||||
|
// is easier. it shouldn't add any runtime cost in non-js builds.
|
||||||
|
func github_com_jtolds_gls_markS(tag uint, cb func()) { _m(tag, cb) }
|
||||||
|
func github_com_jtolds_gls_mark0(tag uint, cb func()) { _m(tag, cb) }
|
||||||
|
func github_com_jtolds_gls_mark1(tag uint, cb func()) { _m(tag, cb) }
|
||||||
|
func github_com_jtolds_gls_mark2(tag uint, cb func()) { _m(tag, cb) }
|
||||||
|
func github_com_jtolds_gls_mark3(tag uint, cb func()) { _m(tag, cb) }
|
||||||
|
func github_com_jtolds_gls_mark4(tag uint, cb func()) { _m(tag, cb) }
|
||||||
|
func github_com_jtolds_gls_mark5(tag uint, cb func()) { _m(tag, cb) }
|
||||||
|
func github_com_jtolds_gls_mark6(tag uint, cb func()) { _m(tag, cb) }
|
||||||
|
func github_com_jtolds_gls_mark7(tag uint, cb func()) { _m(tag, cb) }
|
||||||
|
func github_com_jtolds_gls_mark8(tag uint, cb func()) { _m(tag, cb) }
|
||||||
|
func github_com_jtolds_gls_mark9(tag uint, cb func()) { _m(tag, cb) }
|
||||||
|
func github_com_jtolds_gls_markA(tag uint, cb func()) { _m(tag, cb) }
|
||||||
|
func github_com_jtolds_gls_markB(tag uint, cb func()) { _m(tag, cb) }
|
||||||
|
func github_com_jtolds_gls_markC(tag uint, cb func()) { _m(tag, cb) }
|
||||||
|
func github_com_jtolds_gls_markD(tag uint, cb func()) { _m(tag, cb) }
|
||||||
|
func github_com_jtolds_gls_markE(tag uint, cb func()) { _m(tag, cb) }
|
||||||
|
func github_com_jtolds_gls_markF(tag uint, cb func()) { _m(tag, cb) }
|
||||||
|
|
||||||
|
func _m(tag_remainder uint, cb func()) {
|
||||||
|
if tag_remainder == 0 {
|
||||||
|
cb()
|
||||||
|
} else {
|
||||||
|
mark_lookup[tag_remainder&0xf](tag_remainder>>bitWidth, cb)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func readStackTag() (tag uint, ok bool) {
|
||||||
|
var current_tag uint
|
||||||
|
offset := 0
|
||||||
|
for {
|
||||||
|
// the expectation with getStack is that it will either:
|
||||||
|
// * return everything when offset is 0 and ignore stackBatchSize,
|
||||||
|
// otherwise returning nothing when offset is not 0 (the gopherjs case)
|
||||||
|
// * or it will return at most stackBatchSize, respect offset, and
|
||||||
|
// shouldn't be called when it returns less than stackBatchSize
|
||||||
|
// (the runtime.Callers case).
|
||||||
|
batch := getStack(offset, stackBatchSize)
|
||||||
|
for _, pc := range batch {
|
||||||
|
val, ok := pc_lookup[pc]
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if val < 0 {
|
||||||
|
return current_tag, true
|
||||||
|
}
|
||||||
|
current_tag <<= bitWidth
|
||||||
|
current_tag += uint(val)
|
||||||
|
}
|
||||||
|
if len(batch) < stackBatchSize {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
offset += len(batch)
|
||||||
|
}
|
||||||
|
return 0, false
|
||||||
|
}
|
75
vendor/github.com/jtolds/gls/stack_tags_js.go
generated
vendored
Normal file
75
vendor/github.com/jtolds/gls/stack_tags_js.go
generated
vendored
Normal file
|
@ -0,0 +1,75 @@
|
||||||
|
// +build js
|
||||||
|
|
||||||
|
package gls
|
||||||
|
|
||||||
|
// This file is used for GopherJS builds, which don't have normal runtime
|
||||||
|
// stack trace support
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/gopherjs/gopherjs/js"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
jsFuncNamePrefix = "github_com_jtolds_gls_mark"
|
||||||
|
)
|
||||||
|
|
||||||
|
func jsMarkStack() (f []uintptr) {
|
||||||
|
lines := strings.Split(
|
||||||
|
js.Global.Get("Error").New().Get("stack").String(), "\n")
|
||||||
|
f = make([]uintptr, 0, len(lines))
|
||||||
|
for i, line := range lines {
|
||||||
|
line = strings.TrimSpace(line)
|
||||||
|
if line == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if i == 0 {
|
||||||
|
if line != "Error" {
|
||||||
|
panic("didn't understand js stack trace")
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
fields := strings.Fields(line)
|
||||||
|
if len(fields) < 2 || fields[0] != "at" {
|
||||||
|
panic("didn't understand js stack trace")
|
||||||
|
}
|
||||||
|
|
||||||
|
pos := strings.Index(fields[1], jsFuncNamePrefix)
|
||||||
|
if pos < 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
pos += len(jsFuncNamePrefix)
|
||||||
|
if pos >= len(fields[1]) {
|
||||||
|
panic("didn't understand js stack trace")
|
||||||
|
}
|
||||||
|
char := string(fields[1][pos])
|
||||||
|
switch char {
|
||||||
|
case "S":
|
||||||
|
f = append(f, uintptr(0))
|
||||||
|
default:
|
||||||
|
val, err := strconv.ParseUint(char, 16, 8)
|
||||||
|
if err != nil {
|
||||||
|
panic("didn't understand js stack trace")
|
||||||
|
}
|
||||||
|
f = append(f, uintptr(val)+1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return f
|
||||||
|
}
|
||||||
|
|
||||||
|
func findPtr() uintptr {
|
||||||
|
funcs := jsMarkStack()
|
||||||
|
if len(funcs) == 0 {
|
||||||
|
panic("failed to find function pointer")
|
||||||
|
}
|
||||||
|
return funcs[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
func getStack(offset, amount int) []uintptr {
|
||||||
|
if offset != 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return jsMarkStack()
|
||||||
|
}
|
23
vendor/github.com/jtolds/gls/stack_tags_main.go
generated
vendored
Normal file
23
vendor/github.com/jtolds/gls/stack_tags_main.go
generated
vendored
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
// +build !js
|
||||||
|
|
||||||
|
package gls
|
||||||
|
|
||||||
|
// This file is used for standard Go builds, which have the expected runtime
|
||||||
|
// support
|
||||||
|
|
||||||
|
import (
|
||||||
|
"runtime"
|
||||||
|
)
|
||||||
|
|
||||||
|
func getStack(offset, amount int) []uintptr {
|
||||||
|
stack := make([]uintptr, amount)
|
||||||
|
return stack[:runtime.Callers(offset, stack)]
|
||||||
|
}
|
||||||
|
|
||||||
|
func findPtr() uintptr {
|
||||||
|
pc, _, _, ok := runtime.Caller(3)
|
||||||
|
if !ok {
|
||||||
|
panic("failed to find function pointer")
|
||||||
|
}
|
||||||
|
return pc
|
||||||
|
}
|
21
vendor/github.com/mailhog/MailHog-Server/LICENSE.md
generated
vendored
Normal file
21
vendor/github.com/mailhog/MailHog-Server/LICENSE.md
generated
vendored
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
The MIT License (MIT)
|
||||||
|
|
||||||
|
Copyright (c) 2014 - 2016 Ian Kent
|
||||||
|
|
||||||
|
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.
|
17
vendor/github.com/mailhog/MailHog-Server/Makefile
generated
vendored
Normal file
17
vendor/github.com/mailhog/MailHog-Server/Makefile
generated
vendored
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
DEPS = $(go list -f '{{range .TestImports}}{{.}} {{end}}' ./...)
|
||||||
|
|
||||||
|
all: release-deps fmt combined
|
||||||
|
|
||||||
|
combined:
|
||||||
|
go install .
|
||||||
|
|
||||||
|
release:
|
||||||
|
gox -output="build/{{.Dir}}_{{.OS}}_{{.Arch}}" .
|
||||||
|
|
||||||
|
fmt:
|
||||||
|
go fmt ./...
|
||||||
|
|
||||||
|
release-deps:
|
||||||
|
go get github.com/mitchellh/gox
|
||||||
|
|
||||||
|
.PNONY: all combined release fmt release-deps
|
10
vendor/github.com/mailhog/MailHog-Server/README.md
generated
vendored
Normal file
10
vendor/github.com/mailhog/MailHog-Server/README.md
generated
vendored
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
MailHog Server [![Build Status](https://travis-ci.org/mailhog/MailHog-Server.svg?branch=master)](https://travis-ci.org/mailhog/MailHog-Server)
|
||||||
|
=========
|
||||||
|
|
||||||
|
MailHog-Server is the MailHog SMTP and HTTP API server.
|
||||||
|
|
||||||
|
### Licence
|
||||||
|
|
||||||
|
Copyright © 2014 - 2016, Ian Kent (http://iankent.uk)
|
||||||
|
|
||||||
|
Released under MIT license, see [LICENSE](LICENSE.md) for details.
|
23
vendor/github.com/mailhog/MailHog-Server/api/api.go
generated
vendored
Normal file
23
vendor/github.com/mailhog/MailHog-Server/api/api.go
generated
vendored
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
gohttp "net/http"
|
||||||
|
|
||||||
|
"github.com/gorilla/pat"
|
||||||
|
"github.com/mailhog/MailHog-Server/config"
|
||||||
|
)
|
||||||
|
|
||||||
|
func CreateAPI(conf *config.Config, r gohttp.Handler) {
|
||||||
|
apiv1 := createAPIv1(conf, r.(*pat.Router))
|
||||||
|
apiv2 := createAPIv2(conf, r.(*pat.Router))
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case msg := <-conf.MessageChan:
|
||||||
|
apiv1.messageChan <- msg
|
||||||
|
apiv2.messageChan <- msg
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
359
vendor/github.com/mailhog/MailHog-Server/api/v1.go
generated
vendored
Normal file
359
vendor/github.com/mailhog/MailHog-Server/api/v1.go
generated
vendored
Normal file
|
@ -0,0 +1,359 @@
|
||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/base64"
|
||||||
|
"encoding/json"
|
||||||
|
"net/http"
|
||||||
|
"net/smtp"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/gorilla/pat"
|
||||||
|
"github.com/ian-kent/go-log/log"
|
||||||
|
"github.com/mailhog/MailHog-Server/config"
|
||||||
|
"github.com/mailhog/data"
|
||||||
|
"github.com/mailhog/storage"
|
||||||
|
|
||||||
|
"github.com/ian-kent/goose"
|
||||||
|
)
|
||||||
|
|
||||||
|
// APIv1 implements version 1 of the MailHog API
|
||||||
|
//
|
||||||
|
// The specification has been frozen and will eventually be deprecated.
|
||||||
|
// Only bug fixes and non-breaking changes will be applied here.
|
||||||
|
//
|
||||||
|
// Any changes/additions should be added in APIv2.
|
||||||
|
type APIv1 struct {
|
||||||
|
config *config.Config
|
||||||
|
messageChan chan *data.Message
|
||||||
|
}
|
||||||
|
|
||||||
|
// FIXME should probably move this into APIv1 struct
|
||||||
|
var stream *goose.EventStream
|
||||||
|
|
||||||
|
// ReleaseConfig is an alias to preserve go package API
|
||||||
|
type ReleaseConfig config.OutgoingSMTP
|
||||||
|
|
||||||
|
func createAPIv1(conf *config.Config, r *pat.Router) *APIv1 {
|
||||||
|
log.Println("Creating API v1 with WebPath: " + conf.WebPath)
|
||||||
|
apiv1 := &APIv1{
|
||||||
|
config: conf,
|
||||||
|
messageChan: make(chan *data.Message),
|
||||||
|
}
|
||||||
|
|
||||||
|
stream = goose.NewEventStream()
|
||||||
|
|
||||||
|
r.Path(conf.WebPath + "/api/v1/messages").Methods("GET").HandlerFunc(apiv1.messages)
|
||||||
|
r.Path(conf.WebPath + "/api/v1/messages").Methods("DELETE").HandlerFunc(apiv1.delete_all)
|
||||||
|
r.Path(conf.WebPath + "/api/v1/messages").Methods("OPTIONS").HandlerFunc(apiv1.defaultOptions)
|
||||||
|
|
||||||
|
r.Path(conf.WebPath + "/api/v1/messages/{id}").Methods("GET").HandlerFunc(apiv1.message)
|
||||||
|
r.Path(conf.WebPath + "/api/v1/messages/{id}").Methods("DELETE").HandlerFunc(apiv1.delete_one)
|
||||||
|
r.Path(conf.WebPath + "/api/v1/messages/{id}").Methods("OPTIONS").HandlerFunc(apiv1.defaultOptions)
|
||||||
|
|
||||||
|
r.Path(conf.WebPath + "/api/v1/messages/{id}/download").Methods("GET").HandlerFunc(apiv1.download)
|
||||||
|
r.Path(conf.WebPath + "/api/v1/messages/{id}/download").Methods("OPTIONS").HandlerFunc(apiv1.defaultOptions)
|
||||||
|
|
||||||
|
r.Path(conf.WebPath + "/api/v1/messages/{id}/mime/part/{part}/download").Methods("GET").HandlerFunc(apiv1.download_part)
|
||||||
|
r.Path(conf.WebPath + "/api/v1/messages/{id}/mime/part/{part}/download").Methods("OPTIONS").HandlerFunc(apiv1.defaultOptions)
|
||||||
|
|
||||||
|
r.Path(conf.WebPath + "/api/v1/messages/{id}/release").Methods("POST").HandlerFunc(apiv1.release_one)
|
||||||
|
r.Path(conf.WebPath + "/api/v1/messages/{id}/release").Methods("OPTIONS").HandlerFunc(apiv1.defaultOptions)
|
||||||
|
|
||||||
|
r.Path(conf.WebPath + "/api/v1/events").Methods("GET").HandlerFunc(apiv1.eventstream)
|
||||||
|
r.Path(conf.WebPath + "/api/v1/events").Methods("OPTIONS").HandlerFunc(apiv1.defaultOptions)
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
keepaliveTicker := time.Tick(time.Minute)
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case msg := <-apiv1.messageChan:
|
||||||
|
log.Println("Got message in APIv1 event stream")
|
||||||
|
bytes, _ := json.MarshalIndent(msg, "", " ")
|
||||||
|
json := string(bytes)
|
||||||
|
log.Printf("Sending content: %s\n", json)
|
||||||
|
apiv1.broadcast(json)
|
||||||
|
case <-keepaliveTicker:
|
||||||
|
apiv1.keepalive()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
return apiv1
|
||||||
|
}
|
||||||
|
|
||||||
|
func (apiv1 *APIv1) defaultOptions(w http.ResponseWriter, req *http.Request) {
|
||||||
|
if len(apiv1.config.CORSOrigin) > 0 {
|
||||||
|
w.Header().Add("Access-Control-Allow-Origin", apiv1.config.CORSOrigin)
|
||||||
|
w.Header().Add("Access-Control-Allow-Methods", "OPTIONS,GET,POST,DELETE")
|
||||||
|
w.Header().Add("Access-Control-Allow-Headers", "Content-Type")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (apiv1 *APIv1) broadcast(json string) {
|
||||||
|
log.Println("[APIv1] BROADCAST /api/v1/events")
|
||||||
|
b := []byte(json)
|
||||||
|
stream.Notify("data", b)
|
||||||
|
}
|
||||||
|
|
||||||
|
// keepalive sends an empty keep alive message.
|
||||||
|
//
|
||||||
|
// This not only can keep connections alive, but also will detect broken
|
||||||
|
// connections. Without this it is possible for the server to become
|
||||||
|
// unresponsive due to too many open files.
|
||||||
|
func (apiv1 *APIv1) keepalive() {
|
||||||
|
log.Println("[APIv1] KEEPALIVE /api/v1/events")
|
||||||
|
stream.Notify("keepalive", []byte{})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (apiv1 *APIv1) eventstream(w http.ResponseWriter, req *http.Request) {
|
||||||
|
log.Println("[APIv1] GET /api/v1/events")
|
||||||
|
|
||||||
|
//apiv1.defaultOptions(session)
|
||||||
|
if len(apiv1.config.CORSOrigin) > 0 {
|
||||||
|
w.Header().Add("Access-Control-Allow-Origin", apiv1.config.CORSOrigin)
|
||||||
|
w.Header().Add("Access-Control-Allow-Methods", "OPTIONS,GET,POST,DELETE")
|
||||||
|
}
|
||||||
|
|
||||||
|
stream.AddReceiver(w)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (apiv1 *APIv1) messages(w http.ResponseWriter, req *http.Request) {
|
||||||
|
log.Println("[APIv1] GET /api/v1/messages")
|
||||||
|
|
||||||
|
apiv1.defaultOptions(w, req)
|
||||||
|
|
||||||
|
// TODO start, limit
|
||||||
|
switch apiv1.config.Storage.(type) {
|
||||||
|
case *storage.MongoDB:
|
||||||
|
messages, _ := apiv1.config.Storage.(*storage.MongoDB).List(0, 1000)
|
||||||
|
bytes, _ := json.Marshal(messages)
|
||||||
|
w.Header().Add("Content-Type", "text/json")
|
||||||
|
w.Write(bytes)
|
||||||
|
case *storage.InMemory:
|
||||||
|
messages, _ := apiv1.config.Storage.(*storage.InMemory).List(0, 1000)
|
||||||
|
bytes, _ := json.Marshal(messages)
|
||||||
|
w.Header().Add("Content-Type", "text/json")
|
||||||
|
w.Write(bytes)
|
||||||
|
default:
|
||||||
|
w.WriteHeader(500)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (apiv1 *APIv1) message(w http.ResponseWriter, req *http.Request) {
|
||||||
|
id := req.URL.Query().Get(":id")
|
||||||
|
log.Printf("[APIv1] GET /api/v1/messages/%s\n", id)
|
||||||
|
|
||||||
|
apiv1.defaultOptions(w, req)
|
||||||
|
|
||||||
|
message, err := apiv1.config.Storage.Load(id)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("- Error: %s", err)
|
||||||
|
w.WriteHeader(500)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
bytes, err := json.Marshal(message)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("- Error: %s", err)
|
||||||
|
w.WriteHeader(500)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
w.Header().Set("Content-Type", "text/json")
|
||||||
|
w.Write(bytes)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (apiv1 *APIv1) download(w http.ResponseWriter, req *http.Request) {
|
||||||
|
id := req.URL.Query().Get(":id")
|
||||||
|
log.Printf("[APIv1] GET /api/v1/messages/%s\n", id)
|
||||||
|
|
||||||
|
apiv1.defaultOptions(w, req)
|
||||||
|
|
||||||
|
w.Header().Set("Content-Type", "message/rfc822")
|
||||||
|
w.Header().Set("Content-Disposition", "attachment; filename=\""+id+".eml\"")
|
||||||
|
|
||||||
|
switch apiv1.config.Storage.(type) {
|
||||||
|
case *storage.MongoDB:
|
||||||
|
message, _ := apiv1.config.Storage.(*storage.MongoDB).Load(id)
|
||||||
|
for h, l := range message.Content.Headers {
|
||||||
|
for _, v := range l {
|
||||||
|
w.Write([]byte(h + ": " + v + "\r\n"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
w.Write([]byte("\r\n" + message.Content.Body))
|
||||||
|
case *storage.InMemory:
|
||||||
|
message, _ := apiv1.config.Storage.(*storage.InMemory).Load(id)
|
||||||
|
for h, l := range message.Content.Headers {
|
||||||
|
for _, v := range l {
|
||||||
|
w.Write([]byte(h + ": " + v + "\r\n"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
w.Write([]byte("\r\n" + message.Content.Body))
|
||||||
|
default:
|
||||||
|
w.WriteHeader(500)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (apiv1 *APIv1) download_part(w http.ResponseWriter, req *http.Request) {
|
||||||
|
id := req.URL.Query().Get(":id")
|
||||||
|
part := req.URL.Query().Get(":part")
|
||||||
|
log.Printf("[APIv1] GET /api/v1/messages/%s/mime/part/%s/download\n", id, part)
|
||||||
|
|
||||||
|
// TODO extension from content-type?
|
||||||
|
apiv1.defaultOptions(w, req)
|
||||||
|
|
||||||
|
w.Header().Set("Content-Disposition", "attachment; filename=\""+id+"-part-"+part+"\"")
|
||||||
|
|
||||||
|
message, _ := apiv1.config.Storage.Load(id)
|
||||||
|
contentTransferEncoding := ""
|
||||||
|
pid, _ := strconv.Atoi(part)
|
||||||
|
for h, l := range message.MIME.Parts[pid].Headers {
|
||||||
|
for _, v := range l {
|
||||||
|
switch strings.ToLower(h) {
|
||||||
|
case "content-disposition":
|
||||||
|
// Prevent duplicate "content-disposition"
|
||||||
|
w.Header().Set(h, v)
|
||||||
|
case "content-transfer-encoding":
|
||||||
|
if contentTransferEncoding == "" {
|
||||||
|
contentTransferEncoding = v
|
||||||
|
}
|
||||||
|
fallthrough
|
||||||
|
default:
|
||||||
|
w.Header().Add(h, v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
body := []byte(message.MIME.Parts[pid].Body)
|
||||||
|
if strings.ToLower(contentTransferEncoding) == "base64" {
|
||||||
|
var e error
|
||||||
|
body, e = base64.StdEncoding.DecodeString(message.MIME.Parts[pid].Body)
|
||||||
|
if e != nil {
|
||||||
|
log.Printf("[APIv1] Decoding base64 encoded body failed: %s", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
w.Write(body)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (apiv1 *APIv1) delete_all(w http.ResponseWriter, req *http.Request) {
|
||||||
|
log.Println("[APIv1] POST /api/v1/messages")
|
||||||
|
|
||||||
|
apiv1.defaultOptions(w, req)
|
||||||
|
|
||||||
|
w.Header().Add("Content-Type", "text/json")
|
||||||
|
|
||||||
|
err := apiv1.config.Storage.DeleteAll()
|
||||||
|
if err != nil {
|
||||||
|
log.Println(err)
|
||||||
|
w.WriteHeader(500)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
w.WriteHeader(200)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (apiv1 *APIv1) release_one(w http.ResponseWriter, req *http.Request) {
|
||||||
|
id := req.URL.Query().Get(":id")
|
||||||
|
log.Printf("[APIv1] POST /api/v1/messages/%s/release\n", id)
|
||||||
|
|
||||||
|
apiv1.defaultOptions(w, req)
|
||||||
|
|
||||||
|
w.Header().Add("Content-Type", "text/json")
|
||||||
|
msg, _ := apiv1.config.Storage.Load(id)
|
||||||
|
|
||||||
|
decoder := json.NewDecoder(req.Body)
|
||||||
|
var cfg ReleaseConfig
|
||||||
|
err := decoder.Decode(&cfg)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Error decoding request body: %s", err)
|
||||||
|
w.WriteHeader(500)
|
||||||
|
w.Write([]byte("Error decoding request body"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("%+v", cfg)
|
||||||
|
|
||||||
|
log.Printf("Got message: %s", msg.ID)
|
||||||
|
|
||||||
|
if cfg.Save {
|
||||||
|
if _, ok := apiv1.config.OutgoingSMTP[cfg.Name]; ok {
|
||||||
|
log.Printf("Server already exists named %s", cfg.Name)
|
||||||
|
w.WriteHeader(400)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
cf := config.OutgoingSMTP(cfg)
|
||||||
|
apiv1.config.OutgoingSMTP[cfg.Name] = &cf
|
||||||
|
log.Printf("Saved server with name %s", cfg.Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(cfg.Name) > 0 {
|
||||||
|
if c, ok := apiv1.config.OutgoingSMTP[cfg.Name]; ok {
|
||||||
|
log.Printf("Using server with name: %s", cfg.Name)
|
||||||
|
cfg.Name = c.Name
|
||||||
|
if len(cfg.Email) == 0 {
|
||||||
|
cfg.Email = c.Email
|
||||||
|
}
|
||||||
|
cfg.Host = c.Host
|
||||||
|
cfg.Port = c.Port
|
||||||
|
cfg.Username = c.Username
|
||||||
|
cfg.Password = c.Password
|
||||||
|
cfg.Mechanism = c.Mechanism
|
||||||
|
} else {
|
||||||
|
log.Printf("Server not found: %s", cfg.Name)
|
||||||
|
w.WriteHeader(400)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("Releasing to %s (via %s:%s)", cfg.Email, cfg.Host, cfg.Port)
|
||||||
|
|
||||||
|
bytes := make([]byte, 0)
|
||||||
|
for h, l := range msg.Content.Headers {
|
||||||
|
for _, v := range l {
|
||||||
|
bytes = append(bytes, []byte(h+": "+v+"\r\n")...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
bytes = append(bytes, []byte("\r\n"+msg.Content.Body)...)
|
||||||
|
|
||||||
|
var auth smtp.Auth
|
||||||
|
|
||||||
|
if len(cfg.Username) > 0 || len(cfg.Password) > 0 {
|
||||||
|
log.Printf("Found username/password, using auth mechanism: [%s]", cfg.Mechanism)
|
||||||
|
switch cfg.Mechanism {
|
||||||
|
case "CRAMMD5":
|
||||||
|
auth = smtp.CRAMMD5Auth(cfg.Username, cfg.Password)
|
||||||
|
case "PLAIN":
|
||||||
|
auth = smtp.PlainAuth("", cfg.Username, cfg.Password, cfg.Host)
|
||||||
|
default:
|
||||||
|
log.Printf("Error - invalid authentication mechanism")
|
||||||
|
w.WriteHeader(400)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
err = smtp.SendMail(cfg.Host+":"+cfg.Port, auth, "nobody@"+apiv1.config.Hostname, []string{cfg.Email}, bytes)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Failed to release message: %s", err)
|
||||||
|
w.WriteHeader(500)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
log.Printf("Message released successfully")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (apiv1 *APIv1) delete_one(w http.ResponseWriter, req *http.Request) {
|
||||||
|
id := req.URL.Query().Get(":id")
|
||||||
|
|
||||||
|
log.Printf("[APIv1] POST /api/v1/messages/%s/delete\n", id)
|
||||||
|
|
||||||
|
apiv1.defaultOptions(w, req)
|
||||||
|
|
||||||
|
w.Header().Add("Content-Type", "text/json")
|
||||||
|
err := apiv1.config.Storage.DeleteOne(id)
|
||||||
|
if err != nil {
|
||||||
|
log.Println(err)
|
||||||
|
w.WriteHeader(500)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
w.WriteHeader(200)
|
||||||
|
}
|
258
vendor/github.com/mailhog/MailHog-Server/api/v2.go
generated
vendored
Normal file
258
vendor/github.com/mailhog/MailHog-Server/api/v2.go
generated
vendored
Normal file
|
@ -0,0 +1,258 @@
|
||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"github.com/gorilla/pat"
|
||||||
|
"github.com/ian-kent/go-log/log"
|
||||||
|
"github.com/mailhog/MailHog-Server/config"
|
||||||
|
"github.com/mailhog/MailHog-Server/monkey"
|
||||||
|
"github.com/mailhog/MailHog-Server/websockets"
|
||||||
|
"github.com/mailhog/data"
|
||||||
|
)
|
||||||
|
|
||||||
|
// APIv2 implements version 2 of the MailHog API
|
||||||
|
//
|
||||||
|
// It is currently experimental and may change in future releases.
|
||||||
|
// Use APIv1 for guaranteed compatibility.
|
||||||
|
type APIv2 struct {
|
||||||
|
config *config.Config
|
||||||
|
messageChan chan *data.Message
|
||||||
|
wsHub *websockets.Hub
|
||||||
|
}
|
||||||
|
|
||||||
|
func createAPIv2(conf *config.Config, r *pat.Router) *APIv2 {
|
||||||
|
log.Println("Creating API v2 with WebPath: " + conf.WebPath)
|
||||||
|
apiv2 := &APIv2{
|
||||||
|
config: conf,
|
||||||
|
messageChan: make(chan *data.Message),
|
||||||
|
wsHub: websockets.NewHub(),
|
||||||
|
}
|
||||||
|
|
||||||
|
r.Path(conf.WebPath + "/api/v2/messages").Methods("GET").HandlerFunc(apiv2.messages)
|
||||||
|
r.Path(conf.WebPath + "/api/v2/messages").Methods("OPTIONS").HandlerFunc(apiv2.defaultOptions)
|
||||||
|
|
||||||
|
r.Path(conf.WebPath + "/api/v2/search").Methods("GET").HandlerFunc(apiv2.search)
|
||||||
|
r.Path(conf.WebPath + "/api/v2/search").Methods("OPTIONS").HandlerFunc(apiv2.defaultOptions)
|
||||||
|
|
||||||
|
r.Path(conf.WebPath + "/api/v2/jim").Methods("GET").HandlerFunc(apiv2.jim)
|
||||||
|
r.Path(conf.WebPath + "/api/v2/jim").Methods("POST").HandlerFunc(apiv2.createJim)
|
||||||
|
r.Path(conf.WebPath + "/api/v2/jim").Methods("PUT").HandlerFunc(apiv2.updateJim)
|
||||||
|
r.Path(conf.WebPath + "/api/v2/jim").Methods("DELETE").HandlerFunc(apiv2.deleteJim)
|
||||||
|
r.Path(conf.WebPath + "/api/v2/jim").Methods("OPTIONS").HandlerFunc(apiv2.defaultOptions)
|
||||||
|
|
||||||
|
r.Path(conf.WebPath + "/api/v2/outgoing-smtp").Methods("GET").HandlerFunc(apiv2.listOutgoingSMTP)
|
||||||
|
r.Path(conf.WebPath + "/api/v2/outgoing-smtp").Methods("OPTIONS").HandlerFunc(apiv2.defaultOptions)
|
||||||
|
|
||||||
|
r.Path(conf.WebPath + "/api/v2/websocket").Methods("GET").HandlerFunc(apiv2.websocket)
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case msg := <-apiv2.messageChan:
|
||||||
|
log.Println("Got message in APIv2 websocket channel")
|
||||||
|
apiv2.broadcast(msg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
return apiv2
|
||||||
|
}
|
||||||
|
|
||||||
|
func (apiv2 *APIv2) defaultOptions(w http.ResponseWriter, req *http.Request) {
|
||||||
|
if len(apiv2.config.CORSOrigin) > 0 {
|
||||||
|
w.Header().Add("Access-Control-Allow-Origin", apiv2.config.CORSOrigin)
|
||||||
|
w.Header().Add("Access-Control-Allow-Methods", "OPTIONS,GET,PUT,POST,DELETE")
|
||||||
|
w.Header().Add("Access-Control-Allow-Headers", "Content-Type")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type messagesResult struct {
|
||||||
|
Total int `json:"total"`
|
||||||
|
Count int `json:"count"`
|
||||||
|
Start int `json:"start"`
|
||||||
|
Items []data.Message `json:"items"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (apiv2 *APIv2) getStartLimit(w http.ResponseWriter, req *http.Request) (start, limit int) {
|
||||||
|
start = 0
|
||||||
|
limit = 50
|
||||||
|
|
||||||
|
s := req.URL.Query().Get("start")
|
||||||
|
if n, e := strconv.ParseInt(s, 10, 64); e == nil && n > 0 {
|
||||||
|
start = int(n)
|
||||||
|
}
|
||||||
|
|
||||||
|
l := req.URL.Query().Get("limit")
|
||||||
|
if n, e := strconv.ParseInt(l, 10, 64); e == nil && n > 0 {
|
||||||
|
if n > 250 {
|
||||||
|
n = 250
|
||||||
|
}
|
||||||
|
limit = int(n)
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (apiv2 *APIv2) messages(w http.ResponseWriter, req *http.Request) {
|
||||||
|
log.Println("[APIv2] GET /api/v2/messages")
|
||||||
|
|
||||||
|
apiv2.defaultOptions(w, req)
|
||||||
|
|
||||||
|
start, limit := apiv2.getStartLimit(w, req)
|
||||||
|
|
||||||
|
var res messagesResult
|
||||||
|
|
||||||
|
messages, err := apiv2.config.Storage.List(start, limit)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
res.Count = len([]data.Message(*messages))
|
||||||
|
res.Start = start
|
||||||
|
res.Items = []data.Message(*messages)
|
||||||
|
res.Total = apiv2.config.Storage.Count()
|
||||||
|
|
||||||
|
bytes, _ := json.Marshal(res)
|
||||||
|
w.Header().Add("Content-Type", "text/json")
|
||||||
|
w.Write(bytes)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (apiv2 *APIv2) search(w http.ResponseWriter, req *http.Request) {
|
||||||
|
log.Println("[APIv2] GET /api/v2/search")
|
||||||
|
|
||||||
|
apiv2.defaultOptions(w, req)
|
||||||
|
|
||||||
|
start, limit := apiv2.getStartLimit(w, req)
|
||||||
|
|
||||||
|
kind := req.URL.Query().Get("kind")
|
||||||
|
if kind != "from" && kind != "to" && kind != "containing" {
|
||||||
|
w.WriteHeader(400)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
query := req.URL.Query().Get("query")
|
||||||
|
if len(query) == 0 {
|
||||||
|
w.WriteHeader(400)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var res messagesResult
|
||||||
|
|
||||||
|
messages, total, _ := apiv2.config.Storage.Search(kind, query, start, limit)
|
||||||
|
|
||||||
|
res.Count = len([]data.Message(*messages))
|
||||||
|
res.Start = start
|
||||||
|
res.Items = []data.Message(*messages)
|
||||||
|
res.Total = total
|
||||||
|
|
||||||
|
b, _ := json.Marshal(res)
|
||||||
|
w.Header().Add("Content-Type", "application/json")
|
||||||
|
w.Write(b)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (apiv2 *APIv2) jim(w http.ResponseWriter, req *http.Request) {
|
||||||
|
log.Println("[APIv2] GET /api/v2/jim")
|
||||||
|
|
||||||
|
apiv2.defaultOptions(w, req)
|
||||||
|
|
||||||
|
if apiv2.config.Monkey == nil {
|
||||||
|
w.WriteHeader(404)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
b, _ := json.Marshal(apiv2.config.Monkey)
|
||||||
|
w.Header().Add("Content-Type", "application/json")
|
||||||
|
w.Write(b)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (apiv2 *APIv2) deleteJim(w http.ResponseWriter, req *http.Request) {
|
||||||
|
log.Println("[APIv2] DELETE /api/v2/jim")
|
||||||
|
|
||||||
|
apiv2.defaultOptions(w, req)
|
||||||
|
|
||||||
|
if apiv2.config.Monkey == nil {
|
||||||
|
w.WriteHeader(404)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
apiv2.config.Monkey = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (apiv2 *APIv2) createJim(w http.ResponseWriter, req *http.Request) {
|
||||||
|
log.Println("[APIv2] POST /api/v2/jim")
|
||||||
|
|
||||||
|
apiv2.defaultOptions(w, req)
|
||||||
|
|
||||||
|
if apiv2.config.Monkey != nil {
|
||||||
|
w.WriteHeader(400)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
apiv2.config.Monkey = config.Jim
|
||||||
|
|
||||||
|
// Try, but ignore errors
|
||||||
|
// Could be better (e.g., ok if no json, error if badly formed json)
|
||||||
|
// but this works for now
|
||||||
|
apiv2.newJimFromBody(w, req)
|
||||||
|
|
||||||
|
w.WriteHeader(201)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (apiv2 *APIv2) newJimFromBody(w http.ResponseWriter, req *http.Request) error {
|
||||||
|
var jim monkey.Jim
|
||||||
|
|
||||||
|
dec := json.NewDecoder(req.Body)
|
||||||
|
err := dec.Decode(&jim)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
jim.ConfigureFrom(config.Jim)
|
||||||
|
|
||||||
|
config.Jim = &jim
|
||||||
|
apiv2.config.Monkey = &jim
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (apiv2 *APIv2) updateJim(w http.ResponseWriter, req *http.Request) {
|
||||||
|
log.Println("[APIv2] PUT /api/v2/jim")
|
||||||
|
|
||||||
|
apiv2.defaultOptions(w, req)
|
||||||
|
|
||||||
|
if apiv2.config.Monkey == nil {
|
||||||
|
w.WriteHeader(404)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err := apiv2.newJimFromBody(w, req)
|
||||||
|
if err != nil {
|
||||||
|
w.WriteHeader(400)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (apiv2 *APIv2) listOutgoingSMTP(w http.ResponseWriter, req *http.Request) {
|
||||||
|
log.Println("[APIv2] GET /api/v2/outgoing-smtp")
|
||||||
|
|
||||||
|
apiv2.defaultOptions(w, req)
|
||||||
|
|
||||||
|
b, _ := json.Marshal(apiv2.config.OutgoingSMTP)
|
||||||
|
w.Header().Add("Content-Type", "application/json")
|
||||||
|
w.Write(b)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (apiv2 *APIv2) websocket(w http.ResponseWriter, req *http.Request) {
|
||||||
|
log.Println("[APIv2] GET /api/v2/websocket")
|
||||||
|
|
||||||
|
apiv2.wsHub.Serve(w, req)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (apiv2 *APIv2) broadcast(msg *data.Message) {
|
||||||
|
log.Println("[APIv2] BROADCAST /api/v2/websocket")
|
||||||
|
|
||||||
|
apiv2.wsHub.Broadcast(msg)
|
||||||
|
}
|
132
vendor/github.com/mailhog/MailHog-Server/config/config.go
generated
vendored
Normal file
132
vendor/github.com/mailhog/MailHog-Server/config/config.go
generated
vendored
Normal file
|
@ -0,0 +1,132 @@
|
||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"flag"
|
||||||
|
"io/ioutil"
|
||||||
|
"log"
|
||||||
|
|
||||||
|
"github.com/ian-kent/envconf"
|
||||||
|
"github.com/mailhog/MailHog-Server/monkey"
|
||||||
|
"github.com/mailhog/data"
|
||||||
|
"github.com/mailhog/storage"
|
||||||
|
)
|
||||||
|
|
||||||
|
// DefaultConfig is the default config
|
||||||
|
func DefaultConfig() *Config {
|
||||||
|
return &Config{
|
||||||
|
SMTPBindAddr: "0.0.0.0:1025",
|
||||||
|
APIBindAddr: "0.0.0.0:8025",
|
||||||
|
Hostname: "mailhog.example",
|
||||||
|
MongoURI: "127.0.0.1:27017",
|
||||||
|
MongoDb: "mailhog",
|
||||||
|
MongoColl: "messages",
|
||||||
|
MaildirPath: "",
|
||||||
|
StorageType: "memory",
|
||||||
|
CORSOrigin: "",
|
||||||
|
WebPath: "",
|
||||||
|
MessageChan: make(chan *data.Message),
|
||||||
|
OutgoingSMTP: make(map[string]*OutgoingSMTP),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Config is the config, kind of
|
||||||
|
type Config struct {
|
||||||
|
SMTPBindAddr string
|
||||||
|
APIBindAddr string
|
||||||
|
Hostname string
|
||||||
|
MongoURI string
|
||||||
|
MongoDb string
|
||||||
|
MongoColl string
|
||||||
|
StorageType string
|
||||||
|
CORSOrigin string
|
||||||
|
MaildirPath string
|
||||||
|
InviteJim bool
|
||||||
|
Storage storage.Storage
|
||||||
|
MessageChan chan *data.Message
|
||||||
|
Assets func(asset string) ([]byte, error)
|
||||||
|
Monkey monkey.ChaosMonkey
|
||||||
|
OutgoingSMTPFile string
|
||||||
|
OutgoingSMTP map[string]*OutgoingSMTP
|
||||||
|
WebPath string
|
||||||
|
}
|
||||||
|
|
||||||
|
// OutgoingSMTP is an outgoing SMTP server config
|
||||||
|
type OutgoingSMTP struct {
|
||||||
|
Name string
|
||||||
|
Save bool
|
||||||
|
Email string
|
||||||
|
Host string
|
||||||
|
Port string
|
||||||
|
Username string
|
||||||
|
Password string
|
||||||
|
Mechanism string
|
||||||
|
}
|
||||||
|
|
||||||
|
var cfg = DefaultConfig()
|
||||||
|
|
||||||
|
// Jim is a monkey
|
||||||
|
var Jim = &monkey.Jim{}
|
||||||
|
|
||||||
|
// Configure configures stuff
|
||||||
|
func Configure() *Config {
|
||||||
|
switch cfg.StorageType {
|
||||||
|
case "memory":
|
||||||
|
log.Println("Using in-memory storage")
|
||||||
|
cfg.Storage = storage.CreateInMemory()
|
||||||
|
case "mongodb":
|
||||||
|
log.Println("Using MongoDB message storage")
|
||||||
|
s := storage.CreateMongoDB(cfg.MongoURI, cfg.MongoDb, cfg.MongoColl)
|
||||||
|
if s == nil {
|
||||||
|
log.Println("MongoDB storage unavailable, reverting to in-memory storage")
|
||||||
|
cfg.Storage = storage.CreateInMemory()
|
||||||
|
} else {
|
||||||
|
log.Println("Connected to MongoDB")
|
||||||
|
cfg.Storage = s
|
||||||
|
}
|
||||||
|
case "maildir":
|
||||||
|
log.Println("Using maildir message storage")
|
||||||
|
s := storage.CreateMaildir(cfg.MaildirPath)
|
||||||
|
cfg.Storage = s
|
||||||
|
default:
|
||||||
|
log.Fatalf("Invalid storage type %s", cfg.StorageType)
|
||||||
|
}
|
||||||
|
|
||||||
|
Jim.Configure(func(message string, args ...interface{}) {
|
||||||
|
log.Printf(message, args...)
|
||||||
|
})
|
||||||
|
if cfg.InviteJim {
|
||||||
|
cfg.Monkey = Jim
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(cfg.OutgoingSMTPFile) > 0 {
|
||||||
|
b, err := ioutil.ReadFile(cfg.OutgoingSMTPFile)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
var o map[string]*OutgoingSMTP
|
||||||
|
err = json.Unmarshal(b, &o)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
cfg.OutgoingSMTP = o
|
||||||
|
}
|
||||||
|
|
||||||
|
return cfg
|
||||||
|
}
|
||||||
|
|
||||||
|
// RegisterFlags registers flags
|
||||||
|
func RegisterFlags() {
|
||||||
|
flag.StringVar(&cfg.SMTPBindAddr, "smtp-bind-addr", envconf.FromEnvP("MH_SMTP_BIND_ADDR", "0.0.0.0:1025").(string), "SMTP bind interface and port, e.g. 0.0.0.0:1025 or just :1025")
|
||||||
|
flag.StringVar(&cfg.APIBindAddr, "api-bind-addr", envconf.FromEnvP("MH_API_BIND_ADDR", "0.0.0.0:8025").(string), "HTTP bind interface and port for API, e.g. 0.0.0.0:8025 or just :8025")
|
||||||
|
flag.StringVar(&cfg.Hostname, "hostname", envconf.FromEnvP("MH_HOSTNAME", "mailhog.example").(string), "Hostname for EHLO/HELO response, e.g. mailhog.example")
|
||||||
|
flag.StringVar(&cfg.StorageType, "storage", envconf.FromEnvP("MH_STORAGE", "memory").(string), "Message storage: 'memory' (default), 'mongodb' or 'maildir'")
|
||||||
|
flag.StringVar(&cfg.MongoURI, "mongo-uri", envconf.FromEnvP("MH_MONGO_URI", "127.0.0.1:27017").(string), "MongoDB URI, e.g. 127.0.0.1:27017")
|
||||||
|
flag.StringVar(&cfg.MongoDb, "mongo-db", envconf.FromEnvP("MH_MONGO_DB", "mailhog").(string), "MongoDB database, e.g. mailhog")
|
||||||
|
flag.StringVar(&cfg.MongoColl, "mongo-coll", envconf.FromEnvP("MH_MONGO_COLLECTION", "messages").(string), "MongoDB collection, e.g. messages")
|
||||||
|
flag.StringVar(&cfg.CORSOrigin, "cors-origin", envconf.FromEnvP("MH_CORS_ORIGIN", "").(string), "CORS Access-Control-Allow-Origin header for API endpoints")
|
||||||
|
flag.StringVar(&cfg.MaildirPath, "maildir-path", envconf.FromEnvP("MH_MAILDIR_PATH", "").(string), "Maildir path (if storage type is 'maildir')")
|
||||||
|
flag.BoolVar(&cfg.InviteJim, "invite-jim", envconf.FromEnvP("MH_INVITE_JIM", false).(bool), "Decide whether to invite Jim (beware, he causes trouble)")
|
||||||
|
flag.StringVar(&cfg.OutgoingSMTPFile, "outgoing-smtp", envconf.FromEnvP("MH_OUTGOING_SMTP", "").(string), "JSON file containing outgoing SMTP servers")
|
||||||
|
Jim.RegisterFlags()
|
||||||
|
}
|
51
vendor/github.com/mailhog/MailHog-Server/main.go
generated
vendored
Normal file
51
vendor/github.com/mailhog/MailHog-Server/main.go
generated
vendored
Normal file
|
@ -0,0 +1,51 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"flag"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
gohttp "net/http"
|
||||||
|
|
||||||
|
"github.com/ian-kent/go-log/log"
|
||||||
|
"github.com/mailhog/MailHog-Server/api"
|
||||||
|
"github.com/mailhog/MailHog-Server/config"
|
||||||
|
"github.com/mailhog/MailHog-Server/smtp"
|
||||||
|
"github.com/mailhog/MailHog-UI/assets"
|
||||||
|
comcfg "github.com/mailhog/MailHog/config"
|
||||||
|
"github.com/mailhog/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
var conf *config.Config
|
||||||
|
var comconf *comcfg.Config
|
||||||
|
var exitCh chan int
|
||||||
|
|
||||||
|
func configure() {
|
||||||
|
comcfg.RegisterFlags()
|
||||||
|
config.RegisterFlags()
|
||||||
|
flag.Parse()
|
||||||
|
conf = config.Configure()
|
||||||
|
comconf = comcfg.Configure()
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
configure()
|
||||||
|
|
||||||
|
if comconf.AuthFile != "" {
|
||||||
|
http.AuthFile(comconf.AuthFile)
|
||||||
|
}
|
||||||
|
|
||||||
|
exitCh = make(chan int)
|
||||||
|
cb := func(r gohttp.Handler) {
|
||||||
|
api.CreateAPI(conf, r)
|
||||||
|
}
|
||||||
|
go http.Listen(conf.APIBindAddr, assets.Asset, exitCh, cb)
|
||||||
|
go smtp.Listen(conf, exitCh)
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-exitCh:
|
||||||
|
log.Printf("Received exit signal")
|
||||||
|
os.Exit(0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
111
vendor/github.com/mailhog/MailHog-Server/monkey/jim.go
generated
vendored
Normal file
111
vendor/github.com/mailhog/MailHog-Server/monkey/jim.go
generated
vendored
Normal file
|
@ -0,0 +1,111 @@
|
||||||
|
package monkey
|
||||||
|
|
||||||
|
import (
|
||||||
|
"flag"
|
||||||
|
"math/rand"
|
||||||
|
"net"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/ian-kent/linkio"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Jim is a chaos monkey
|
||||||
|
type Jim struct {
|
||||||
|
DisconnectChance float64
|
||||||
|
AcceptChance float64
|
||||||
|
LinkSpeedAffect float64
|
||||||
|
LinkSpeedMin float64
|
||||||
|
LinkSpeedMax float64
|
||||||
|
RejectSenderChance float64
|
||||||
|
RejectRecipientChance float64
|
||||||
|
RejectAuthChance float64
|
||||||
|
logf func(message string, args ...interface{})
|
||||||
|
}
|
||||||
|
|
||||||
|
// RegisterFlags implements ChaosMonkey.RegisterFlags
|
||||||
|
func (j *Jim) RegisterFlags() {
|
||||||
|
flag.Float64Var(&j.DisconnectChance, "jim-disconnect", 0.005, "Chance of disconnect")
|
||||||
|
flag.Float64Var(&j.AcceptChance, "jim-accept", 0.99, "Chance of accept")
|
||||||
|
flag.Float64Var(&j.LinkSpeedAffect, "jim-linkspeed-affect", 0.1, "Chance of affecting link speed")
|
||||||
|
flag.Float64Var(&j.LinkSpeedMin, "jim-linkspeed-min", 1024, "Minimum link speed (in bytes per second)")
|
||||||
|
flag.Float64Var(&j.LinkSpeedMax, "jim-linkspeed-max", 10240, "Maximum link speed (in bytes per second)")
|
||||||
|
flag.Float64Var(&j.RejectSenderChance, "jim-reject-sender", 0.05, "Chance of rejecting a sender (MAIL FROM)")
|
||||||
|
flag.Float64Var(&j.RejectRecipientChance, "jim-reject-recipient", 0.05, "Chance of rejecting a recipient (RCPT TO)")
|
||||||
|
flag.Float64Var(&j.RejectAuthChance, "jim-reject-auth", 0.05, "Chance of rejecting authentication (AUTH)")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Configure implements ChaosMonkey.Configure
|
||||||
|
func (j *Jim) Configure(logf func(string, ...interface{})) {
|
||||||
|
j.logf = logf
|
||||||
|
rand.Seed(time.Now().Unix())
|
||||||
|
}
|
||||||
|
|
||||||
|
// ConfigureFrom lets us configure a new Jim from an old one without
|
||||||
|
// having to expose logf (and any other future private vars)
|
||||||
|
func (j *Jim) ConfigureFrom(j2 *Jim) {
|
||||||
|
j.Configure(j2.logf)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Accept implements ChaosMonkey.Accept
|
||||||
|
func (j *Jim) Accept(conn net.Conn) bool {
|
||||||
|
if rand.Float64() > j.AcceptChance {
|
||||||
|
j.logf("Jim: Rejecting connection\n")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
j.logf("Jim: Allowing connection\n")
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// LinkSpeed implements ChaosMonkey.LinkSpeed
|
||||||
|
func (j *Jim) LinkSpeed() *linkio.Throughput {
|
||||||
|
rand.Seed(time.Now().Unix())
|
||||||
|
if rand.Float64() < j.LinkSpeedAffect {
|
||||||
|
lsDiff := j.LinkSpeedMax - j.LinkSpeedMin
|
||||||
|
lsAffect := j.LinkSpeedMin + (lsDiff * rand.Float64())
|
||||||
|
f := linkio.Throughput(lsAffect) * linkio.BytePerSecond
|
||||||
|
j.logf("Jim: Restricting throughput to %s\n", f)
|
||||||
|
return &f
|
||||||
|
}
|
||||||
|
j.logf("Jim: Allowing unrestricted throughput")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ValidRCPT implements ChaosMonkey.ValidRCPT
|
||||||
|
func (j *Jim) ValidRCPT(rcpt string) bool {
|
||||||
|
if rand.Float64() < j.RejectRecipientChance {
|
||||||
|
j.logf("Jim: Rejecting recipient %s\n", rcpt)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
j.logf("Jim: Allowing recipient%s\n", rcpt)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// ValidMAIL implements ChaosMonkey.ValidMAIL
|
||||||
|
func (j *Jim) ValidMAIL(mail string) bool {
|
||||||
|
if rand.Float64() < j.RejectSenderChance {
|
||||||
|
j.logf("Jim: Rejecting sender %s\n", mail)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
j.logf("Jim: Allowing sender %s\n", mail)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// ValidAUTH implements ChaosMonkey.ValidAUTH
|
||||||
|
func (j *Jim) ValidAUTH(mechanism string, args ...string) bool {
|
||||||
|
if rand.Float64() < j.RejectAuthChance {
|
||||||
|
j.logf("Jim: Rejecting authentication %s: %s\n", mechanism, args)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
j.logf("Jim: Allowing authentication %s: %s\n", mechanism, args)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Disconnect implements ChaosMonkey.Disconnect
|
||||||
|
func (j *Jim) Disconnect() bool {
|
||||||
|
if rand.Float64() < j.DisconnectChance {
|
||||||
|
j.logf("Jim: Being nasty, kicking them off\n")
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
j.logf("Jim: Being nice, letting them stay\n")
|
||||||
|
return false
|
||||||
|
}
|
28
vendor/github.com/mailhog/MailHog-Server/monkey/monkey.go
generated
vendored
Normal file
28
vendor/github.com/mailhog/MailHog-Server/monkey/monkey.go
generated
vendored
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
package monkey
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net"
|
||||||
|
|
||||||
|
"github.com/ian-kent/linkio"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ChaosMonkey should be implemented by chaos monkeys!
|
||||||
|
type ChaosMonkey interface {
|
||||||
|
RegisterFlags()
|
||||||
|
Configure(func(string, ...interface{}))
|
||||||
|
|
||||||
|
// Accept is called for each incoming connection. Returning false closes the connection.
|
||||||
|
Accept(conn net.Conn) bool
|
||||||
|
// LinkSpeed sets the maximum connection throughput (in one direction)
|
||||||
|
LinkSpeed() *linkio.Throughput
|
||||||
|
|
||||||
|
// ValidRCPT is called for the RCPT command. Returning false signals an invalid recipient.
|
||||||
|
ValidRCPT(rcpt string) bool
|
||||||
|
// ValidMAIL is called for the MAIL command. Returning false signals an invalid sender.
|
||||||
|
ValidMAIL(mail string) bool
|
||||||
|
// ValidAUTH is called after authentication. Returning false signals invalid authentication.
|
||||||
|
ValidAUTH(mechanism string, args ...string) bool
|
||||||
|
|
||||||
|
// Disconnect is called after every read. Returning true will close the connection.
|
||||||
|
Disconnect() bool
|
||||||
|
}
|
163
vendor/github.com/mailhog/MailHog-Server/smtp/session.go
generated
vendored
Normal file
163
vendor/github.com/mailhog/MailHog-Server/smtp/session.go
generated
vendored
Normal file
|
@ -0,0 +1,163 @@
|
||||||
|
package smtp
|
||||||
|
|
||||||
|
// http://www.rfc-editor.org/rfc/rfc5321.txt
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io"
|
||||||
|
"log"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/ian-kent/linkio"
|
||||||
|
"github.com/mailhog/MailHog-Server/monkey"
|
||||||
|
"github.com/mailhog/data"
|
||||||
|
"github.com/mailhog/smtp"
|
||||||
|
"github.com/mailhog/storage"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Session represents a SMTP session using net.TCPConn
|
||||||
|
type Session struct {
|
||||||
|
conn io.ReadWriteCloser
|
||||||
|
proto *smtp.Protocol
|
||||||
|
storage storage.Storage
|
||||||
|
messageChan chan *data.Message
|
||||||
|
remoteAddress string
|
||||||
|
isTLS bool
|
||||||
|
line string
|
||||||
|
link *linkio.Link
|
||||||
|
|
||||||
|
reader io.Reader
|
||||||
|
writer io.Writer
|
||||||
|
monkey monkey.ChaosMonkey
|
||||||
|
}
|
||||||
|
|
||||||
|
// Accept starts a new SMTP session using io.ReadWriteCloser
|
||||||
|
func Accept(remoteAddress string, conn io.ReadWriteCloser, storage storage.Storage, messageChan chan *data.Message, hostname string, monkey monkey.ChaosMonkey) {
|
||||||
|
defer conn.Close()
|
||||||
|
|
||||||
|
proto := smtp.NewProtocol()
|
||||||
|
proto.Hostname = hostname
|
||||||
|
var link *linkio.Link
|
||||||
|
reader := io.Reader(conn)
|
||||||
|
writer := io.Writer(conn)
|
||||||
|
if monkey != nil {
|
||||||
|
linkSpeed := monkey.LinkSpeed()
|
||||||
|
if linkSpeed != nil {
|
||||||
|
link = linkio.NewLink(*linkSpeed * linkio.BytePerSecond)
|
||||||
|
reader = link.NewLinkReader(io.Reader(conn))
|
||||||
|
writer = link.NewLinkWriter(io.Writer(conn))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
session := &Session{conn, proto, storage, messageChan, remoteAddress, false, "", link, reader, writer, monkey}
|
||||||
|
proto.LogHandler = session.logf
|
||||||
|
proto.MessageReceivedHandler = session.acceptMessage
|
||||||
|
proto.ValidateSenderHandler = session.validateSender
|
||||||
|
proto.ValidateRecipientHandler = session.validateRecipient
|
||||||
|
proto.ValidateAuthenticationHandler = session.validateAuthentication
|
||||||
|
proto.GetAuthenticationMechanismsHandler = func() []string { return []string{"PLAIN"} }
|
||||||
|
|
||||||
|
session.logf("Starting session")
|
||||||
|
session.Write(proto.Start())
|
||||||
|
for session.Read() == true {
|
||||||
|
if monkey != nil && monkey.Disconnect != nil && monkey.Disconnect() {
|
||||||
|
session.conn.Close()
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
session.logf("Session ended")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Session) validateAuthentication(mechanism string, args ...string) (errorReply *smtp.Reply, ok bool) {
|
||||||
|
if c.monkey != nil {
|
||||||
|
ok := c.monkey.ValidAUTH(mechanism, args...)
|
||||||
|
if !ok {
|
||||||
|
// FIXME better error?
|
||||||
|
return smtp.ReplyUnrecognisedCommand(), false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil, true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Session) validateRecipient(to string) bool {
|
||||||
|
if c.monkey != nil {
|
||||||
|
ok := c.monkey.ValidRCPT(to)
|
||||||
|
if !ok {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Session) validateSender(from string) bool {
|
||||||
|
if c.monkey != nil {
|
||||||
|
ok := c.monkey.ValidMAIL(from)
|
||||||
|
if !ok {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Session) acceptMessage(msg *data.SMTPMessage) (id string, err error) {
|
||||||
|
m := msg.Parse(c.proto.Hostname)
|
||||||
|
c.logf("Storing message %s", m.ID)
|
||||||
|
id, err = c.storage.Store(m)
|
||||||
|
c.messageChan <- m
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Session) logf(message string, args ...interface{}) {
|
||||||
|
message = strings.Join([]string{"[SMTP %s]", message}, " ")
|
||||||
|
args = append([]interface{}{c.remoteAddress}, args...)
|
||||||
|
log.Printf(message, args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read reads from the underlying net.TCPConn
|
||||||
|
func (c *Session) Read() bool {
|
||||||
|
buf := make([]byte, 1024)
|
||||||
|
n, err := c.reader.Read(buf)
|
||||||
|
|
||||||
|
if n == 0 {
|
||||||
|
c.logf("Connection closed by remote host\n")
|
||||||
|
io.Closer(c.conn).Close() // not sure this is necessary?
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
c.logf("Error reading from socket: %s\n", err)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
text := string(buf[0:n])
|
||||||
|
logText := strings.Replace(text, "\n", "\\n", -1)
|
||||||
|
logText = strings.Replace(logText, "\r", "\\r", -1)
|
||||||
|
c.logf("Received %d bytes: '%s'\n", n, logText)
|
||||||
|
|
||||||
|
c.line += text
|
||||||
|
|
||||||
|
for strings.Contains(c.line, "\r\n") {
|
||||||
|
line, reply := c.proto.Parse(c.line)
|
||||||
|
c.line = line
|
||||||
|
|
||||||
|
if reply != nil {
|
||||||
|
c.Write(reply)
|
||||||
|
if reply.Status == 221 {
|
||||||
|
io.Closer(c.conn).Close()
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write writes a reply to the underlying net.TCPConn
|
||||||
|
func (c *Session) Write(reply *smtp.Reply) {
|
||||||
|
lines := reply.Lines()
|
||||||
|
for _, l := range lines {
|
||||||
|
logText := strings.Replace(l, "\n", "\\n", -1)
|
||||||
|
logText = strings.Replace(logText, "\r", "\\r", -1)
|
||||||
|
c.logf("Sent %d bytes: '%s'", len(l), logText)
|
||||||
|
c.writer.Write([]byte(l))
|
||||||
|
}
|
||||||
|
}
|
141
vendor/github.com/mailhog/MailHog-Server/smtp/session_test.go
generated
vendored
Normal file
141
vendor/github.com/mailhog/MailHog-Server/smtp/session_test.go
generated
vendored
Normal file
|
@ -0,0 +1,141 @@
|
||||||
|
package smtp
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"sync"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
. "github.com/smartystreets/goconvey/convey"
|
||||||
|
|
||||||
|
"github.com/mailhog/data"
|
||||||
|
"github.com/mailhog/storage"
|
||||||
|
)
|
||||||
|
|
||||||
|
type fakeRw struct {
|
||||||
|
_read func(p []byte) (n int, err error)
|
||||||
|
_write func(p []byte) (n int, err error)
|
||||||
|
_close func() error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rw *fakeRw) Read(p []byte) (n int, err error) {
|
||||||
|
if rw._read != nil {
|
||||||
|
return rw._read(p)
|
||||||
|
}
|
||||||
|
return 0, nil
|
||||||
|
}
|
||||||
|
func (rw *fakeRw) Close() error {
|
||||||
|
if rw._close != nil {
|
||||||
|
return rw._close()
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
func (rw *fakeRw) Write(p []byte) (n int, err error) {
|
||||||
|
if rw._write != nil {
|
||||||
|
return rw._write(p)
|
||||||
|
}
|
||||||
|
return len(p), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAccept(t *testing.T) {
|
||||||
|
Convey("Accept should handle a connection", t, func() {
|
||||||
|
frw := &fakeRw{}
|
||||||
|
mChan := make(chan *data.Message)
|
||||||
|
Accept("1.1.1.1:11111", frw, storage.CreateInMemory(), mChan, "localhost", nil)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSocketError(t *testing.T) {
|
||||||
|
Convey("Socket errors should return from Accept", t, func() {
|
||||||
|
frw := &fakeRw{
|
||||||
|
_read: func(p []byte) (n int, err error) {
|
||||||
|
return -1, errors.New("OINK")
|
||||||
|
},
|
||||||
|
}
|
||||||
|
mChan := make(chan *data.Message)
|
||||||
|
Accept("1.1.1.1:11111", frw, storage.CreateInMemory(), mChan, "localhost", nil)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAcceptMessage(t *testing.T) {
|
||||||
|
Convey("acceptMessage should be called", t, func() {
|
||||||
|
mbuf := "EHLO localhost\nMAIL FROM:<test>\nRCPT TO:<test>\nDATA\nHi.\r\n.\r\nQUIT\n"
|
||||||
|
var rbuf []byte
|
||||||
|
frw := &fakeRw{
|
||||||
|
_read: func(p []byte) (n int, err error) {
|
||||||
|
if len(p) >= len(mbuf) {
|
||||||
|
ba := []byte(mbuf)
|
||||||
|
mbuf = ""
|
||||||
|
for i, b := range ba {
|
||||||
|
p[i] = b
|
||||||
|
}
|
||||||
|
return len(ba), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
ba := []byte(mbuf[0:len(p)])
|
||||||
|
mbuf = mbuf[len(p):]
|
||||||
|
for i, b := range ba {
|
||||||
|
p[i] = b
|
||||||
|
}
|
||||||
|
return len(ba), nil
|
||||||
|
},
|
||||||
|
_write: func(p []byte) (n int, err error) {
|
||||||
|
rbuf = append(rbuf, p...)
|
||||||
|
return len(p), nil
|
||||||
|
},
|
||||||
|
_close: func() error {
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
mChan := make(chan *data.Message)
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
wg.Add(1)
|
||||||
|
handlerCalled := false
|
||||||
|
go func() {
|
||||||
|
handlerCalled = true
|
||||||
|
<-mChan
|
||||||
|
//FIXME breaks some tests (in drone.io)
|
||||||
|
//m := <-mChan
|
||||||
|
//So(m, ShouldNotBeNil)
|
||||||
|
wg.Done()
|
||||||
|
}()
|
||||||
|
Accept("1.1.1.1:11111", frw, storage.CreateInMemory(), mChan, "localhost", nil)
|
||||||
|
wg.Wait()
|
||||||
|
So(handlerCalled, ShouldBeTrue)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestValidateAuthentication(t *testing.T) {
|
||||||
|
Convey("validateAuthentication is always successful", t, func() {
|
||||||
|
c := &Session{}
|
||||||
|
|
||||||
|
err, ok := c.validateAuthentication("OINK")
|
||||||
|
So(err, ShouldBeNil)
|
||||||
|
So(ok, ShouldBeTrue)
|
||||||
|
|
||||||
|
err, ok = c.validateAuthentication("OINK", "arg1")
|
||||||
|
So(err, ShouldBeNil)
|
||||||
|
So(ok, ShouldBeTrue)
|
||||||
|
|
||||||
|
err, ok = c.validateAuthentication("OINK", "arg1", "arg2")
|
||||||
|
So(err, ShouldBeNil)
|
||||||
|
So(ok, ShouldBeTrue)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestValidateRecipient(t *testing.T) {
|
||||||
|
Convey("validateRecipient is always successful", t, func() {
|
||||||
|
c := &Session{}
|
||||||
|
|
||||||
|
So(c.validateRecipient("OINK"), ShouldBeTrue)
|
||||||
|
So(c.validateRecipient("foo@bar.mailhog"), ShouldBeTrue)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestValidateSender(t *testing.T) {
|
||||||
|
Convey("validateSender is always successful", t, func() {
|
||||||
|
c := &Session{}
|
||||||
|
|
||||||
|
So(c.validateSender("OINK"), ShouldBeTrue)
|
||||||
|
So(c.validateSender("foo@bar.mailhog"), ShouldBeTrue)
|
||||||
|
})
|
||||||
|
}
|
43
vendor/github.com/mailhog/MailHog-Server/smtp/smtp.go
generated
vendored
Normal file
43
vendor/github.com/mailhog/MailHog-Server/smtp/smtp.go
generated
vendored
Normal file
|
@ -0,0 +1,43 @@
|
||||||
|
package smtp
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io"
|
||||||
|
"log"
|
||||||
|
"net"
|
||||||
|
|
||||||
|
"github.com/mailhog/MailHog-Server/config"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Listen(cfg *config.Config, exitCh chan int) *net.TCPListener {
|
||||||
|
log.Printf("[SMTP] Binding to address: %s\n", cfg.SMTPBindAddr)
|
||||||
|
ln, err := net.Listen("tcp", cfg.SMTPBindAddr)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("[SMTP] Error listening on socket: %s\n", err)
|
||||||
|
}
|
||||||
|
defer ln.Close()
|
||||||
|
|
||||||
|
for {
|
||||||
|
conn, err := ln.Accept()
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("[SMTP] Error accepting connection: %s\n", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if cfg.Monkey != nil {
|
||||||
|
ok := cfg.Monkey.Accept(conn)
|
||||||
|
if !ok {
|
||||||
|
conn.Close()
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
go Accept(
|
||||||
|
conn.(*net.TCPConn).RemoteAddr().String(),
|
||||||
|
io.ReadWriteCloser(conn),
|
||||||
|
cfg.Storage,
|
||||||
|
cfg.MessageChan,
|
||||||
|
cfg.Hostname,
|
||||||
|
cfg.Monkey,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
73
vendor/github.com/mailhog/MailHog-Server/websockets/connection.go
generated
vendored
Normal file
73
vendor/github.com/mailhog/MailHog-Server/websockets/connection.go
generated
vendored
Normal file
|
@ -0,0 +1,73 @@
|
||||||
|
package websockets
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/gorilla/websocket"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
// Time allowed to write a message to the peer.
|
||||||
|
writeWait = 10 * time.Second
|
||||||
|
// Time allowed to read the next pong message from the peer.
|
||||||
|
pongWait = 60 * time.Second
|
||||||
|
// Send pings to peer with this period. Must be less than pongWait.
|
||||||
|
pingPeriod = (pongWait * 9) / 10
|
||||||
|
// Maximum message size allowed from peer. Set to minimum allowed value as we don't expect the client to send non-control messages.
|
||||||
|
maxMessageSize = 1
|
||||||
|
)
|
||||||
|
|
||||||
|
type connection struct {
|
||||||
|
hub *Hub
|
||||||
|
ws *websocket.Conn
|
||||||
|
send chan interface{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *connection) readLoop() {
|
||||||
|
defer func() {
|
||||||
|
c.hub.unregisterChan <- c
|
||||||
|
c.ws.Close()
|
||||||
|
}()
|
||||||
|
c.ws.SetReadLimit(maxMessageSize)
|
||||||
|
c.ws.SetReadDeadline(time.Now().Add(pongWait))
|
||||||
|
c.ws.SetPongHandler(func(string) error { c.ws.SetReadDeadline(time.Now().Add(pongWait)); return nil })
|
||||||
|
for {
|
||||||
|
if _, _, err := c.ws.NextReader(); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *connection) writeLoop() {
|
||||||
|
ticker := time.NewTicker(pingPeriod)
|
||||||
|
defer func() {
|
||||||
|
ticker.Stop()
|
||||||
|
c.ws.Close()
|
||||||
|
}()
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case message, ok := <-c.send:
|
||||||
|
if !ok {
|
||||||
|
c.writeControl(websocket.CloseMessage)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := c.writeJSON(message); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
case <-ticker.C:
|
||||||
|
if err := c.writeControl(websocket.PingMessage); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *connection) writeJSON(message interface{}) error {
|
||||||
|
c.ws.SetWriteDeadline(time.Now().Add(writeWait))
|
||||||
|
return c.ws.WriteJSON(message)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *connection) writeControl(messageType int) error {
|
||||||
|
c.ws.SetWriteDeadline(time.Now().Add(writeWait))
|
||||||
|
return c.ws.WriteMessage(messageType, []byte{})
|
||||||
|
}
|
76
vendor/github.com/mailhog/MailHog-Server/websockets/hub.go
generated
vendored
Normal file
76
vendor/github.com/mailhog/MailHog-Server/websockets/hub.go
generated
vendored
Normal file
|
@ -0,0 +1,76 @@
|
||||||
|
package websockets
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/gorilla/websocket"
|
||||||
|
"github.com/ian-kent/go-log/log"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Hub struct {
|
||||||
|
upgrader websocket.Upgrader
|
||||||
|
connections map[*connection]bool
|
||||||
|
messages chan interface{}
|
||||||
|
registerChan chan *connection
|
||||||
|
unregisterChan chan *connection
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewHub() *Hub {
|
||||||
|
hub := &Hub{
|
||||||
|
upgrader: websocket.Upgrader{
|
||||||
|
ReadBufferSize: 256,
|
||||||
|
WriteBufferSize: 4096,
|
||||||
|
CheckOrigin: func(r *http.Request) bool {
|
||||||
|
return true
|
||||||
|
},
|
||||||
|
},
|
||||||
|
connections: make(map[*connection]bool),
|
||||||
|
messages: make(chan interface{}),
|
||||||
|
registerChan: make(chan *connection),
|
||||||
|
unregisterChan: make(chan *connection),
|
||||||
|
}
|
||||||
|
go hub.run()
|
||||||
|
return hub
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Hub) run() {
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case c := <-h.registerChan:
|
||||||
|
h.connections[c] = true
|
||||||
|
case c := <-h.unregisterChan:
|
||||||
|
h.unregister(c)
|
||||||
|
case m := <-h.messages:
|
||||||
|
for c := range h.connections {
|
||||||
|
select {
|
||||||
|
case c.send <- m:
|
||||||
|
default:
|
||||||
|
h.unregister(c)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Hub) unregister(c *connection) {
|
||||||
|
if _, ok := h.connections[c]; ok {
|
||||||
|
close(c.send)
|
||||||
|
delete(h.connections, c)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Hub) Serve(w http.ResponseWriter, r *http.Request) {
|
||||||
|
ws, err := h.upgrader.Upgrade(w, r, nil)
|
||||||
|
if err != nil {
|
||||||
|
log.Println(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c := &connection{hub: h, ws: ws, send: make(chan interface{}, 256)}
|
||||||
|
h.registerChan <- c
|
||||||
|
go c.writeLoop()
|
||||||
|
go c.readLoop()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Hub) Broadcast(data interface{}) {
|
||||||
|
h.messages <- data
|
||||||
|
}
|
21
vendor/github.com/mailhog/MailHog-UI/LICENSE.md
generated
vendored
Normal file
21
vendor/github.com/mailhog/MailHog-UI/LICENSE.md
generated
vendored
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
The MIT License (MIT)
|
||||||
|
|
||||||
|
Copyright (c) 2014 - 2016 Ian Kent
|
||||||
|
|
||||||
|
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.
|
16
vendor/github.com/mailhog/MailHog-UI/Makefile
generated
vendored
Normal file
16
vendor/github.com/mailhog/MailHog-UI/Makefile
generated
vendored
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
all: bindata fmt ui
|
||||||
|
|
||||||
|
ui:
|
||||||
|
go install .
|
||||||
|
|
||||||
|
bindata: bindata-deps
|
||||||
|
-rm assets/assets.go
|
||||||
|
go-bindata -o assets/assets.go -pkg assets assets/...
|
||||||
|
|
||||||
|
bindata-deps:
|
||||||
|
go get github.com/jteeuwen/go-bindata/...
|
||||||
|
|
||||||
|
fmt:
|
||||||
|
go fmt ./...
|
||||||
|
|
||||||
|
.PNONY: all ui bindata bindata-deps fmt
|
10
vendor/github.com/mailhog/MailHog-UI/README.md
generated
vendored
Normal file
10
vendor/github.com/mailhog/MailHog-UI/README.md
generated
vendored
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
MailHog UI [![Build Status](https://travis-ci.org/mailhog/MailHog-UI.svg?branch=master)](https://travis-ci.org/mailhog/MailHog-UI)
|
||||||
|
=========
|
||||||
|
|
||||||
|
MailHog-UI is the MailHog web based user interface.
|
||||||
|
|
||||||
|
### Licence
|
||||||
|
|
||||||
|
Copyright © 2014 - 2016, Ian Kent (http://iankent.uk)
|
||||||
|
|
||||||
|
Released under MIT license, see [LICENSE](LICENSE.md) for details.
|
753
vendor/github.com/mailhog/MailHog-UI/assets/assets.go
generated
vendored
Normal file
753
vendor/github.com/mailhog/MailHog-UI/assets/assets.go
generated
vendored
Normal file
File diff suppressed because one or more lines are too long
32
vendor/github.com/mailhog/MailHog-UI/config/config.go
generated
vendored
Normal file
32
vendor/github.com/mailhog/MailHog-UI/config/config.go
generated
vendored
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"flag"
|
||||||
|
|
||||||
|
"github.com/ian-kent/envconf"
|
||||||
|
)
|
||||||
|
|
||||||
|
func DefaultConfig() *Config {
|
||||||
|
return &Config{
|
||||||
|
APIHost: "",
|
||||||
|
UIBindAddr: "0.0.0.0:8025",
|
||||||
|
WebPath: "",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type Config struct {
|
||||||
|
APIHost string
|
||||||
|
UIBindAddr string
|
||||||
|
WebPath string
|
||||||
|
}
|
||||||
|
|
||||||
|
var cfg = DefaultConfig()
|
||||||
|
|
||||||
|
func Configure() *Config {
|
||||||
|
return cfg
|
||||||
|
}
|
||||||
|
|
||||||
|
func RegisterFlags() {
|
||||||
|
flag.StringVar(&cfg.APIHost, "api-host", envconf.FromEnvP("MH_API_HOST", "").(string), "API URL for MailHog UI to connect to, e.g. http://some.host:1234")
|
||||||
|
flag.StringVar(&cfg.UIBindAddr, "ui-bind-addr", envconf.FromEnvP("MH_UI_BIND_ADDR", "0.0.0.0:8025").(string), "HTTP bind interface and port for UI, e.g. 0.0.0.0:8025 or just :8025")
|
||||||
|
}
|
54
vendor/github.com/mailhog/MailHog-UI/main.go
generated
vendored
Normal file
54
vendor/github.com/mailhog/MailHog-UI/main.go
generated
vendored
Normal file
|
@ -0,0 +1,54 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"flag"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
gohttp "net/http"
|
||||||
|
|
||||||
|
"github.com/gorilla/pat"
|
||||||
|
"github.com/ian-kent/go-log/log"
|
||||||
|
"github.com/mailhog/MailHog-UI/assets"
|
||||||
|
"github.com/mailhog/MailHog-UI/config"
|
||||||
|
"github.com/mailhog/MailHog-UI/web"
|
||||||
|
comcfg "github.com/mailhog/MailHog/config"
|
||||||
|
"github.com/mailhog/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
var conf *config.Config
|
||||||
|
var comconf *comcfg.Config
|
||||||
|
var exitCh chan int
|
||||||
|
|
||||||
|
func configure() {
|
||||||
|
comcfg.RegisterFlags()
|
||||||
|
config.RegisterFlags()
|
||||||
|
flag.Parse()
|
||||||
|
conf = config.Configure()
|
||||||
|
comconf = comcfg.Configure()
|
||||||
|
// FIXME hacky
|
||||||
|
web.APIHost = conf.APIHost
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
configure()
|
||||||
|
|
||||||
|
// FIXME need to make API URL configurable
|
||||||
|
|
||||||
|
if comconf.AuthFile != "" {
|
||||||
|
http.AuthFile(comconf.AuthFile)
|
||||||
|
}
|
||||||
|
|
||||||
|
exitCh = make(chan int)
|
||||||
|
cb := func(r gohttp.Handler) {
|
||||||
|
web.CreateWeb(conf, r.(*pat.Router), assets.Asset)
|
||||||
|
}
|
||||||
|
go http.Listen(conf.UIBindAddr, assets.Asset, exitCh, cb)
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-exitCh:
|
||||||
|
log.Printf("Received exit signal")
|
||||||
|
os.Exit(0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
119
vendor/github.com/mailhog/MailHog-UI/web/web.go
generated
vendored
Normal file
119
vendor/github.com/mailhog/MailHog-UI/web/web.go
generated
vendored
Normal file
|
@ -0,0 +1,119 @@
|
||||||
|
package web
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"html/template"
|
||||||
|
"log"
|
||||||
|
"mime"
|
||||||
|
"net/http"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/gorilla/pat"
|
||||||
|
"github.com/mailhog/MailHog-UI/config"
|
||||||
|
)
|
||||||
|
|
||||||
|
var APIHost string
|
||||||
|
var WebPath string
|
||||||
|
|
||||||
|
type Web struct {
|
||||||
|
config *config.Config
|
||||||
|
asset func(string) ([]byte, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
func CreateWeb(cfg *config.Config, r http.Handler, asset func(string) ([]byte, error)) *Web {
|
||||||
|
web := &Web{
|
||||||
|
config: cfg,
|
||||||
|
asset: asset,
|
||||||
|
}
|
||||||
|
|
||||||
|
pat := r.(*pat.Router)
|
||||||
|
|
||||||
|
WebPath = cfg.WebPath
|
||||||
|
|
||||||
|
log.Printf("Serving under http://%s%s/", cfg.UIBindAddr, WebPath)
|
||||||
|
|
||||||
|
pat.Path(WebPath + "/images/{file:.*}").Methods("GET").HandlerFunc(web.Static("assets/images/{{file}}"))
|
||||||
|
pat.Path(WebPath + "/css/{file:.*}").Methods("GET").HandlerFunc(web.Static("assets/css/{{file}}"))
|
||||||
|
pat.Path(WebPath + "/js/{file:.*}").Methods("GET").HandlerFunc(web.Static("assets/js/{{file}}"))
|
||||||
|
pat.Path(WebPath + "/fonts/{file:.*}").Methods("GET").HandlerFunc(web.Static("assets/fonts/{{file}}"))
|
||||||
|
pat.StrictSlash(true).Path(WebPath + "/").Methods("GET").HandlerFunc(web.Index())
|
||||||
|
|
||||||
|
return web
|
||||||
|
}
|
||||||
|
|
||||||
|
func (web Web) Static(pattern string) func(http.ResponseWriter, *http.Request) {
|
||||||
|
return func(w http.ResponseWriter, req *http.Request) {
|
||||||
|
fp := strings.TrimSuffix(pattern, "{{file}}") + req.URL.Query().Get(":file")
|
||||||
|
if b, err := web.asset(fp); err == nil {
|
||||||
|
ext := filepath.Ext(fp)
|
||||||
|
|
||||||
|
w.Header().Set("Content-Type", mime.TypeByExtension(ext))
|
||||||
|
w.WriteHeader(200)
|
||||||
|
w.Write(b)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
log.Printf("[UI] File not found: %s", fp)
|
||||||
|
w.WriteHeader(404)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (web Web) Index() func(http.ResponseWriter, *http.Request) {
|
||||||
|
tmpl := template.New("index.html")
|
||||||
|
tmpl.Delims("[:", ":]")
|
||||||
|
|
||||||
|
asset, err := web.asset("assets/templates/index.html")
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("[UI] Error loading index.html: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
tmpl, err = tmpl.Parse(string(asset))
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("[UI] Error parsing index.html: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
layout := template.New("layout.html")
|
||||||
|
layout.Delims("[:", ":]")
|
||||||
|
|
||||||
|
asset, err = web.asset("assets/templates/layout.html")
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("[UI] Error loading layout.html: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
layout, err = layout.Parse(string(asset))
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("[UI] Error parsing layout.html: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return func(w http.ResponseWriter, req *http.Request) {
|
||||||
|
data := map[string]interface{}{
|
||||||
|
"config": web.config,
|
||||||
|
"Page": "Browse",
|
||||||
|
"APIHost": APIHost,
|
||||||
|
}
|
||||||
|
|
||||||
|
b := new(bytes.Buffer)
|
||||||
|
err := tmpl.Execute(b, data)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("[UI] Error executing template: %s", err)
|
||||||
|
w.WriteHeader(500)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
data["Content"] = template.HTML(b.String())
|
||||||
|
|
||||||
|
b = new(bytes.Buffer)
|
||||||
|
err = layout.Execute(b, data)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("[UI] Error executing template: %s", err)
|
||||||
|
w.WriteHeader(500)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||||
|
w.WriteHeader(200)
|
||||||
|
w.Write(b.Bytes())
|
||||||
|
}
|
||||||
|
}
|
21
vendor/github.com/mailhog/data/LICENSE.md
generated
vendored
Normal file
21
vendor/github.com/mailhog/data/LICENSE.md
generated
vendored
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
The MIT License (MIT)
|
||||||
|
|
||||||
|
Copyright (c) 2014 Ian Kent
|
||||||
|
|
||||||
|
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.
|
10
vendor/github.com/mailhog/data/README.md
generated
vendored
Normal file
10
vendor/github.com/mailhog/data/README.md
generated
vendored
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
MailHog data library [![GoDoc](https://godoc.org/github.com/mailhog/data?status.svg)](https://godoc.org/github.com/mailhog/data) [![Build Status](https://travis-ci.org/mailhog/data.svg?branch=master)](https://travis-ci.org/mailhog/data)
|
||||||
|
=========
|
||||||
|
|
||||||
|
`github.com/mailhog/data` implements a data library
|
||||||
|
|
||||||
|
### Licence
|
||||||
|
|
||||||
|
Copyright © 2014-2015, Ian Kent (http://iankent.uk)
|
||||||
|
|
||||||
|
Released under MIT license, see [LICENSE](LICENSE.md) for details.
|
330
vendor/github.com/mailhog/data/message.go
generated
vendored
Normal file
330
vendor/github.com/mailhog/data/message.go
generated
vendored
Normal file
|
@ -0,0 +1,330 @@
|
||||||
|
package data
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"crypto/rand"
|
||||||
|
"encoding/base64"
|
||||||
|
"io"
|
||||||
|
"log"
|
||||||
|
"mime"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// LogHandler is called for each log message. If nil, log messages will
|
||||||
|
// be output using log.Printf instead.
|
||||||
|
var LogHandler func(message string, args ...interface{})
|
||||||
|
|
||||||
|
func logf(message string, args ...interface{}) {
|
||||||
|
if LogHandler != nil {
|
||||||
|
LogHandler(message, args...)
|
||||||
|
} else {
|
||||||
|
log.Printf(message, args...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MessageID represents the ID of an SMTP message including the hostname part
|
||||||
|
type MessageID string
|
||||||
|
|
||||||
|
// NewMessageID generates a new message ID
|
||||||
|
func NewMessageID(hostname string) (MessageID, error) {
|
||||||
|
size := 32
|
||||||
|
|
||||||
|
rb := make([]byte, size)
|
||||||
|
_, err := rand.Read(rb)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return MessageID(""), err
|
||||||
|
}
|
||||||
|
|
||||||
|
rs := base64.URLEncoding.EncodeToString(rb)
|
||||||
|
|
||||||
|
return MessageID(rs + "@" + hostname), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Messages represents an array of Messages
|
||||||
|
// - TODO is this even required?
|
||||||
|
type Messages []Message
|
||||||
|
|
||||||
|
// Message represents a parsed SMTP message
|
||||||
|
type Message struct {
|
||||||
|
ID MessageID
|
||||||
|
From *Path
|
||||||
|
To []*Path
|
||||||
|
Content *Content
|
||||||
|
Created time.Time
|
||||||
|
MIME *MIMEBody // FIXME refactor to use Content.MIME
|
||||||
|
Raw *SMTPMessage
|
||||||
|
}
|
||||||
|
|
||||||
|
// Path represents an SMTP forward-path or return-path
|
||||||
|
type Path struct {
|
||||||
|
Relays []string
|
||||||
|
Mailbox string
|
||||||
|
Domain string
|
||||||
|
Params string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Content represents the body content of an SMTP message
|
||||||
|
type Content struct {
|
||||||
|
Headers map[string][]string
|
||||||
|
Body string
|
||||||
|
Size int
|
||||||
|
MIME *MIMEBody
|
||||||
|
}
|
||||||
|
|
||||||
|
// SMTPMessage represents a raw SMTP message
|
||||||
|
type SMTPMessage struct {
|
||||||
|
From string
|
||||||
|
To []string
|
||||||
|
Data string
|
||||||
|
Helo string
|
||||||
|
}
|
||||||
|
|
||||||
|
// MIMEBody represents a collection of MIME parts
|
||||||
|
type MIMEBody struct {
|
||||||
|
Parts []*Content
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse converts a raw SMTP message to a parsed MIME message
|
||||||
|
func (m *SMTPMessage) Parse(hostname string) *Message {
|
||||||
|
var arr []*Path
|
||||||
|
for _, path := range m.To {
|
||||||
|
arr = append(arr, PathFromString(path))
|
||||||
|
}
|
||||||
|
|
||||||
|
id, _ := NewMessageID(hostname)
|
||||||
|
msg := &Message{
|
||||||
|
ID: id,
|
||||||
|
From: PathFromString(m.From),
|
||||||
|
To: arr,
|
||||||
|
Content: ContentFromString(m.Data),
|
||||||
|
Created: time.Now(),
|
||||||
|
Raw: m,
|
||||||
|
}
|
||||||
|
|
||||||
|
if msg.Content.IsMIME() {
|
||||||
|
logf("Parsing MIME body")
|
||||||
|
msg.MIME = msg.Content.ParseMIMEBody()
|
||||||
|
}
|
||||||
|
|
||||||
|
// find headers
|
||||||
|
var hasMessageID bool
|
||||||
|
var receivedHeaderName string
|
||||||
|
var returnPathHeaderName string
|
||||||
|
|
||||||
|
for k := range msg.Content.Headers {
|
||||||
|
if strings.ToLower(k) == "message-id" {
|
||||||
|
hasMessageID = true
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if strings.ToLower(k) == "received" {
|
||||||
|
receivedHeaderName = k
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if strings.ToLower(k) == "return-path" {
|
||||||
|
returnPathHeaderName = k
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !hasMessageID {
|
||||||
|
msg.Content.Headers["Message-ID"] = []string{string(id)}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(receivedHeaderName) > 0 {
|
||||||
|
msg.Content.Headers[receivedHeaderName] = append(msg.Content.Headers[receivedHeaderName], "from "+m.Helo+" by "+hostname+" (MailHog)\r\n id "+string(id)+"; "+time.Now().Format(time.RFC1123Z))
|
||||||
|
} else {
|
||||||
|
msg.Content.Headers["Received"] = []string{"from " + m.Helo + " by " + hostname + " (MailHog)\r\n id " + string(id) + "; " + time.Now().Format(time.RFC1123Z)}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(returnPathHeaderName) > 0 {
|
||||||
|
msg.Content.Headers[returnPathHeaderName] = append(msg.Content.Headers[returnPathHeaderName], "<"+m.From+">")
|
||||||
|
} else {
|
||||||
|
msg.Content.Headers["Return-Path"] = []string{"<" + m.From + ">"}
|
||||||
|
}
|
||||||
|
|
||||||
|
return msg
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bytes returns an io.Reader containing the raw message data
|
||||||
|
func (m *SMTPMessage) Bytes() io.Reader {
|
||||||
|
var b = new(bytes.Buffer)
|
||||||
|
|
||||||
|
b.WriteString("HELO:<" + m.Helo + ">\r\n")
|
||||||
|
b.WriteString("FROM:<" + m.From + ">\r\n")
|
||||||
|
for _, t := range m.To {
|
||||||
|
b.WriteString("TO:<" + t + ">\r\n")
|
||||||
|
}
|
||||||
|
b.WriteString("\r\n")
|
||||||
|
b.WriteString(m.Data)
|
||||||
|
|
||||||
|
return b
|
||||||
|
}
|
||||||
|
|
||||||
|
// FromBytes returns a SMTPMessage from raw message bytes (as output by SMTPMessage.Bytes())
|
||||||
|
func FromBytes(b []byte) *SMTPMessage {
|
||||||
|
msg := &SMTPMessage{}
|
||||||
|
var headerDone bool
|
||||||
|
for _, l := range strings.Split(string(b), "\n") {
|
||||||
|
if !headerDone {
|
||||||
|
if strings.HasPrefix(l, "HELO:<") {
|
||||||
|
l = strings.TrimPrefix(l, "HELO:<")
|
||||||
|
l = strings.TrimSuffix(l, ">\r")
|
||||||
|
msg.Helo = l
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if strings.HasPrefix(l, "FROM:<") {
|
||||||
|
l = strings.TrimPrefix(l, "FROM:<")
|
||||||
|
l = strings.TrimSuffix(l, ">\r")
|
||||||
|
msg.From = l
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if strings.HasPrefix(l, "TO:<") {
|
||||||
|
l = strings.TrimPrefix(l, "TO:<")
|
||||||
|
l = strings.TrimSuffix(l, ">\r")
|
||||||
|
msg.To = append(msg.To, l)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if strings.TrimSpace(l) == "" {
|
||||||
|
headerDone = true
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
msg.Data += l + "\n"
|
||||||
|
}
|
||||||
|
return msg
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bytes returns an io.Reader containing the raw message data
|
||||||
|
func (m *Message) Bytes() io.Reader {
|
||||||
|
var b = new(bytes.Buffer)
|
||||||
|
|
||||||
|
for k, vs := range m.Content.Headers {
|
||||||
|
for _, v := range vs {
|
||||||
|
b.WriteString(k + ": " + v + "\r\n")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
b.WriteString("\r\n")
|
||||||
|
b.WriteString(m.Content.Body)
|
||||||
|
|
||||||
|
return b
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsMIME detects a valid MIME header
|
||||||
|
func (content *Content) IsMIME() bool {
|
||||||
|
header, ok := content.Headers["Content-Type"]
|
||||||
|
if !ok {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return strings.HasPrefix(header[0], "multipart/")
|
||||||
|
}
|
||||||
|
|
||||||
|
// ParseMIMEBody parses SMTP message content into multiple MIME parts
|
||||||
|
func (content *Content) ParseMIMEBody() *MIMEBody {
|
||||||
|
var parts []*Content
|
||||||
|
|
||||||
|
if hdr, ok := content.Headers["Content-Type"]; ok {
|
||||||
|
if len(hdr) > 0 {
|
||||||
|
boundary := extractBoundary(hdr[0])
|
||||||
|
var p []string
|
||||||
|
if len(boundary) > 0 {
|
||||||
|
p = strings.Split(content.Body, "--"+boundary)
|
||||||
|
logf("Got boundary: %s", boundary)
|
||||||
|
} else {
|
||||||
|
logf("Boundary not found: %s", hdr[0])
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, s := range p {
|
||||||
|
if len(s) > 0 {
|
||||||
|
part := ContentFromString(strings.Trim(s, "\r\n"))
|
||||||
|
if part.IsMIME() {
|
||||||
|
logf("Parsing inner MIME body")
|
||||||
|
part.MIME = part.ParseMIMEBody()
|
||||||
|
}
|
||||||
|
parts = append(parts, part)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return &MIMEBody{
|
||||||
|
Parts: parts,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// PathFromString parses a forward-path or reverse-path into its parts
|
||||||
|
func PathFromString(path string) *Path {
|
||||||
|
var relays []string
|
||||||
|
email := path
|
||||||
|
if strings.Contains(path, ":") {
|
||||||
|
x := strings.SplitN(path, ":", 2)
|
||||||
|
r, e := x[0], x[1]
|
||||||
|
email = e
|
||||||
|
relays = strings.Split(r, ",")
|
||||||
|
}
|
||||||
|
mailbox, domain := "", ""
|
||||||
|
if strings.Contains(email, "@") {
|
||||||
|
x := strings.SplitN(email, "@", 2)
|
||||||
|
mailbox, domain = x[0], x[1]
|
||||||
|
} else {
|
||||||
|
mailbox = email
|
||||||
|
}
|
||||||
|
|
||||||
|
return &Path{
|
||||||
|
Relays: relays,
|
||||||
|
Mailbox: mailbox,
|
||||||
|
Domain: domain,
|
||||||
|
Params: "", // FIXME?
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ContentFromString parses SMTP content into separate headers and body
|
||||||
|
func ContentFromString(data string) *Content {
|
||||||
|
logf("Parsing Content from string: '%s'", data)
|
||||||
|
x := strings.SplitN(data, "\r\n\r\n", 2)
|
||||||
|
h := make(map[string][]string, 0)
|
||||||
|
|
||||||
|
// FIXME this fails if the message content has no headers - specifically,
|
||||||
|
// if it doesn't contain \r\n\r\n
|
||||||
|
|
||||||
|
if len(x) == 2 {
|
||||||
|
headers, body := x[0], x[1]
|
||||||
|
hdrs := strings.Split(headers, "\r\n")
|
||||||
|
var lastHdr = ""
|
||||||
|
for _, hdr := range hdrs {
|
||||||
|
if lastHdr != "" && (strings.HasPrefix(hdr, " ") || strings.HasPrefix(hdr, "\t")) {
|
||||||
|
h[lastHdr][len(h[lastHdr])-1] = h[lastHdr][len(h[lastHdr])-1] + hdr
|
||||||
|
} else if strings.Contains(hdr, ": ") {
|
||||||
|
y := strings.SplitN(hdr, ": ", 2)
|
||||||
|
key, value := y[0], y[1]
|
||||||
|
// TODO multiple header fields
|
||||||
|
h[key] = []string{value}
|
||||||
|
lastHdr = key
|
||||||
|
} else if len(hdr) > 0 {
|
||||||
|
logf("Found invalid header: '%s'", hdr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return &Content{
|
||||||
|
Size: len(data),
|
||||||
|
Headers: h,
|
||||||
|
Body: body,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return &Content{
|
||||||
|
Size: len(data),
|
||||||
|
Headers: h,
|
||||||
|
Body: x[0],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// extractBoundary extract boundary string in contentType.
|
||||||
|
// It returns empty string if no valid boundary found
|
||||||
|
func extractBoundary(contentType string) string {
|
||||||
|
_, params, err := mime.ParseMediaType(contentType)
|
||||||
|
if err == nil {
|
||||||
|
return params["boundary"]
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
21
vendor/github.com/mailhog/http/LICENSE.md
generated
vendored
Normal file
21
vendor/github.com/mailhog/http/LICENSE.md
generated
vendored
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
The MIT License (MIT)
|
||||||
|
|
||||||
|
Copyright (c) 2015 Ian Kent
|
||||||
|
|
||||||
|
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.
|
10
vendor/github.com/mailhog/http/README.md
generated
vendored
Normal file
10
vendor/github.com/mailhog/http/README.md
generated
vendored
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
MailHog HTTP utilities [![GoDoc](https://godoc.org/github.com/mailhog/http?status.svg)](https://godoc.org/github.com/mailhog/http) [![Build Status](https://travis-ci.org/mailhog/http.svg?branch=master)](https://travis-ci.org/mailhog/http)
|
||||||
|
=========
|
||||||
|
|
||||||
|
`github.com/mailhog/http` provides HTTP utilities used by MailHog-UI and MailHog-Server.
|
||||||
|
|
||||||
|
### Licence
|
||||||
|
|
||||||
|
Copyright © 2014-2015, Ian Kent (http://iankent.uk)
|
||||||
|
|
||||||
|
Released under MIT license, see [LICENSE](LICENSE.md) for details.
|
112
vendor/github.com/mailhog/http/server.go
generated
vendored
Normal file
112
vendor/github.com/mailhog/http/server.go
generated
vendored
Normal file
|
@ -0,0 +1,112 @@
|
||||||
|
package http
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"io"
|
||||||
|
"io/ioutil"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/gorilla/pat"
|
||||||
|
"github.com/ian-kent/go-log/log"
|
||||||
|
"golang.org/x/crypto/bcrypt"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Authorised should be given a function to enable HTTP Basic Authentication
|
||||||
|
var Authorised func(string, string) bool
|
||||||
|
var users map[string]string
|
||||||
|
|
||||||
|
// AuthFile sets Authorised to a function which validates against file
|
||||||
|
func AuthFile(file string) {
|
||||||
|
users = make(map[string]string)
|
||||||
|
|
||||||
|
b, err := ioutil.ReadFile(file)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("[HTTP] Error reading auth-file: %s", err)
|
||||||
|
// FIXME - go-log
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
buf := bytes.NewBuffer(b)
|
||||||
|
|
||||||
|
for {
|
||||||
|
l, err := buf.ReadString('\n')
|
||||||
|
l = strings.TrimSpace(l)
|
||||||
|
if len(l) > 0 {
|
||||||
|
p := strings.SplitN(l, ":", 2)
|
||||||
|
if len(p) < 2 {
|
||||||
|
log.Fatalf("[HTTP] Error reading auth-file, invalid line: %s", l)
|
||||||
|
// FIXME - go-log
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
users[p[0]] = p[1]
|
||||||
|
}
|
||||||
|
switch {
|
||||||
|
case err == io.EOF:
|
||||||
|
break
|
||||||
|
case err != nil:
|
||||||
|
log.Fatalf("[HTTP] Error reading auth-file: %s", err)
|
||||||
|
// FIXME - go-log
|
||||||
|
os.Exit(1)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if err == io.EOF {
|
||||||
|
break
|
||||||
|
} else if err != nil {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("[HTTP] Loaded %d users from %s", len(users), file)
|
||||||
|
|
||||||
|
Authorised = func(u, pw string) bool {
|
||||||
|
hpw, ok := users[u]
|
||||||
|
if !ok {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
err := bcrypt.CompareHashAndPassword([]byte(hpw), []byte(pw))
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// BasicAuthHandler is middleware to check HTTP Basic Authentication
|
||||||
|
// if an authorisation function is defined.
|
||||||
|
func BasicAuthHandler(h http.Handler) http.Handler {
|
||||||
|
f := func(w http.ResponseWriter, req *http.Request) {
|
||||||
|
if Authorised == nil {
|
||||||
|
h.ServeHTTP(w, req)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
u, pw, ok := req.BasicAuth()
|
||||||
|
if !ok || !Authorised(u, pw) {
|
||||||
|
w.Header().Set("WWW-Authenticate", "Basic")
|
||||||
|
w.WriteHeader(401)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
h.ServeHTTP(w, req)
|
||||||
|
}
|
||||||
|
|
||||||
|
return http.HandlerFunc(f)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Listen binds to httpBindAddr
|
||||||
|
func Listen(httpBindAddr string, Asset func(string) ([]byte, error), exitCh chan int, registerCallback func(http.Handler)) {
|
||||||
|
log.Info("[HTTP] Binding to address: %s", httpBindAddr)
|
||||||
|
|
||||||
|
pat := pat.New()
|
||||||
|
registerCallback(pat)
|
||||||
|
|
||||||
|
//compress := handlers.CompressHandler(pat)
|
||||||
|
auth := BasicAuthHandler(pat) //compress)
|
||||||
|
|
||||||
|
err := http.ListenAndServe(httpBindAddr, auth)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("[HTTP] Error binding to address %s: %s", httpBindAddr, err)
|
||||||
|
}
|
||||||
|
}
|
21
vendor/github.com/mailhog/mhsendmail/LICENSE.md
generated
vendored
Normal file
21
vendor/github.com/mailhog/mhsendmail/LICENSE.md
generated
vendored
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
The MIT License (MIT)
|
||||||
|
|
||||||
|
Copyright (c) 2015 - 2016 Ian Kent
|
||||||
|
|
||||||
|
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.
|
82
vendor/github.com/mailhog/mhsendmail/cmd/cmd.go
generated
vendored
Normal file
82
vendor/github.com/mailhog/mhsendmail/cmd/cmd.go
generated
vendored
Normal file
|
@ -0,0 +1,82 @@
|
||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
"log"
|
||||||
|
"net/mail"
|
||||||
|
"net/smtp"
|
||||||
|
"os"
|
||||||
|
"os/user"
|
||||||
|
)
|
||||||
|
|
||||||
|
import flag "github.com/spf13/pflag"
|
||||||
|
|
||||||
|
// Go runs the MailHog sendmail replacement.
|
||||||
|
func Go() {
|
||||||
|
host, err := os.Hostname()
|
||||||
|
if err != nil {
|
||||||
|
host = "localhost"
|
||||||
|
}
|
||||||
|
|
||||||
|
username := "nobody"
|
||||||
|
user, err := user.Current()
|
||||||
|
if err == nil && user != nil && len(user.Username) > 0 {
|
||||||
|
username = user.Username
|
||||||
|
}
|
||||||
|
|
||||||
|
fromAddr := username + "@" + host
|
||||||
|
smtpAddr := "localhost:1025"
|
||||||
|
var recip []string
|
||||||
|
|
||||||
|
// defaults from envars if provided
|
||||||
|
if len(os.Getenv("MH_SENDMAIL_SMTP_ADDR")) > 0 {
|
||||||
|
smtpAddr = os.Getenv("MH_SENDMAIL_SMTP_ADDR")
|
||||||
|
}
|
||||||
|
if len(os.Getenv("MH_SENDMAIL_FROM")) > 0 {
|
||||||
|
fromAddr = os.Getenv("MH_SENDMAIL_FROM")
|
||||||
|
}
|
||||||
|
|
||||||
|
var verbose bool
|
||||||
|
|
||||||
|
// override defaults from cli flags
|
||||||
|
flag.StringVar(&smtpAddr, "smtp-addr", smtpAddr, "SMTP server address")
|
||||||
|
flag.StringVarP(&fromAddr, "from", "f", fromAddr, "SMTP sender")
|
||||||
|
flag.BoolP("long-i", "i", true, "Ignored. This flag exists for sendmail compatibility.")
|
||||||
|
flag.BoolP("long-t", "t", true, "Ignored. This flag exists for sendmail compatibility.")
|
||||||
|
flag.BoolVarP(&verbose, "verbose", "v", false, "Verbose mode (sends debug output to stderr)")
|
||||||
|
flag.Parse()
|
||||||
|
|
||||||
|
// allow recipient to be passed as an argument
|
||||||
|
recip = flag.Args()
|
||||||
|
|
||||||
|
if verbose {
|
||||||
|
fmt.Fprintln(os.Stderr, smtpAddr, fromAddr)
|
||||||
|
}
|
||||||
|
|
||||||
|
body, err := ioutil.ReadAll(os.Stdin)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintln(os.Stderr, "error reading stdin")
|
||||||
|
os.Exit(11)
|
||||||
|
}
|
||||||
|
|
||||||
|
msg, err := mail.ReadMessage(bytes.NewReader(body))
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintln(os.Stderr, "error parsing message body")
|
||||||
|
os.Exit(11)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(recip) == 0 {
|
||||||
|
// We only need to parse the message to get a recipient if none where
|
||||||
|
// provided on the command line.
|
||||||
|
recip = append(recip, msg.Header.Get("To"))
|
||||||
|
}
|
||||||
|
|
||||||
|
err = smtp.SendMail(smtpAddr, nil, fromAddr, recip, body)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintln(os.Stderr, "error sending mail")
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
21
vendor/github.com/mailhog/smtp/LICENSE.md
generated
vendored
Normal file
21
vendor/github.com/mailhog/smtp/LICENSE.md
generated
vendored
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
The MIT License (MIT)
|
||||||
|
|
||||||
|
Copyright (c) 2014 Ian Kent
|
||||||
|
|
||||||
|
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.
|
100
vendor/github.com/mailhog/smtp/README.md
generated
vendored
Normal file
100
vendor/github.com/mailhog/smtp/README.md
generated
vendored
Normal file
|
@ -0,0 +1,100 @@
|
||||||
|
MailHog SMTP Protocol [![GoDoc](https://godoc.org/github.com/mailhog/smtp?status.svg)](https://godoc.org/github.com/mailhog/smtp) [![Build Status](https://travis-ci.org/mailhog/smtp.svg?branch=master)](https://travis-ci.org/mailhog/smtp)
|
||||||
|
=========
|
||||||
|
|
||||||
|
`github.com/mailhog/smtp` implements an SMTP server state machine.
|
||||||
|
|
||||||
|
It attempts to encapsulate as much of the SMTP protocol (plus its extensions) as possible
|
||||||
|
without compromising configurability or requiring specific backend implementations.
|
||||||
|
|
||||||
|
* ESMTP server implementing [RFC5321](http://tools.ietf.org/html/rfc5321)
|
||||||
|
* Support for:
|
||||||
|
* AUTH [RFC4954](http://tools.ietf.org/html/rfc4954)
|
||||||
|
* PIPELINING [RFC2920](http://tools.ietf.org/html/rfc2920)
|
||||||
|
* STARTTLS [RFC3207](http://tools.ietf.org/html/rfc3207)
|
||||||
|
|
||||||
|
```go
|
||||||
|
proto := NewProtocol()
|
||||||
|
reply := proto.Start()
|
||||||
|
reply = proto.ProcessCommand("EHLO localhost")
|
||||||
|
// ...
|
||||||
|
```
|
||||||
|
|
||||||
|
See [MailHog-Server](https://github.com/mailhog/MailHog-Server) and [MailHog-MTA](https://github.com/mailhog/MailHog-MTA) for example implementations.
|
||||||
|
|
||||||
|
### Commands and replies
|
||||||
|
|
||||||
|
Interaction with the state machine is via:
|
||||||
|
* the `Parse` function
|
||||||
|
* the `ProcessCommand` and `ProcessData` functions
|
||||||
|
|
||||||
|
You can mix the use of all three functions as necessary.
|
||||||
|
|
||||||
|
#### Parse
|
||||||
|
|
||||||
|
`Parse` should be used on a raw text stream. It looks for an end of line (`\r\n`), and if found, processes a single command. Any unprocessed data is returned.
|
||||||
|
|
||||||
|
If any unprocessed data is returned, `Parse` should be
|
||||||
|
called again to process then next command.
|
||||||
|
|
||||||
|
```go
|
||||||
|
text := "EHLO localhost\r\nMAIL FROM:<test>\r\nDATA\r\nTest\r\n.\r\n"
|
||||||
|
|
||||||
|
var reply *smtp.Reply
|
||||||
|
for {
|
||||||
|
text, reply = proto.Parse(text)
|
||||||
|
if len(text) == 0 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### ProcessCommand and ProcessData
|
||||||
|
|
||||||
|
`ProcessCommand` should be used for an already parsed command (i.e., a complete
|
||||||
|
SMTP "line" excluding the line ending).
|
||||||
|
|
||||||
|
`ProcessData` should be used if the protocol is in `DATA` state.
|
||||||
|
|
||||||
|
```go
|
||||||
|
reply = proto.ProcessCommand("EHLO localhost")
|
||||||
|
reply = proto.ProcessCommand("MAIL FROM:<test>")
|
||||||
|
reply = proto.ProcessCommand("DATA")
|
||||||
|
reply = proto.ProcessData("Test\r\n.\r\n")
|
||||||
|
```
|
||||||
|
|
||||||
|
### Hooks
|
||||||
|
|
||||||
|
The state machine provides hooks to manipulate its behaviour.
|
||||||
|
|
||||||
|
See [![GoDoc](https://godoc.org/github.com/mailhog/smtp?status.svg)](https://godoc.org/github.com/mailhog/smtp) for more information.
|
||||||
|
|
||||||
|
| Hook | Description
|
||||||
|
| ---------------------------------- | -----------
|
||||||
|
| LogHandler | Called for every log message
|
||||||
|
| MessageReceivedHandler | Called for each message received
|
||||||
|
| ValidateSenderHandler | Called after MAIL FROM
|
||||||
|
| ValidateRecipientHandler | Called after RCPT TO
|
||||||
|
| ValidateAuthenticationHandler | Called after AUTH
|
||||||
|
| SMTPVerbFilter | Called for every SMTP command processed
|
||||||
|
| TLSHandler | Callback mashup called after STARTTLS
|
||||||
|
| GetAuthenticationMechanismsHandler | Called for each EHLO command
|
||||||
|
|
||||||
|
### Behaviour flags
|
||||||
|
|
||||||
|
The state machine also exports variables to control its behaviour:
|
||||||
|
|
||||||
|
See [![GoDoc](https://godoc.org/github.com/mailhog/smtp?status.svg)](https://godoc.org/github.com/mailhog/smtp) for more information.
|
||||||
|
|
||||||
|
| Variable | Description
|
||||||
|
| ---------------------- | -----------
|
||||||
|
| RejectBrokenRCPTSyntax | Reject non-conforming RCPT syntax
|
||||||
|
| RejectBrokenMAILSyntax | Reject non-conforming MAIL syntax
|
||||||
|
| RequireTLS | Require STARTTLS before other commands
|
||||||
|
| MaximumRecipients | Maximum recipients per message
|
||||||
|
| MaximumLineLength | Maximum length of SMTP line
|
||||||
|
|
||||||
|
### Licence
|
||||||
|
|
||||||
|
Copyright © 2014-2015, Ian Kent (http://iankent.uk)
|
||||||
|
|
||||||
|
Released under MIT license, see [LICENSE](LICENSE.md) for details.
|
504
vendor/github.com/mailhog/smtp/protocol.go
generated
vendored
Normal file
504
vendor/github.com/mailhog/smtp/protocol.go
generated
vendored
Normal file
|
@ -0,0 +1,504 @@
|
||||||
|
package smtp
|
||||||
|
|
||||||
|
// http://www.rfc-editor.org/rfc/rfc5321.txt
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/base64"
|
||||||
|
"errors"
|
||||||
|
"log"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/mailhog/data"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Command is a struct representing an SMTP command (verb + arguments)
|
||||||
|
type Command struct {
|
||||||
|
verb string
|
||||||
|
args string
|
||||||
|
orig string
|
||||||
|
}
|
||||||
|
|
||||||
|
// ParseCommand returns a Command from the line string
|
||||||
|
func ParseCommand(line string) *Command {
|
||||||
|
words := strings.Split(line, " ")
|
||||||
|
command := strings.ToUpper(words[0])
|
||||||
|
args := strings.Join(words[1:len(words)], " ")
|
||||||
|
|
||||||
|
return &Command{
|
||||||
|
verb: command,
|
||||||
|
args: args,
|
||||||
|
orig: line,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Protocol is a state machine representing an SMTP session
|
||||||
|
type Protocol struct {
|
||||||
|
lastCommand *Command
|
||||||
|
|
||||||
|
TLSPending bool
|
||||||
|
TLSUpgraded bool
|
||||||
|
|
||||||
|
State State
|
||||||
|
Message *data.SMTPMessage
|
||||||
|
|
||||||
|
Hostname string
|
||||||
|
Ident string
|
||||||
|
|
||||||
|
MaximumLineLength int
|
||||||
|
MaximumRecipients int
|
||||||
|
|
||||||
|
// LogHandler is called for each log message. If nil, log messages will
|
||||||
|
// be output using log.Printf instead.
|
||||||
|
LogHandler func(message string, args ...interface{})
|
||||||
|
// MessageReceivedHandler is called for each message accepted by the
|
||||||
|
// SMTP protocol. It must return a MessageID or error. If nil, messages
|
||||||
|
// will be rejected with an error.
|
||||||
|
MessageReceivedHandler func(*data.SMTPMessage) (string, error)
|
||||||
|
// ValidateSenderHandler should return true if the sender is valid,
|
||||||
|
// otherwise false. If nil, all senders will be accepted.
|
||||||
|
ValidateSenderHandler func(from string) bool
|
||||||
|
// ValidateRecipientHandler should return true if the recipient is valid,
|
||||||
|
// otherwise false. If nil, all recipients will be accepted.
|
||||||
|
ValidateRecipientHandler func(to string) bool
|
||||||
|
// ValidateAuthenticationhandler should return true if the authentication
|
||||||
|
// parameters are valid, otherwise false. If nil, all authentication
|
||||||
|
// attempts will be accepted.
|
||||||
|
ValidateAuthenticationHandler func(mechanism string, args ...string) (errorReply *Reply, ok bool)
|
||||||
|
// SMTPVerbFilter is called after each command is parsed, but before
|
||||||
|
// any code is executed. This provides an opportunity to reject unwanted verbs,
|
||||||
|
// e.g. to require AUTH before MAIL
|
||||||
|
SMTPVerbFilter func(verb string, args ...string) (errorReply *Reply)
|
||||||
|
// TLSHandler is called when a STARTTLS command is received.
|
||||||
|
//
|
||||||
|
// It should acknowledge the TLS request and set ok to true.
|
||||||
|
// It should also return a callback which will be invoked after the reply is
|
||||||
|
// sent. E.g., a TCP connection can only perform the upgrade after sending the reply
|
||||||
|
//
|
||||||
|
// Once the upgrade is complete, invoke the done function (e.g., from the returned callback)
|
||||||
|
//
|
||||||
|
// If TLS upgrade isn't possible, return an errorReply and set ok to false.
|
||||||
|
TLSHandler func(done func(ok bool)) (errorReply *Reply, callback func(), ok bool)
|
||||||
|
|
||||||
|
// GetAuthenticationMechanismsHandler should return an array of strings
|
||||||
|
// listing accepted authentication mechanisms
|
||||||
|
GetAuthenticationMechanismsHandler func() []string
|
||||||
|
|
||||||
|
// RejectBrokenRCPTSyntax controls whether the protocol accepts technically
|
||||||
|
// invalid syntax for the RCPT command. Set to true, the RCPT syntax requires
|
||||||
|
// no space between `TO:` and the opening `<`
|
||||||
|
RejectBrokenRCPTSyntax bool
|
||||||
|
// RejectBrokenMAILSyntax controls whether the protocol accepts technically
|
||||||
|
// invalid syntax for the MAIL command. Set to true, the MAIL syntax requires
|
||||||
|
// no space between `FROM:` and the opening `<`
|
||||||
|
RejectBrokenMAILSyntax bool
|
||||||
|
// RequireTLS controls whether TLS is required for a connection before other
|
||||||
|
// commands can be issued, applied at the protocol layer.
|
||||||
|
RequireTLS bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewProtocol returns a new SMTP state machine in INVALID state
|
||||||
|
// handler is called when a message is received and should return a message ID
|
||||||
|
func NewProtocol() *Protocol {
|
||||||
|
p := &Protocol{
|
||||||
|
Hostname: "mailhog.example",
|
||||||
|
Ident: "ESMTP MailHog",
|
||||||
|
State: INVALID,
|
||||||
|
MaximumLineLength: -1,
|
||||||
|
MaximumRecipients: -1,
|
||||||
|
}
|
||||||
|
p.resetState()
|
||||||
|
return p
|
||||||
|
}
|
||||||
|
|
||||||
|
func (proto *Protocol) resetState() {
|
||||||
|
proto.Message = &data.SMTPMessage{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (proto *Protocol) logf(message string, args ...interface{}) {
|
||||||
|
message = strings.Join([]string{"[PROTO: %s]", message}, " ")
|
||||||
|
args = append([]interface{}{StateMap[proto.State]}, args...)
|
||||||
|
|
||||||
|
if proto.LogHandler != nil {
|
||||||
|
proto.LogHandler(message, args...)
|
||||||
|
} else {
|
||||||
|
log.Printf(message, args...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start begins an SMTP conversation with a 220 reply, placing the state
|
||||||
|
// machine in ESTABLISH state.
|
||||||
|
func (proto *Protocol) Start() *Reply {
|
||||||
|
proto.logf("Started session, switching to ESTABLISH state")
|
||||||
|
proto.State = ESTABLISH
|
||||||
|
return ReplyIdent(proto.Hostname + " " + proto.Ident)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse parses a line string and returns any remaining line string
|
||||||
|
// and a reply, if a command was found. Parse does nothing until a
|
||||||
|
// new line is found.
|
||||||
|
// - TODO decide whether to move this to a buffer inside Protocol
|
||||||
|
// sort of like it this way, since it gives control back to the caller
|
||||||
|
func (proto *Protocol) Parse(line string) (string, *Reply) {
|
||||||
|
var reply *Reply
|
||||||
|
|
||||||
|
if !strings.Contains(line, "\r\n") {
|
||||||
|
return line, reply
|
||||||
|
}
|
||||||
|
|
||||||
|
parts := strings.SplitN(line, "\r\n", 2)
|
||||||
|
line = parts[1]
|
||||||
|
|
||||||
|
if proto.MaximumLineLength > -1 {
|
||||||
|
if len(parts[0]) > proto.MaximumLineLength {
|
||||||
|
return line, ReplyLineTooLong()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO collapse AUTH states into separate processing
|
||||||
|
if proto.State == DATA {
|
||||||
|
reply = proto.ProcessData(parts[0])
|
||||||
|
} else {
|
||||||
|
reply = proto.ProcessCommand(parts[0])
|
||||||
|
}
|
||||||
|
|
||||||
|
return line, reply
|
||||||
|
}
|
||||||
|
|
||||||
|
// ProcessData handles content received (with newlines stripped) while
|
||||||
|
// in the SMTP DATA state
|
||||||
|
func (proto *Protocol) ProcessData(line string) (reply *Reply) {
|
||||||
|
proto.Message.Data += line + "\r\n"
|
||||||
|
|
||||||
|
if strings.HasSuffix(proto.Message.Data, "\r\n.\r\n") {
|
||||||
|
proto.Message.Data = strings.Replace(proto.Message.Data, "\r\n..", "\r\n.", -1)
|
||||||
|
|
||||||
|
proto.logf("Got EOF, storing message and switching to MAIL state")
|
||||||
|
proto.Message.Data = strings.TrimSuffix(proto.Message.Data, "\r\n.\r\n")
|
||||||
|
proto.State = MAIL
|
||||||
|
|
||||||
|
defer proto.resetState()
|
||||||
|
|
||||||
|
if proto.MessageReceivedHandler == nil {
|
||||||
|
return ReplyStorageFailed("No storage backend")
|
||||||
|
}
|
||||||
|
|
||||||
|
id, err := proto.MessageReceivedHandler(proto.Message)
|
||||||
|
if err != nil {
|
||||||
|
proto.logf("Error storing message: %s", err)
|
||||||
|
return ReplyStorageFailed("Unable to store message")
|
||||||
|
}
|
||||||
|
return ReplyOk("Ok: queued as " + id)
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// ProcessCommand processes a line of text as a command
|
||||||
|
// It expects the line string to be a properly formed SMTP verb and arguments
|
||||||
|
func (proto *Protocol) ProcessCommand(line string) (reply *Reply) {
|
||||||
|
line = strings.Trim(line, "\r\n")
|
||||||
|
proto.logf("Processing line: %s", line)
|
||||||
|
|
||||||
|
words := strings.Split(line, " ")
|
||||||
|
command := strings.ToUpper(words[0])
|
||||||
|
args := strings.Join(words[1:len(words)], " ")
|
||||||
|
proto.logf("In state %d, got command '%s', args '%s'", proto.State, command, args)
|
||||||
|
|
||||||
|
cmd := ParseCommand(strings.TrimSuffix(line, "\r\n"))
|
||||||
|
return proto.Command(cmd)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Command applies an SMTP verb and arguments to the state machine
|
||||||
|
func (proto *Protocol) Command(command *Command) (reply *Reply) {
|
||||||
|
defer func() {
|
||||||
|
proto.lastCommand = command
|
||||||
|
}()
|
||||||
|
if proto.SMTPVerbFilter != nil {
|
||||||
|
proto.logf("sending to SMTP verb filter")
|
||||||
|
r := proto.SMTPVerbFilter(command.verb)
|
||||||
|
if r != nil {
|
||||||
|
proto.logf("response returned by SMTP verb filter")
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
}
|
||||||
|
switch {
|
||||||
|
case proto.TLSPending && !proto.TLSUpgraded:
|
||||||
|
proto.logf("Got command before TLS upgrade complete")
|
||||||
|
// FIXME what to do?
|
||||||
|
return ReplyBye()
|
||||||
|
case "RSET" == command.verb:
|
||||||
|
proto.logf("Got RSET command, switching to MAIL state")
|
||||||
|
proto.State = MAIL
|
||||||
|
proto.Message = &data.SMTPMessage{}
|
||||||
|
return ReplyOk()
|
||||||
|
case "NOOP" == command.verb:
|
||||||
|
proto.logf("Got NOOP verb, staying in %s state", StateMap[proto.State])
|
||||||
|
return ReplyOk()
|
||||||
|
case "QUIT" == command.verb:
|
||||||
|
proto.logf("Got QUIT verb, staying in %s state", StateMap[proto.State])
|
||||||
|
proto.State = DONE
|
||||||
|
return ReplyBye()
|
||||||
|
case ESTABLISH == proto.State:
|
||||||
|
proto.logf("In ESTABLISH state")
|
||||||
|
switch command.verb {
|
||||||
|
case "HELO":
|
||||||
|
return proto.HELO(command.args)
|
||||||
|
case "EHLO":
|
||||||
|
return proto.EHLO(command.args)
|
||||||
|
case "STARTTLS":
|
||||||
|
return proto.STARTTLS(command.args)
|
||||||
|
default:
|
||||||
|
proto.logf("Got unknown command for ESTABLISH state: '%s'", command.verb)
|
||||||
|
return ReplyUnrecognisedCommand()
|
||||||
|
}
|
||||||
|
case "STARTTLS" == command.verb:
|
||||||
|
proto.logf("Got STARTTLS command outside ESTABLISH state")
|
||||||
|
return proto.STARTTLS(command.args)
|
||||||
|
case proto.RequireTLS && !proto.TLSUpgraded:
|
||||||
|
proto.logf("RequireTLS set and not TLS not upgraded")
|
||||||
|
return ReplyMustIssueSTARTTLSFirst()
|
||||||
|
case AUTHPLAIN == proto.State:
|
||||||
|
proto.logf("Got PLAIN authentication response: '%s', switching to MAIL state", command.args)
|
||||||
|
proto.State = MAIL
|
||||||
|
if proto.ValidateAuthenticationHandler != nil {
|
||||||
|
// TODO error handling
|
||||||
|
val, _ := base64.StdEncoding.DecodeString(command.orig)
|
||||||
|
bits := strings.Split(string(val), string(rune(0)))
|
||||||
|
|
||||||
|
if len(bits) < 3 {
|
||||||
|
return ReplyError(errors.New("Badly formed parameter"))
|
||||||
|
}
|
||||||
|
|
||||||
|
user, pass := bits[1], bits[2]
|
||||||
|
|
||||||
|
if reply, ok := proto.ValidateAuthenticationHandler("PLAIN", user, pass); !ok {
|
||||||
|
return reply
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ReplyAuthOk()
|
||||||
|
case AUTHLOGIN == proto.State:
|
||||||
|
proto.logf("Got LOGIN authentication response: '%s', switching to AUTHLOGIN2 state", command.args)
|
||||||
|
proto.State = AUTHLOGIN2
|
||||||
|
return ReplyAuthResponse("UGFzc3dvcmQ6")
|
||||||
|
case AUTHLOGIN2 == proto.State:
|
||||||
|
proto.logf("Got LOGIN authentication response: '%s', switching to MAIL state", command.args)
|
||||||
|
proto.State = MAIL
|
||||||
|
if proto.ValidateAuthenticationHandler != nil {
|
||||||
|
if reply, ok := proto.ValidateAuthenticationHandler("LOGIN", proto.lastCommand.orig, command.orig); !ok {
|
||||||
|
return reply
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ReplyAuthOk()
|
||||||
|
case AUTHCRAMMD5 == proto.State:
|
||||||
|
proto.logf("Got CRAM-MD5 authentication response: '%s', switching to MAIL state", command.args)
|
||||||
|
proto.State = MAIL
|
||||||
|
if proto.ValidateAuthenticationHandler != nil {
|
||||||
|
if reply, ok := proto.ValidateAuthenticationHandler("CRAM-MD5", command.orig); !ok {
|
||||||
|
return reply
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ReplyAuthOk()
|
||||||
|
case MAIL == proto.State:
|
||||||
|
proto.logf("In MAIL state")
|
||||||
|
switch command.verb {
|
||||||
|
case "AUTH":
|
||||||
|
proto.logf("Got AUTH command, staying in MAIL state")
|
||||||
|
switch {
|
||||||
|
case strings.HasPrefix(command.args, "PLAIN "):
|
||||||
|
proto.logf("Got PLAIN authentication: %s", strings.TrimPrefix(command.args, "PLAIN "))
|
||||||
|
if proto.ValidateAuthenticationHandler != nil {
|
||||||
|
val, _ := base64.StdEncoding.DecodeString(strings.TrimPrefix(command.args, "PLAIN "))
|
||||||
|
bits := strings.Split(string(val), string(rune(0)))
|
||||||
|
|
||||||
|
if len(bits) < 3 {
|
||||||
|
return ReplyError(errors.New("Badly formed parameter"))
|
||||||
|
}
|
||||||
|
|
||||||
|
user, pass := bits[1], bits[2]
|
||||||
|
|
||||||
|
if reply, ok := proto.ValidateAuthenticationHandler("PLAIN", user, pass); !ok {
|
||||||
|
return reply
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ReplyAuthOk()
|
||||||
|
case "LOGIN" == command.args:
|
||||||
|
proto.logf("Got LOGIN authentication, switching to AUTH state")
|
||||||
|
proto.State = AUTHLOGIN
|
||||||
|
return ReplyAuthResponse("VXNlcm5hbWU6")
|
||||||
|
case "PLAIN" == command.args:
|
||||||
|
proto.logf("Got PLAIN authentication (no args), switching to AUTH2 state")
|
||||||
|
proto.State = AUTHPLAIN
|
||||||
|
return ReplyAuthResponse("")
|
||||||
|
case "CRAM-MD5" == command.args:
|
||||||
|
proto.logf("Got CRAM-MD5 authentication, switching to AUTH state")
|
||||||
|
proto.State = AUTHCRAMMD5
|
||||||
|
return ReplyAuthResponse("PDQxOTI5NDIzNDEuMTI4Mjg0NzJAc291cmNlZm91ci5hbmRyZXcuY211LmVkdT4=")
|
||||||
|
case strings.HasPrefix(command.args, "EXTERNAL "):
|
||||||
|
proto.logf("Got EXTERNAL authentication: %s", strings.TrimPrefix(command.args, "EXTERNAL "))
|
||||||
|
if proto.ValidateAuthenticationHandler != nil {
|
||||||
|
if reply, ok := proto.ValidateAuthenticationHandler("EXTERNAL", strings.TrimPrefix(command.args, "EXTERNAL ")); !ok {
|
||||||
|
return reply
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ReplyAuthOk()
|
||||||
|
default:
|
||||||
|
return ReplyUnsupportedAuth()
|
||||||
|
}
|
||||||
|
case "MAIL":
|
||||||
|
proto.logf("Got MAIL command, switching to RCPT state")
|
||||||
|
from, err := proto.ParseMAIL(command.args)
|
||||||
|
if err != nil {
|
||||||
|
return ReplyError(err)
|
||||||
|
}
|
||||||
|
if proto.ValidateSenderHandler != nil {
|
||||||
|
if !proto.ValidateSenderHandler(from) {
|
||||||
|
// TODO correct sender error response
|
||||||
|
return ReplyError(errors.New("Invalid sender " + from))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
proto.Message.From = from
|
||||||
|
proto.State = RCPT
|
||||||
|
return ReplySenderOk(from)
|
||||||
|
case "HELO":
|
||||||
|
return proto.HELO(command.args)
|
||||||
|
case "EHLO":
|
||||||
|
return proto.EHLO(command.args)
|
||||||
|
default:
|
||||||
|
proto.logf("Got unknown command for MAIL state: '%s'", command)
|
||||||
|
return ReplyUnrecognisedCommand()
|
||||||
|
}
|
||||||
|
case RCPT == proto.State:
|
||||||
|
proto.logf("In RCPT state")
|
||||||
|
switch command.verb {
|
||||||
|
case "RCPT":
|
||||||
|
proto.logf("Got RCPT command")
|
||||||
|
if proto.MaximumRecipients > -1 && len(proto.Message.To) >= proto.MaximumRecipients {
|
||||||
|
return ReplyTooManyRecipients()
|
||||||
|
}
|
||||||
|
to, err := proto.ParseRCPT(command.args)
|
||||||
|
if err != nil {
|
||||||
|
return ReplyError(err)
|
||||||
|
}
|
||||||
|
if proto.ValidateRecipientHandler != nil {
|
||||||
|
if !proto.ValidateRecipientHandler(to) {
|
||||||
|
// TODO correct send error response
|
||||||
|
return ReplyError(errors.New("Invalid recipient " + to))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
proto.Message.To = append(proto.Message.To, to)
|
||||||
|
proto.State = RCPT
|
||||||
|
return ReplyRecipientOk(to)
|
||||||
|
case "HELO":
|
||||||
|
return proto.HELO(command.args)
|
||||||
|
case "EHLO":
|
||||||
|
return proto.EHLO(command.args)
|
||||||
|
case "DATA":
|
||||||
|
proto.logf("Got DATA command, switching to DATA state")
|
||||||
|
proto.State = DATA
|
||||||
|
return ReplyDataResponse()
|
||||||
|
default:
|
||||||
|
proto.logf("Got unknown command for RCPT state: '%s'", command)
|
||||||
|
return ReplyUnrecognisedCommand()
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
proto.logf("Command not recognised")
|
||||||
|
return ReplyUnrecognisedCommand()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// HELO creates a reply to a HELO command
|
||||||
|
func (proto *Protocol) HELO(args string) (reply *Reply) {
|
||||||
|
proto.logf("Got HELO command, switching to MAIL state")
|
||||||
|
proto.State = MAIL
|
||||||
|
proto.Message.Helo = args
|
||||||
|
return ReplyOk("Hello " + args)
|
||||||
|
}
|
||||||
|
|
||||||
|
// EHLO creates a reply to a EHLO command
|
||||||
|
func (proto *Protocol) EHLO(args string) (reply *Reply) {
|
||||||
|
proto.logf("Got EHLO command, switching to MAIL state")
|
||||||
|
proto.State = MAIL
|
||||||
|
proto.Message.Helo = args
|
||||||
|
replyArgs := []string{"Hello " + args, "PIPELINING"}
|
||||||
|
|
||||||
|
if proto.TLSHandler != nil && !proto.TLSPending && !proto.TLSUpgraded {
|
||||||
|
replyArgs = append(replyArgs, "STARTTLS")
|
||||||
|
}
|
||||||
|
|
||||||
|
if !proto.RequireTLS || proto.TLSUpgraded {
|
||||||
|
if proto.GetAuthenticationMechanismsHandler != nil {
|
||||||
|
mechanisms := proto.GetAuthenticationMechanismsHandler()
|
||||||
|
if len(mechanisms) > 0 {
|
||||||
|
replyArgs = append(replyArgs, "AUTH "+strings.Join(mechanisms, " "))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ReplyOk(replyArgs...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// STARTTLS creates a reply to a STARTTLS command
|
||||||
|
func (proto *Protocol) STARTTLS(args string) (reply *Reply) {
|
||||||
|
if proto.TLSUpgraded {
|
||||||
|
return ReplyUnrecognisedCommand()
|
||||||
|
}
|
||||||
|
|
||||||
|
if proto.TLSHandler == nil {
|
||||||
|
proto.logf("tls handler not found")
|
||||||
|
return ReplyUnrecognisedCommand()
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(args) > 0 {
|
||||||
|
return ReplySyntaxError("no parameters allowed")
|
||||||
|
}
|
||||||
|
|
||||||
|
r, callback, ok := proto.TLSHandler(func(ok bool) {
|
||||||
|
proto.TLSUpgraded = ok
|
||||||
|
proto.TLSPending = ok
|
||||||
|
if ok {
|
||||||
|
proto.resetState()
|
||||||
|
proto.State = ESTABLISH
|
||||||
|
}
|
||||||
|
})
|
||||||
|
if !ok {
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
|
proto.TLSPending = true
|
||||||
|
return ReplyReadyToStartTLS(callback)
|
||||||
|
}
|
||||||
|
|
||||||
|
var parseMailBrokenRegexp = regexp.MustCompile("(?i:From):\\s*<([^>]+)>")
|
||||||
|
var parseMailRFCRegexp = regexp.MustCompile("(?i:From):<([^>]+)>")
|
||||||
|
|
||||||
|
// ParseMAIL returns the forward-path from a MAIL command argument
|
||||||
|
func (proto *Protocol) ParseMAIL(mail string) (string, error) {
|
||||||
|
var match []string
|
||||||
|
if proto.RejectBrokenMAILSyntax {
|
||||||
|
match = parseMailRFCRegexp.FindStringSubmatch(mail)
|
||||||
|
} else {
|
||||||
|
match = parseMailBrokenRegexp.FindStringSubmatch(mail)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(match) != 2 {
|
||||||
|
return "", errors.New("Invalid syntax in MAIL command")
|
||||||
|
}
|
||||||
|
return match[1], nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var parseRcptBrokenRegexp = regexp.MustCompile("(?i:To):\\s*<([^>]+)>")
|
||||||
|
var parseRcptRFCRegexp = regexp.MustCompile("(?i:To):<([^>]+)>")
|
||||||
|
|
||||||
|
// ParseRCPT returns the return-path from a RCPT command argument
|
||||||
|
func (proto *Protocol) ParseRCPT(rcpt string) (string, error) {
|
||||||
|
var match []string
|
||||||
|
if proto.RejectBrokenRCPTSyntax {
|
||||||
|
match = parseRcptRFCRegexp.FindStringSubmatch(rcpt)
|
||||||
|
} else {
|
||||||
|
match = parseRcptBrokenRegexp.FindStringSubmatch(rcpt)
|
||||||
|
}
|
||||||
|
if len(match) != 2 {
|
||||||
|
return "", errors.New("Invalid syntax in RCPT command")
|
||||||
|
}
|
||||||
|
return match[1], nil
|
||||||
|
}
|
111
vendor/github.com/mailhog/smtp/reply.go
generated
vendored
Normal file
111
vendor/github.com/mailhog/smtp/reply.go
generated
vendored
Normal file
|
@ -0,0 +1,111 @@
|
||||||
|
package smtp
|
||||||
|
|
||||||
|
import "strconv"
|
||||||
|
|
||||||
|
// http://www.rfc-editor.org/rfc/rfc5321.txt
|
||||||
|
|
||||||
|
// Reply is a struct representing an SMTP reply (status code + lines)
|
||||||
|
type Reply struct {
|
||||||
|
Status int
|
||||||
|
lines []string
|
||||||
|
Done func()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Lines returns the formatted SMTP reply
|
||||||
|
func (r Reply) Lines() []string {
|
||||||
|
var lines []string
|
||||||
|
|
||||||
|
if len(r.lines) == 0 {
|
||||||
|
l := strconv.Itoa(r.Status)
|
||||||
|
lines = append(lines, l+"\n")
|
||||||
|
return lines
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, line := range r.lines {
|
||||||
|
l := ""
|
||||||
|
if i == len(r.lines)-1 {
|
||||||
|
l = strconv.Itoa(r.Status) + " " + line + "\r\n"
|
||||||
|
} else {
|
||||||
|
l = strconv.Itoa(r.Status) + "-" + line + "\r\n"
|
||||||
|
}
|
||||||
|
lines = append(lines, l)
|
||||||
|
}
|
||||||
|
|
||||||
|
return lines
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReplyIdent creates a 220 welcome reply
|
||||||
|
func ReplyIdent(ident string) *Reply { return &Reply{220, []string{ident}, nil} }
|
||||||
|
|
||||||
|
// ReplyReadyToStartTLS creates a 220 ready to start TLS reply
|
||||||
|
func ReplyReadyToStartTLS(callback func()) *Reply {
|
||||||
|
return &Reply{220, []string{"Ready to start TLS"}, callback}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReplyBye creates a 221 Bye reply
|
||||||
|
func ReplyBye() *Reply { return &Reply{221, []string{"Bye"}, nil} }
|
||||||
|
|
||||||
|
// ReplyAuthOk creates a 235 authentication successful reply
|
||||||
|
func ReplyAuthOk() *Reply { return &Reply{235, []string{"Authentication successful"}, nil} }
|
||||||
|
|
||||||
|
// ReplyOk creates a 250 Ok reply
|
||||||
|
func ReplyOk(message ...string) *Reply {
|
||||||
|
if len(message) == 0 {
|
||||||
|
message = []string{"Ok"}
|
||||||
|
}
|
||||||
|
return &Reply{250, message, nil}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReplySenderOk creates a 250 Sender ok reply
|
||||||
|
func ReplySenderOk(sender string) *Reply {
|
||||||
|
return &Reply{250, []string{"Sender " + sender + " ok"}, nil}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReplyRecipientOk creates a 250 Sender ok reply
|
||||||
|
func ReplyRecipientOk(recipient string) *Reply {
|
||||||
|
return &Reply{250, []string{"Recipient " + recipient + " ok"}, nil}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReplyAuthResponse creates a 334 authentication reply
|
||||||
|
func ReplyAuthResponse(response string) *Reply { return &Reply{334, []string{response}, nil} }
|
||||||
|
|
||||||
|
// ReplyDataResponse creates a 354 data reply
|
||||||
|
func ReplyDataResponse() *Reply { return &Reply{354, []string{"End data with <CR><LF>.<CR><LF>"}, nil} }
|
||||||
|
|
||||||
|
// ReplyStorageFailed creates a 452 error reply
|
||||||
|
func ReplyStorageFailed(reason string) *Reply { return &Reply{452, []string{reason}, nil} }
|
||||||
|
|
||||||
|
// ReplyUnrecognisedCommand creates a 500 Unrecognised command reply
|
||||||
|
func ReplyUnrecognisedCommand() *Reply { return &Reply{500, []string{"Unrecognised command"}, nil} }
|
||||||
|
|
||||||
|
// ReplyLineTooLong creates a 500 Line too long reply
|
||||||
|
func ReplyLineTooLong() *Reply { return &Reply{500, []string{"Line too long"}, nil} }
|
||||||
|
|
||||||
|
// ReplySyntaxError creates a 501 Syntax error reply
|
||||||
|
func ReplySyntaxError(response string) *Reply {
|
||||||
|
if len(response) > 0 {
|
||||||
|
response = " (" + response + ")"
|
||||||
|
}
|
||||||
|
return &Reply{501, []string{"Syntax error" + response}, nil}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReplyUnsupportedAuth creates a 504 unsupported authentication reply
|
||||||
|
func ReplyUnsupportedAuth() *Reply {
|
||||||
|
return &Reply{504, []string{"Unsupported authentication mechanism"}, nil}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReplyMustIssueSTARTTLSFirst creates a 530 reply for RFC3207
|
||||||
|
func ReplyMustIssueSTARTTLSFirst() *Reply {
|
||||||
|
return &Reply{530, []string{"Must issue a STARTTLS command first"}, nil}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReplyInvalidAuth creates a 535 error reply
|
||||||
|
func ReplyInvalidAuth() *Reply {
|
||||||
|
return &Reply{535, []string{"Authentication credentials invalid"}, nil}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReplyError creates a 500 error reply
|
||||||
|
func ReplyError(err error) *Reply { return &Reply{550, []string{err.Error()}, nil} }
|
||||||
|
|
||||||
|
// ReplyTooManyRecipients creates a 552 too many recipients reply
|
||||||
|
func ReplyTooManyRecipients() *Reply { return &Reply{552, []string{"Too many recipients"}, nil} }
|
32
vendor/github.com/mailhog/smtp/state.go
generated
vendored
Normal file
32
vendor/github.com/mailhog/smtp/state.go
generated
vendored
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
package smtp
|
||||||
|
|
||||||
|
// State represents the state of an SMTP conversation
|
||||||
|
type State int
|
||||||
|
|
||||||
|
// SMTP message conversation states
|
||||||
|
const (
|
||||||
|
INVALID = State(-1)
|
||||||
|
ESTABLISH = State(iota)
|
||||||
|
AUTHPLAIN
|
||||||
|
AUTHLOGIN
|
||||||
|
AUTHLOGIN2
|
||||||
|
AUTHCRAMMD5
|
||||||
|
MAIL
|
||||||
|
RCPT
|
||||||
|
DATA
|
||||||
|
DONE
|
||||||
|
)
|
||||||
|
|
||||||
|
// StateMap provides string representations of SMTP conversation states
|
||||||
|
var StateMap = map[State]string{
|
||||||
|
INVALID: "INVALID",
|
||||||
|
ESTABLISH: "ESTABLISH",
|
||||||
|
AUTHPLAIN: "AUTHPLAIN",
|
||||||
|
AUTHLOGIN: "AUTHLOGIN",
|
||||||
|
AUTHLOGIN2: "AUTHLOGIN2",
|
||||||
|
AUTHCRAMMD5: "AUTHCRAMMD5",
|
||||||
|
MAIL: "MAIL",
|
||||||
|
RCPT: "RCPT",
|
||||||
|
DATA: "DATA",
|
||||||
|
DONE: "DONE",
|
||||||
|
}
|
21
vendor/github.com/mailhog/storage/LICENSE.md
generated
vendored
Normal file
21
vendor/github.com/mailhog/storage/LICENSE.md
generated
vendored
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
The MIT License (MIT)
|
||||||
|
|
||||||
|
Copyright (c) 2014 - 2016 Ian Kent
|
||||||
|
|
||||||
|
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.
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue