Merge pull request #266 from sosedoff/connect-backend

Add ability to connect with settings from third-party backend
This commit is contained in:
Dan Sosedoff 2017-09-21 20:37:46 -05:00 committed by GitHub
commit 83598f4013
17 changed files with 275 additions and 1316 deletions

View File

@ -13,6 +13,7 @@ go:
- 1.6.4 - 1.6.4
- 1.7.6 - 1.7.6
- 1.8.3 - 1.8.3
- 1.9
env: env:
- GO15VENDOREXPERIMENT=1 - GO15VENDOREXPERIMENT=1

11
Godeps/Godeps.json generated
View File

@ -1,6 +1,6 @@
{ {
"ImportPath": "github.com/sosedoff/pgweb", "ImportPath": "github.com/sosedoff/pgweb",
"GoVersion": "go1.7", "GoVersion": "go1.8",
"GodepVersion": "v79", "GodepVersion": "v79",
"Packages": [ "Packages": [
"./..." "./..."
@ -49,11 +49,6 @@
"Comment": "sqlx-v1.0-61-gb468c08", "Comment": "sqlx-v1.0-61-gb468c08",
"Rev": "b468c08552f4efac78b94708eb040170a8184c47" "Rev": "b468c08552f4efac78b94708eb040170a8184c47"
}, },
{
"ImportPath": "github.com/julienschmidt/httprouter",
"Comment": "v1-37-gb55664b",
"Rev": "b55664b9e92004aebb7f19a19a9d06271f3a41fc"
},
{ {
"ImportPath": "github.com/lib/pq", "ImportPath": "github.com/lib/pq",
"Comment": "go1.0-cutoff-56-gdc50b6a", "Comment": "go1.0-cutoff-56-gdc50b6a",
@ -81,6 +76,10 @@
"Comment": "v1.1.3", "Comment": "v1.1.3",
"Rev": "f390dcf405f7b83c997eac1b06768bb9f44dec18" "Rev": "f390dcf405f7b83c997eac1b06768bb9f44dec18"
}, },
{
"ImportPath": "github.com/tuvistavie/securerandom",
"Rev": "15512123a948d62f6361bd84818e11f2ad84059a"
},
{ {
"ImportPath": "golang.org/x/crypto/curve25519", "ImportPath": "golang.org/x/crypto/curve25519",
"Rev": "ede567c8e044a5913dad1d1af3696d9da953104c" "Rev": "ede567c8e044a5913dad1d1af3696d9da953104c"

View File

@ -9,6 +9,7 @@ import (
"time" "time"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/tuvistavie/securerandom"
"github.com/sosedoff/pgweb/pkg/bookmarks" "github.com/sosedoff/pgweb/pkg/bookmarks"
"github.com/sosedoff/pgweb/pkg/client" "github.com/sosedoff/pgweb/pkg/client"
@ -69,6 +70,51 @@ func GetSessions(c *gin.Context) {
c.JSON(200, map[string]int{"sessions": len(DbSessions)}) c.JSON(200, map[string]int{"sessions": len(DbSessions)})
} }
func ConnectWithBackend(c *gin.Context) {
// Setup a new backend client
backend := Backend{
Endpoint: command.Opts.ConnectBackend,
Token: command.Opts.ConnectToken,
PassHeaders: command.Opts.ConnectHeaders,
}
// Fetch connection credentials
cred, err := backend.FetchCredential(c.Param("resource"), c)
if err != nil {
c.JSON(400, Error{err.Error()})
return
}
// Make the new session
sessionId, err := securerandom.Uuid()
if err != nil {
c.JSON(400, Error{err.Error()})
return
}
c.Request.Header.Add("x-session-id", sessionId)
// Connect to the database
cl, err := client.NewFromUrl(cred.DatabaseUrl, nil)
if err != nil {
c.JSON(400, Error{err.Error()})
return
}
cl.External = true
// Finalize session seetup
_, err = cl.Info()
if err == nil {
err = setClient(c, cl)
}
if err != nil {
cl.Close()
c.JSON(400, Error{err.Error()})
return
}
c.Redirect(301, fmt.Sprintf("/%s?session=%s", command.Opts.Prefix, sessionId))
}
func Connect(c *gin.Context) { func Connect(c *gin.Context) {
if command.Opts.LockSession { if command.Opts.LockSession {
c.JSON(400, Error{"Session is locked"}) c.JSON(400, Error{"Session is locked"})
@ -141,6 +187,12 @@ func SwitchDb(c *gin.Context) {
return return
} }
// Do not allow switching databases for connections from third-party backends
if conn.External {
c.JSON(400, Error{"Session is locked"})
return
}
currentUrl, err := neturl.Parse(conn.ConnectionString) currentUrl, err := neturl.Parse(conn.ConnectionString)
if err != nil { if err != nil {
c.JSON(400, Error{"Unable to parse current connection string"}) c.JSON(400, Error{"Unable to parse current connection string"})
@ -199,6 +251,12 @@ func Disconnect(c *gin.Context) {
} }
func GetDatabases(c *gin.Context) { func GetDatabases(c *gin.Context) {
conn := DB(c)
if conn.External {
c.JSON(403, Error{"Not permitted"})
return
}
names, err := DB(c).Databases() names, err := DB(c).Databases()
serveResult(names, err, c) serveResult(names, err, c)
} }

75
pkg/api/backend.go Normal file
View File

@ -0,0 +1,75 @@
package api
import (
"bytes"
"encoding/json"
"fmt"
"io/ioutil"
"log"
"net/http"
"strings"
"github.com/gin-gonic/gin"
)
type Backend struct {
Endpoint string
Token string
PassHeaders string
}
type BackendRequest struct {
Resource string `json:"resource"`
Token string `json:"token"`
Headers map[string]string `json:"headers"`
}
type BackendCredential struct {
DatabaseUrl string `json:"database_url"`
}
func (be Backend) FetchCredential(resource string, c *gin.Context) (*BackendCredential, error) {
request := BackendRequest{
Resource: resource,
Token: be.Token,
Headers: map[string]string{},
}
for _, name := range strings.Split(be.PassHeaders, ",") {
request.Headers[strings.ToLower(name)] = c.Request.Header.Get(name)
}
body, err := json.Marshal(request)
if err != nil {
return nil, err
}
resp, err := http.Post(be.Endpoint, "application/json", bytes.NewReader(body))
if err != nil {
// Any connection-related issues will show up in the server log
log.Println("Unable to fetch backend credential:", err)
// We dont want to expose the url of the backend here, so reply with generic error
return nil, fmt.Errorf("Unable to connect to the auth backend")
}
defer resp.Body.Close()
if resp.StatusCode != 200 {
return nil, fmt.Errorf("Got HTTP error %v from backend", resp.StatusCode)
}
respBody, err := ioutil.ReadAll(resp.Body)
if err != nil {
return nil, err
}
cred := &BackendCredential{}
if err := json.Unmarshal(respBody, cred); err != nil {
return nil, err
}
if cred.DatabaseUrl == "" {
return nil, fmt.Errorf("Database url was not provided")
}
return cred, nil
}

View File

@ -49,4 +49,6 @@ func SetupRoutes(router *gin.Engine) {
api.GET("/bookmarks", GetBookmarks) api.GET("/bookmarks", GetBookmarks)
api.GET("/export", DataExport) api.GET("/export", DataExport)
} }
group.GET("/connect/:resource", ConnectWithBackend)
} }

View File

@ -22,6 +22,7 @@ type Client struct {
tunnel *Tunnel tunnel *Tunnel
serverVersion string serverVersion string
lastQueryTime time.Time lastQueryTime time.Time
External bool
History []history.Record `json:"history"` History []history.Record `json:"history"`
ConnectionString string `json:"connection_string"` ConnectionString string `json:"connection_string"`
} }

View File

@ -1,6 +1,7 @@
package command package command
import ( import (
"errors"
"os" "os"
"strings" "strings"
@ -29,6 +30,9 @@ type Options struct {
Bookmark string `short:"b" long:"bookmark" description:"Bookmark to use for connection. Bookmark files are stored under $HOME/.pgweb/bookmarks/*.toml" default:""` Bookmark string `short:"b" long:"bookmark" description:"Bookmark to use for connection. Bookmark files are stored under $HOME/.pgweb/bookmarks/*.toml" default:""`
BookmarksDir string `long:"bookmarks-dir" description:"Overrides default directory for bookmark files to search" default:""` BookmarksDir string `long:"bookmarks-dir" description:"Overrides default directory for bookmark files to search" default:""`
DisablePrettyJson bool `long:"no-pretty-json" description:"Disable JSON formatting feature for result export" default:"false"` DisablePrettyJson bool `long:"no-pretty-json" description:"Disable JSON formatting feature for result export" default:"false"`
ConnectBackend string `long:"connect-backend" description:"Enable database authentication through a third party backend"`
ConnectToken string `long:"connect-token" description:"Authentication token for the third-party connect backend"`
ConnectHeaders string `long:"connect-headers" description:"List of headers to pass to the connect backend"`
} }
var Opts Options var Opts Options
@ -64,5 +68,18 @@ func ParseOptions() error {
Opts.AuthPass = os.Getenv("AUTH_PASS") Opts.AuthPass = os.Getenv("AUTH_PASS")
} }
if Opts.ConnectBackend != "" {
if !Opts.Sessions {
return errors.New("--sessions flag must be set")
}
if Opts.ConnectToken == "" {
return errors.New("--connect-token flag must be set")
}
} else {
if Opts.ConnectToken != "" || Opts.ConnectHeaders != "" {
return errors.New("--connect-backend flag must be set")
}
}
return nil return nil
} }

File diff suppressed because one or more lines are too long

View File

@ -1204,6 +1204,15 @@ $(document).ready(function() {
initEditor(); initEditor();
addShortcutTooltips(); addShortcutTooltips();
// Set session from the url
var reqUrl = new URL(window.location);
var sessionId = reqUrl.searchParams.get("session");
if (sessionId && sessionId != "") {
sessionStorage.setItem("session_id", sessionId);
window.history.pushState({}, document.title, window.location.pathname);
}
apiCall("get", "/connection", {}, function(resp) { apiCall("get", "/connection", {}, function(resp) {
if (resp.error) { if (resp.error) {
connected = false; connected = false;

View File

@ -1,7 +0,0 @@
language: go
go:
- 1.1
- 1.2
- 1.3
- 1.4
- tip

View File

@ -1,24 +0,0 @@
Copyright (c) 2013 Julien Schmidt. All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in the
documentation and/or other materials provided with the distribution.
* The names of the contributors may not be used to endorse or promote
products derived from this software without specific prior written
permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL JULIEN SCHMIDT 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.

View File

@ -1,297 +0,0 @@
# HttpRouter [![Build Status](https://travis-ci.org/julienschmidt/httprouter.png?branch=master)](https://travis-ci.org/julienschmidt/httprouter) [![GoDoc](http://godoc.org/github.com/julienschmidt/httprouter?status.png)](http://godoc.org/github.com/julienschmidt/httprouter)
HttpRouter is a lightweight high performance HTTP request router
(also called *multiplexer* or just *mux* for short) for [Go](http://golang.org/).
In contrast to the default mux of Go's net/http package, this router supports
variables in the routing pattern and matches against the request method.
It also scales better.
The router is optimized for best performance and a small memory footprint.
It scales well even with very long paths and a large number of routes.
A compressing dynamic trie (radix tree) structure is used for efficient matching.
## Features
**Zero Garbage:** The matching and dispatching process generates zero bytes of
garbage. In fact, the only heap allocations that are made, is by building the
slice of the key-value pairs for path parameters. If the request path contains
no parameters, not a single heap allocation is necessary.
**Best Performance:** [Benchmarks speak for themselves](https://github.com/julienschmidt/go-http-routing-benchmark).
See below for technical details of the implementation.
**Parameters in your routing pattern:** Stop parsing the requested URL path,
just give the path segment a name and the router delivers the dynamic value to
you. Because of the design of the router, path parameters are very cheap.
**Only explicit matches:** With other routers, like [http.ServeMux](http://golang.org/pkg/net/http/#ServeMux),
a requested URL path could match multiple patterns. Therefore they have some
awkward pattern priority rules, like *longest match* or *first registered,
first matched*. By design of this router, a request can only match exactly one
or no route. As a result, there are also no unintended matches, which makes it
great for SEO and improves the user experience.
**Stop caring about trailing slashes:** Choose the URL style you like, the
router automatically redirects the client if a trailing slash is missing or if
there is one extra. Of course it only does so, if the new path has a handler.
If you don't like it, you can turn off this behavior.
**Path auto-correction:** Besides detecting the missing or additional trailing
slash at no extra cost, the router can also fix wrong cases and remove
superfluous path elements (like `../` or `//`).
Is [CAPTAIN CAPS LOCK](http://www.urbandictionary.com/define.php?term=Captain+Caps+Lock) one of your users?
HttpRouter can help him by making a case-insensitive look-up and redirecting him
to the correct URL.
**No more server crashes:** You can set a PanicHandler to deal with panics
occurring during handling a HTTP request. The router then recovers and lets the
PanicHandler log what happened and deliver a nice error page.
Of course you can also set a **custom NotFound handler** and **serve static files**.
## Usage
This is just a quick introduction, view the [GoDoc](http://godoc.org/github.com/julienschmidt/httprouter) for details.
Let's start with a trivial example:
```go
package main
import (
"fmt"
"github.com/julienschmidt/httprouter"
"net/http"
"log"
)
func Index(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
fmt.Fprint(w, "Welcome!\n")
}
func Hello(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
fmt.Fprintf(w, "hello, %s!\n", ps.ByName("name"))
}
func main() {
router := httprouter.New()
router.GET("/", Index)
router.GET("/hello/:name", Hello)
log.Fatal(http.ListenAndServe(":8080", router))
}
```
### Named parameters
As you can see, `:name` is a *named parameter*.
The values are accessible via `httprouter.Params`, which is just a slice of `httprouter.Param`s.
You can get the value of a parameter either by its index in the slice, or by using the `ByName(name)` method:
`:name` can be retrived by `ByName("name")`.
Named parameters only match a single path segment:
```
Pattern: /user/:user
/user/gordon match
/user/you match
/user/gordon/profile no match
/user/ no match
```
**Note:** Since this router has only explicit matches, you can not register static routes and parameters for the same path segment. For example you can not register the patterns `/user/new` and `/user/:user` for the same request method at the same time. The routing of different request methods is independent from each other.
### Catch-All parameters
The second type are *catch-all* parameters and have the form `*name`.
Like the name suggests, they match everything.
Therefore they must always be at the **end** of the pattern:
```
Pattern: /src/*filepath
/src/ match
/src/somefile.go match
/src/subdir/somefile.go match
```
## How does it work?
The router relies on a tree structure which makes heavy use of *common prefixes*,
it is basically a *compact* [*prefix tree*](http://en.wikipedia.org/wiki/Trie)
(or just [*Radix tree*](http://en.wikipedia.org/wiki/Radix_tree)).
Nodes with a common prefix also share a common parent. Here is a short example
what the routing tree for the `GET` request method could look like:
```
Priority Path Handle
9 \ *<1>
3 ├s nil
2 |├earch\ *<2>
1 |└upport\ *<3>
2 ├blog\ *<4>
1 | └:post nil
1 | └\ *<5>
2 ├about-us\ *<6>
1 | └team\ *<7>
1 └contact\ *<8>
```
Every `*<num>` represents the memory address of a handler function (a pointer).
If you follow a path trough the tree from the root to the leaf, you get the
complete route path, e.g `\blog\:post\`, where `:post` is just a placeholder
([*parameter*](#named-parameters)) for an actual post name. Unlike hash-maps, a
tree structure also allows us to use dynamic parts like the `:post` parameter,
since we actually match against the routing patterns instead of just comparing
hashes. [As benchmarks show](https://github.com/julienschmidt/go-http-routing-benchmark),
this works very well and efficient.
Since URL paths have a hierarchical structure and make use only of a limited set
of characters (byte values), it is very likely that there are a lot of common
prefixes. This allows us to easily reduce the routing into ever smaller problems.
Moreover the router manages a separate tree for every request method.
For one thing it is more space efficient than holding a method->handle map in
every single node, for another thing is also allows us to greatly reduce the
routing problem before even starting the look-up in the prefix-tree.
For even better scalability, the child nodes on each tree level are ordered by
priority, where the priority is just the number of handles registered in sub
nodes (children, grandchildren, and so on..).
This helps in two ways:
1. Nodes which are part of the most routing paths are evaluated first. This
helps to make as much routes as possible to be reachable as fast as possible.
2. It is some sort of cost compensation. The longest reachable path (highest
cost) can always be evaluated first. The following scheme visualizes the tree
structure. Nodes are evaluated from top to bottom and from left to right.
```
├------------
├---------
├-----
├----
├--
├--
└-
```
## Why doesn't this work with http.Handler?
**It does!** The router itself implements the http.Handler interface.
Moreover the router provides convenient [adapters for http.Handler](http://godoc.org/github.com/julienschmidt/httprouter#Router.Handler)s and [http.HandlerFunc](http://godoc.org/github.com/julienschmidt/httprouter#Router.HandlerFunc)s
which allows them to be used as a [httprouter.Handle](http://godoc.org/github.com/julienschmidt/httprouter#Router.Handle) when registering a route.
The only disadvantage is, that no parameter values can be retrieved when a
http.Handler or http.HandlerFunc is used, since there is no efficient way to
pass the values with the existing function parameters.
Therefore [httprouter.Handle](http://godoc.org/github.com/julienschmidt/httprouter#Router.Handle) has a third function parameter.
Just try it out for yourself, the usage of HttpRouter is very straightforward. The package is compact and minimalistic, but also probably one of the easiest routers to set up.
## Where can I find Middleware *X*?
This package just provides a very efficient request router with a few extra
features. The router is just a [http.Handler](http://golang.org/pkg/net/http/#Handler),
you can chain any http.Handler compatible middleware before the router,
for example the [Gorilla handlers](http://www.gorillatoolkit.org/pkg/handlers).
Or you could [just write your own](http://justinas.org/writing-http-middleware-in-go/),
it's very easy!
Alternatively, you could try [a framework building upon HttpRouter](#web-frameworks--co-based-on-httprouter).
### Multi-domain / Sub-domains
Here is a quick example: Does your server serve multiple domains / hosts?
You want to use sub-domains?
Define a router per host!
```go
// We need an object that implements the http.Handler interface.
// Therefore we need a type for which we implement the ServeHTTP method.
// We just use a map here, in which we map host names (with port) to http.Handlers
type HostSwitch map[string]http.Handler
// Implement the ServerHTTP method on our new type
func (hs HostSwitch) ServeHTTP(w http.ResponseWriter, r *http.Request) {
// Check if a http.Handler is registered for the given host.
// If yes, use it to handle the request.
if handler := hs[r.Host]; handler != nil {
handler.ServeHTTP(w, r)
} else {
// Handle host names for wich no handler is registered
http.Error(w, "Forbidden", 403) // Or Redirect?
}
}
func main() {
// Initialize a router as usual
router := httprouter.New()
router.GET("/", Index)
router.GET("/hello/:name", Hello)
// Make a new HostSwitch and insert the router (our http handler)
// for example.com and port 12345
hs := make(HostSwitch)
hs["example.com:12345"] = router
// Use the HostSwitch to listen and serve on port 12345
log.Fatal(http.ListenAndServe(":12345", hs))
}
```
### Basic Authentication
Another quick example: Basic Authentification (RFC 2617) for handles:
```go
package main
import (
"bytes"
"encoding/base64"
"fmt"
"github.com/julienschmidt/httprouter"
"net/http"
"log"
"strings"
)
func BasicAuth(h httprouter.Handle, user, pass []byte) httprouter.Handle {
return func(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
const basicAuthPrefix string = "Basic "
// Get the Basic Authentication credentials
auth := r.Header.Get("Authorization")
if strings.HasPrefix(auth, basicAuthPrefix) {
// Check credentials
payload, err := base64.StdEncoding.DecodeString(auth[len(basicAuthPrefix):])
if err == nil {
pair := bytes.SplitN(payload, []byte(":"), 2)
if len(pair) == 2 && bytes.Equal(pair[0], user) && bytes.Equal(pair[1], pass) {
// Delegate request to the given handle
h(w, r, ps)
return
}
}
}
// Request Basic Authentication otherwise
w.Header().Set("WWW-Authenticate", "Basic realm=Restricted")
http.Error(w, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)
}
}
func Index(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
fmt.Fprint(w, "Not protected!\n")
}
func Protected(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
fmt.Fprint(w, "Protected!\n")
}
func main() {
user := []byte("gordon")
pass := []byte("secret!")
router := httprouter.New()
router.GET("/", Index)
router.GET("/protected/", BasicAuth(Protected, user, pass))
log.Fatal(http.ListenAndServe(":8080", router))
}
```
## Web Frameworks & Co based on HttpRouter
If the HttpRouter is a bit too minimalistic for you, you might try one of the following more high-level 3rd-party web frameworks building upon the HttpRouter package:
* [Gin](https://github.com/gin-gonic/gin): Features a martini-like API with much better performance
* [Hikaru](https://github.com/najeira/hikaru): Supports standalone and Google AppEngine
* [Hitch](https://github.com/nbio/hitch): Hitch ties httprouter, [httpcontext](https://github.com/nbio/httpcontext), and middleware up in a bow

View File

@ -1,123 +0,0 @@
// Copyright 2013 Julien Schmidt. All rights reserved.
// Based on the path package, Copyright 2009 The Go Authors.
// Use of this source code is governed by a BSD-style license that can be found
// in the LICENSE file.
package httprouter
// CleanPath is the URL version of path.Clean, it returns a canonical URL path
// for p, eliminating . and .. elements.
//
// The following rules are applied iteratively until no further processing can
// be done:
// 1. Replace multiple slashes with a single slash.
// 2. Eliminate each . path name element (the current directory).
// 3. Eliminate each inner .. path name element (the parent directory)
// along with the non-.. element that precedes it.
// 4. Eliminate .. elements that begin a rooted path:
// that is, replace "/.." by "/" at the beginning of a path.
//
// If the result of this process is an empty string, "/" is returned
func CleanPath(p string) string {
// Turn empty string into "/"
if p == "" {
return "/"
}
n := len(p)
var buf []byte
// Invariants:
// reading from path; r is index of next byte to process.
// writing to buf; w is index of next byte to write.
// path must start with '/'
r := 1
w := 1
if p[0] != '/' {
r = 0
buf = make([]byte, n+1)
buf[0] = '/'
}
trailing := n > 2 && p[n-1] == '/'
// A bit more clunky without a 'lazybuf' like the path package, but the loop
// gets completely inlined (bufApp). So in contrast to the path package this
// loop has no expensive function calls (except 1x make)
for r < n {
switch {
case p[r] == '/':
// empty path element, trailing slash is added after the end
r++
case p[r] == '.' && r+1 == n:
trailing = true
r++
case p[r] == '.' && p[r+1] == '/':
// . element
r++
case p[r] == '.' && p[r+1] == '.' && (r+2 == n || p[r+2] == '/'):
// .. element: remove to last /
r += 2
if w > 1 {
// can backtrack
w--
if buf == nil {
for w > 1 && p[w] != '/' {
w--
}
} else {
for w > 1 && buf[w] != '/' {
w--
}
}
}
default:
// real path element.
// add slash if needed
if w > 1 {
bufApp(&buf, p, w, '/')
w++
}
// copy element
for r < n && p[r] != '/' {
bufApp(&buf, p, w, p[r])
w++
r++
}
}
}
// re-append trailing slash
if trailing && w > 1 {
bufApp(&buf, p, w, '/')
w++
}
if buf == nil {
return p[:w]
}
return string(buf[:w])
}
// internal helper to lazily create a buffer if necessary
func bufApp(buf *[]byte, s string, w int, c byte) {
if *buf == nil {
if s[w] == c {
return
}
*buf = make([]byte, len(s))
copy(*buf, s[:w])
}
(*buf)[w] = c
}

View File

@ -1,322 +0,0 @@
// Copyright 2013 Julien Schmidt. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be found
// in the LICENSE file.
// Package httprouter is a trie based high performance HTTP request router.
//
// A trivial example is:
//
// package main
//
// import (
// "fmt"
// "github.com/julienschmidt/httprouter"
// "net/http"
// "log"
// )
//
// func Index(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
// fmt.Fprint(w, "Welcome!\n")
// }
//
// func Hello(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
// fmt.Fprintf(w, "hello, %s!\n", ps.ByName("name"))
// }
//
// func main() {
// router := httprouter.New()
// router.GET("/", Index)
// router.GET("/hello/:name", Hello)
//
// log.Fatal(http.ListenAndServe(":8080", router))
// }
//
// The router matches incoming requests by the request method and the path.
// If a handle is registered for this path and method, the router delegates the
// request to that function.
// For the methods GET, POST, PUT, PATCH and DELETE shortcut functions exist to
// register handles, for all other methods router.Handle can be used.
//
// The registered path, against which the router matches incoming requests, can
// contain two types of parameters:
// Syntax Type
// :name named parameter
// *name catch-all parameter
//
// Named parameters are dynamic path segments. They match anything until the
// next '/' or the path end:
// Path: /blog/:category/:post
//
// Requests:
// /blog/go/request-routers match: category="go", post="request-routers"
// /blog/go/request-routers/ no match, but the router would redirect
// /blog/go/ no match
// /blog/go/request-routers/comments no match
//
// Catch-all parameters match anything until the path end, including the
// directory index (the '/' before the catch-all). Since they match anything
// until the end, catch-all paramerters must always be the final path element.
// Path: /files/*filepath
//
// Requests:
// /files/ match: filepath="/"
// /files/LICENSE match: filepath="/LICENSE"
// /files/templates/article.html match: filepath="/templates/article.html"
// /files no match, but the router would redirect
//
// The value of parameters is saved as a slice of the Param struct, consisting
// each of a key and a value. The slice is passed to the Handle func as a third
// parameter.
// There are two ways to retrieve the value of a parameter:
// // by the name of the parameter
// user := ps.ByName("user") // defined by :user or *user
//
// // by the index of the parameter. This way you can also get the name (key)
// thirdKey := ps[2].Key // the name of the 3rd parameter
// thirdValue := ps[2].Value // the value of the 3rd parameter
package httprouter
import (
"net/http"
)
// Handle is a function that can be registered to a route to handle HTTP
// requests. Like http.HandlerFunc, but has a third parameter for the values of
// wildcards (variables).
type Handle func(http.ResponseWriter, *http.Request, Params)
// Param is a single URL parameter, consisting of a key and a value.
type Param struct {
Key string
Value string
}
// Params is a Param-slice, as returned by the router.
// The slice is ordered, the first URL parameter is also the first slice value.
// It is therefore safe to read values by the index.
type Params []Param
// ByName returns the value of the first Param which key matches the given name.
// If no matching Param is found, an empty string is returned.
func (ps Params) ByName(name string) string {
for i := range ps {
if ps[i].Key == name {
return ps[i].Value
}
}
return ""
}
// Router is a http.Handler which can be used to dispatch requests to different
// handler functions via configurable routes
type Router struct {
trees map[string]*node
// Enables automatic redirection if the current route can't be matched but a
// handler for the path with (without) the trailing slash exists.
// For example if /foo/ is requested but a route only exists for /foo, the
// client is redirected to /foo with http status code 301 for GET requests
// and 307 for all other request methods.
RedirectTrailingSlash bool
// If enabled, the router tries to fix the current request path, if no
// handle is registered for it.
// First superfluous path elements like ../ or // are removed.
// Afterwards the router does a case-insensitive lookup of the cleaned path.
// If a handle can be found for this route, the router makes a redirection
// to the corrected path with status code 301 for GET requests and 307 for
// all other request methods.
// For example /FOO and /..//Foo could be redirected to /foo.
// RedirectTrailingSlash is independent of this option.
RedirectFixedPath bool
// Configurable http.HandlerFunc which is called when no matching route is
// found. If it is not set, http.NotFound is used.
NotFound http.HandlerFunc
// Function to handle panics recovered from http handlers.
// It should be used to generate a error page and return the http error code
// 500 (Internal Server Error).
// The handler can be used to keep your server from crashing because of
// unrecovered panics.
PanicHandler func(http.ResponseWriter, *http.Request, interface{})
}
// Make sure the Router conforms with the http.Handler interface
var _ http.Handler = New()
// New returns a new initialized Router.
// Path auto-correction, including trailing slashes, is enabled by default.
func New() *Router {
return &Router{
RedirectTrailingSlash: true,
RedirectFixedPath: true,
}
}
// GET is a shortcut for router.Handle("GET", path, handle)
func (r *Router) GET(path string, handle Handle) {
r.Handle("GET", path, handle)
}
// HEAD is a shortcut for router.Handle("HEAD", path, handle)
func (r *Router) HEAD(path string, handle Handle) {
r.Handle("HEAD", path, handle)
}
// POST is a shortcut for router.Handle("POST", path, handle)
func (r *Router) POST(path string, handle Handle) {
r.Handle("POST", path, handle)
}
// PUT is a shortcut for router.Handle("PUT", path, handle)
func (r *Router) PUT(path string, handle Handle) {
r.Handle("PUT", path, handle)
}
// PATCH is a shortcut for router.Handle("PATCH", path, handle)
func (r *Router) PATCH(path string, handle Handle) {
r.Handle("PATCH", path, handle)
}
// DELETE is a shortcut for router.Handle("DELETE", path, handle)
func (r *Router) DELETE(path string, handle Handle) {
r.Handle("DELETE", path, handle)
}
// Handle registers a new request handle with the given path and method.
//
// For GET, POST, PUT, PATCH and DELETE requests the respective shortcut
// functions can be used.
//
// This function is intended for bulk loading and to allow the usage of less
// frequently used, non-standardized or custom methods (e.g. for internal
// communication with a proxy).
func (r *Router) Handle(method, path string, handle Handle) {
if path[0] != '/' {
panic("path must begin with '/'")
}
if r.trees == nil {
r.trees = make(map[string]*node)
}
root := r.trees[method]
if root == nil {
root = new(node)
r.trees[method] = root
}
root.addRoute(path, handle)
}
// Handler is an adapter which allows the usage of an http.Handler as a
// request handle.
func (r *Router) Handler(method, path string, handler http.Handler) {
r.Handle(method, path,
func(w http.ResponseWriter, req *http.Request, _ Params) {
handler.ServeHTTP(w, req)
},
)
}
// HandlerFunc is an adapter which allows the usage of an http.HandlerFunc as a
// request handle.
func (r *Router) HandlerFunc(method, path string, handler http.HandlerFunc) {
r.Handle(method, path,
func(w http.ResponseWriter, req *http.Request, _ Params) {
handler(w, req)
},
)
}
// ServeFiles serves files from the given file system root.
// The path must end with "/*filepath", files are then served from the local
// path /defined/root/dir/*filepath.
// For example if root is "/etc" and *filepath is "passwd", the local file
// "/etc/passwd" would be served.
// Internally a http.FileServer is used, therefore http.NotFound is used instead
// of the Router's NotFound handler.
// To use the operating system's file system implementation,
// use http.Dir:
// router.ServeFiles("/src/*filepath", http.Dir("/var/www"))
func (r *Router) ServeFiles(path string, root http.FileSystem) {
if len(path) < 10 || path[len(path)-10:] != "/*filepath" {
panic("path must end with /*filepath")
}
fileServer := http.FileServer(root)
r.GET(path, func(w http.ResponseWriter, req *http.Request, ps Params) {
req.URL.Path = ps.ByName("filepath")
fileServer.ServeHTTP(w, req)
})
}
func (r *Router) recv(w http.ResponseWriter, req *http.Request) {
if rcv := recover(); rcv != nil {
r.PanicHandler(w, req, rcv)
}
}
// Lookup allows the manual lookup of a method + path combo.
// This is e.g. useful to build a framework around this router.
func (r *Router) Lookup(method, path string) (Handle, Params, bool) {
if root := r.trees[method]; root != nil {
return root.getValue(path)
}
return nil, nil, false
}
// ServeHTTP makes the router implement the http.Handler interface.
func (r *Router) ServeHTTP(w http.ResponseWriter, req *http.Request) {
if r.PanicHandler != nil {
defer r.recv(w, req)
}
if root := r.trees[req.Method]; root != nil {
path := req.URL.Path
if handle, ps, tsr := root.getValue(path); handle != nil {
handle(w, req, ps)
return
} else if req.Method != "CONNECT" && path != "/" {
code := 301 // Permanent redirect, request with GET method
if req.Method != "GET" {
// Temporary redirect, request with same method
// As of Go 1.3, Go does not support status code 308.
code = 307
}
if tsr && r.RedirectTrailingSlash {
if len(path) > 1 && path[len(path)-1] == '/' {
req.URL.Path = path[:len(path)-1]
} else {
req.URL.Path = path + "/"
}
http.Redirect(w, req, req.URL.String(), code)
return
}
// Try to fix the request path
if r.RedirectFixedPath {
fixedPath, found := root.findCaseInsensitivePath(
CleanPath(path),
r.RedirectTrailingSlash,
)
if found {
req.URL.Path = string(fixedPath)
http.Redirect(w, req, req.URL.String(), code)
return
}
}
}
}
// Handle 404
if r.NotFound != nil {
r.NotFound(w, req)
} else {
http.NotFound(w, req)
}
}

View File

@ -1,534 +0,0 @@
// Copyright 2013 Julien Schmidt. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be found
// in the LICENSE file.
package httprouter
import (
"strings"
"unicode"
)
func min(a, b int) int {
if a <= b {
return a
}
return b
}
func countParams(path string) uint8 {
var n uint
for i := 0; i < len(path); i++ {
if path[i] != ':' && path[i] != '*' {
continue
}
n++
}
if n >= 255 {
return 255
}
return uint8(n)
}
type nodeType uint8
const (
static nodeType = 0
param nodeType = 1
catchAll nodeType = 2
)
type node struct {
path string
wildChild bool
nType nodeType
maxParams uint8
indices []byte
children []*node
handle Handle
priority uint32
}
// increments priority of the given child and reorders if necessary
func (n *node) incrementChildPrio(i int) int {
n.children[i].priority++
prio := n.children[i].priority
// adjust position (move to front)
for j := i - 1; j >= 0 && n.children[j].priority < prio; j-- {
// swap node positions
tmpN := n.children[j]
n.children[j] = n.children[i]
n.children[i] = tmpN
tmpI := n.indices[j]
n.indices[j] = n.indices[i]
n.indices[i] = tmpI
i--
}
return i
}
// addRoute adds a node with the given handle to the path.
// Not concurrency-safe!
func (n *node) addRoute(path string, handle Handle) {
n.priority++
numParams := countParams(path)
// non-empty tree
if len(n.path) > 0 || len(n.children) > 0 {
WALK:
for {
// Update maxParams of the current node
if numParams > n.maxParams {
n.maxParams = numParams
}
// Find the longest common prefix.
// This also implies that the commom prefix contains no ':' or '*'
// since the existing key can't contain this chars.
i := 0
for max := min(len(path), len(n.path)); i < max && path[i] == n.path[i]; i++ {
}
// Split edge
if i < len(n.path) {
child := node{
path: n.path[i:],
wildChild: n.wildChild,
indices: n.indices,
children: n.children,
handle: n.handle,
priority: n.priority - 1,
}
// Update maxParams (max of all children)
for i := range child.children {
if child.children[i].maxParams > child.maxParams {
child.maxParams = child.children[i].maxParams
}
}
n.children = []*node{&child}
n.indices = []byte{n.path[i]}
n.path = path[:i]
n.handle = nil
n.wildChild = false
}
// Make new node a child of this node
if i < len(path) {
path = path[i:]
if n.wildChild {
n = n.children[0]
n.priority++
// Update maxParams of the child node
if numParams > n.maxParams {
n.maxParams = numParams
}
numParams--
// Check if the wildcard matches
if len(path) >= len(n.path) && n.path == path[:len(n.path)] {
// check for longer wildcard, e.g. :name and :names
if len(n.path) >= len(path) || path[len(n.path)] == '/' {
continue WALK
}
}
panic("conflict with wildcard route")
}
c := path[0]
// slash after param
if n.nType == param && c == '/' && len(n.children) == 1 {
n = n.children[0]
n.priority++
continue WALK
}
// Check if a child with the next path byte exists
for i, index := range n.indices {
if c == index {
i = n.incrementChildPrio(i)
n = n.children[i]
continue WALK
}
}
// Otherwise insert it
if c != ':' && c != '*' {
n.indices = append(n.indices, c)
child := &node{
maxParams: numParams,
}
n.children = append(n.children, child)
n.incrementChildPrio(len(n.indices) - 1)
n = child
}
n.insertChild(numParams, path, handle)
return
} else if i == len(path) { // Make node a (in-path) leaf
if n.handle != nil {
panic("a Handle is already registered for this path")
}
n.handle = handle
}
return
}
} else { // Empty tree
n.insertChild(numParams, path, handle)
}
}
func (n *node) insertChild(numParams uint8, path string, handle Handle) {
var offset int
// find prefix until first wildcard (beginning with ':'' or '*'')
for i, max := 0, len(path); numParams > 0; i++ {
c := path[i]
if c != ':' && c != '*' {
continue
}
// Check if this Node existing children which would be
// unreachable if we insert the wildcard here
if len(n.children) > 0 {
panic("wildcard route conflicts with existing children")
}
// find wildcard end (either '/' or path end)
end := i + 1
for end < max && path[end] != '/' {
end++
}
if end-i < 2 {
panic("wildcards must be named with a non-empty name")
}
if c == ':' { // param
// split path at the beginning of the wildcard
if i > 0 {
n.path = path[offset:i]
offset = i
}
child := &node{
nType: param,
maxParams: numParams,
}
n.children = []*node{child}
n.wildChild = true
n = child
n.priority++
numParams--
// if the path doesn't end with the wildcard, then there
// will be another non-wildcard subpath starting with '/'
if end < max {
n.path = path[offset:end]
offset = end
child := &node{
maxParams: numParams,
priority: 1,
}
n.children = []*node{child}
n = child
}
} else { // catchAll
if end != max || numParams > 1 {
panic("catch-all routes are only allowed at the end of the path")
}
if len(n.path) > 0 && n.path[len(n.path)-1] == '/' {
panic("catch-all conflicts with existing handle for the path segment root")
}
// currently fixed width 1 for '/'
i--
if path[i] != '/' {
panic("no / before catch-all")
}
n.path = path[offset:i]
// first node: catchAll node with empty path
child := &node{
wildChild: true,
nType: catchAll,
maxParams: 1,
}
n.children = []*node{child}
n.indices = []byte{path[i]}
n = child
n.priority++
// second node: node holding the variable
child = &node{
path: path[i:],
nType: catchAll,
maxParams: 1,
handle: handle,
priority: 1,
}
n.children = []*node{child}
return
}
}
// insert remaining path part and handle to the leaf
n.path = path[offset:]
n.handle = handle
}
// Returns the handle registered with the given path (key). The values of
// wildcards are saved to a map.
// If no handle can be found, a TSR (trailing slash redirect) recommendation is
// made if a handle exists with an extra (without the) trailing slash for the
// given path.
func (n *node) getValue(path string) (handle Handle, p Params, tsr bool) {
walk: // Outer loop for walking the tree
for {
if len(path) > len(n.path) {
if path[:len(n.path)] == n.path {
path = path[len(n.path):]
// If this node does not have a wildcard (param or catchAll)
// child, we can just look up the next child node and continue
// to walk down the tree
if !n.wildChild {
c := path[0]
for i, index := range n.indices {
if c == index {
n = n.children[i]
continue walk
}
}
// Nothing found.
// We can recommend to redirect to the same URL without a
// trailing slash if a leaf exists for that path.
tsr = (path == "/" && n.handle != nil)
return
}
// handle wildcard child
n = n.children[0]
switch n.nType {
case param:
// find param end (either '/' or path end)
end := 0
for end < len(path) && path[end] != '/' {
end++
}
// save param value
if p == nil {
// lazy allocation
p = make(Params, 0, n.maxParams)
}
i := len(p)
p = p[:i+1] // expand slice within preallocated capacity
p[i].Key = n.path[1:]
p[i].Value = path[:end]
// we need to go deeper!
if end < len(path) {
if len(n.children) > 0 {
path = path[end:]
n = n.children[0]
continue walk
}
// ... but we can't
tsr = (len(path) == end+1)
return
}
if handle = n.handle; handle != nil {
return
} else if len(n.children) == 1 {
// No handle found. Check if a handle for this path + a
// trailing slash exists for TSR recommendation
n = n.children[0]
tsr = (n.path == "/" && n.handle != nil)
}
return
case catchAll:
// save param value
if p == nil {
// lazy allocation
p = make(Params, 0, n.maxParams)
}
i := len(p)
p = p[:i+1] // expand slice within preallocated capacity
p[i].Key = n.path[2:]
p[i].Value = path
handle = n.handle
return
default:
panic("Unknown node type")
}
}
} else if path == n.path {
// We should have reached the node containing the handle.
// Check if this node has a handle registered.
if handle = n.handle; handle != nil {
return
}
// No handle found. Check if a handle for this path + a
// trailing slash exists for trailing slash recommendation
for i, index := range n.indices {
if index == '/' {
n = n.children[i]
tsr = (n.path == "/" && n.handle != nil) ||
(n.nType == catchAll && n.children[0].handle != nil)
return
}
}
return
}
// Nothing found. We can recommend to redirect to the same URL with an
// extra trailing slash if a leaf exists for that path
tsr = (path == "/") ||
(len(n.path) == len(path)+1 && n.path[len(path)] == '/' &&
path == n.path[:len(n.path)-1] && n.handle != nil)
return
}
}
// Makes a case-insensitive lookup of the given path and tries to find a handler.
// It can optionally also fix trailing slashes.
// It returns the case-corrected path and a bool indicating wether the lookup
// was successful.
func (n *node) findCaseInsensitivePath(path string, fixTrailingSlash bool) (ciPath []byte, found bool) {
ciPath = make([]byte, 0, len(path)+1) // preallocate enough memory
// Outer loop for walking the tree
for len(path) >= len(n.path) && strings.ToLower(path[:len(n.path)]) == strings.ToLower(n.path) {
path = path[len(n.path):]
ciPath = append(ciPath, n.path...)
if len(path) > 0 {
// If this node does not have a wildcard (param or catchAll) child,
// we can just look up the next child node and continue to walk down
// the tree
if !n.wildChild {
r := unicode.ToLower(rune(path[0]))
for i, index := range n.indices {
// must use recursive approach since both index and
// ToLower(index) could exist. We must check both.
if r == unicode.ToLower(rune(index)) {
out, found := n.children[i].findCaseInsensitivePath(path, fixTrailingSlash)
if found {
return append(ciPath, out...), true
}
}
}
// Nothing found. We can recommend to redirect to the same URL
// without a trailing slash if a leaf exists for that path
found = (fixTrailingSlash && path == "/" && n.handle != nil)
return
} else {
n = n.children[0]
switch n.nType {
case param:
// find param end (either '/' or path end)
k := 0
for k < len(path) && path[k] != '/' {
k++
}
// add param value to case insensitive path
ciPath = append(ciPath, path[:k]...)
// we need to go deeper!
if k < len(path) {
if len(n.children) > 0 {
path = path[k:]
n = n.children[0]
continue
} else { // ... but we can't
if fixTrailingSlash && len(path) == k+1 {
return ciPath, true
}
return
}
}
if n.handle != nil {
return ciPath, true
} else if fixTrailingSlash && len(n.children) == 1 {
// No handle found. Check if a handle for this path + a
// trailing slash exists
n = n.children[0]
if n.path == "/" && n.handle != nil {
return append(ciPath, '/'), true
}
}
return
case catchAll:
return append(ciPath, path...), true
default:
panic("Unknown node type")
}
}
} else {
// We should have reached the node containing the handle.
// Check if this node has a handle registered.
if n.handle != nil {
return ciPath, true
}
// No handle found.
// Try to fix the path by adding a trailing slash
if fixTrailingSlash {
for i, index := range n.indices {
if index == '/' {
n = n.children[i]
if (n.path == "/" && n.handle != nil) ||
(n.nType == catchAll && n.children[0].handle != nil) {
return append(ciPath, '/'), true
}
return
}
}
}
return
}
}
// Nothing found.
// Try to fix the path by adding / removing a trailing slash
if fixTrailingSlash {
if path == "/" {
return ciPath, true
}
if len(path)+1 == len(n.path) && n.path[len(path)] == '/' &&
strings.ToLower(path) == strings.ToLower(n.path[:len(path)]) &&
n.handle != nil {
return append(ciPath, n.path...), true
}
}
return
}

34
vendor/github.com/tuvistavie/securerandom/README.md generated vendored Normal file
View File

@ -0,0 +1,34 @@
# securerandom
Port of Ruby `securerandom` module for Golang.
The following functions are implemented.
```go
func RandomBytes(n int) ([]byte, error)
func Base64(n int, padded bool) (string, error)
func UrlSafeBase64(n int, padded bool) (string, error)
func Hex(n int) (string, error)
func Uuid() (string, error)
```
## Sample usage
```go
package main
import (
"fmt"
sr "github.com/tuvistavie/securerandom"
)
func main() {
b, _ := sr.Base64(10, true)
fmt.Println(b)
b, _ = sr.Hex(10)
fmt.Println(b)
b, _ = sr.Uuid()
fmt.Println(b)
}
```
For more information, check out the [documentation of the ruby module](http://ruby-doc.org/stdlib-2.1.0/libdoc/securerandom/rdoc/SecureRandom.html).

70
vendor/github.com/tuvistavie/securerandom/srandom.go generated vendored Normal file
View File

@ -0,0 +1,70 @@
package securerandom
import (
"bytes"
"crypto/rand"
"encoding/base64"
"encoding/binary"
"encoding/hex"
"fmt"
"strings"
)
func RandomBytes(n int) ([]byte, error) {
bytes := make([]byte, n)
_, err := rand.Read(bytes)
if err != nil {
return nil, err
}
return bytes, nil
}
func Base64(n int, padded bool) (string, error) {
bytes, err := RandomBytes(n)
if err != nil {
return "", err
}
result := base64.StdEncoding.EncodeToString(bytes)
result = strings.Replace(result, "\n", "", -1)
if !padded {
result = strings.Replace(result, "=", "", -1)
}
return result, nil
}
func UrlSafeBase64(n int, padded bool) (string, error) {
result, err := Base64(n, padded)
if err != nil {
return "", err
}
result = strings.Replace(result, "+", "-", -1)
result = strings.Replace(result, "/", "_", -1)
return result, nil
}
func Hex(n int) (string, error) {
bytes, err := RandomBytes(n)
if err != nil {
return "", err
}
return hex.EncodeToString(bytes), nil
}
func Uuid() (string, error) {
var first, last uint32
var middle [4]uint16
randomBytes, err := RandomBytes(16)
if err != nil {
return "", err
}
buffer := bytes.NewBuffer(randomBytes)
binary.Read(buffer, binary.BigEndian, &first)
for i := 0; i < 4; i++ {
binary.Read(buffer, binary.BigEndian, &middle[i])
}
binary.Read(buffer, binary.BigEndian, &last)
middle[1] = (middle[1] & 0x0fff) | 0x4000
middle[2] = (middle[2] & 0x3fff) | 0x8000
return fmt.Sprintf("%08x-%04x-%04x-%04x-%04x%08x",
first, middle[0], middle[1], middle[2], middle[3], last), nil
}