mirror of
https://github.com/labstack/echo.git
synced 2025-04-27 12:32:09 +02:00
initial commit
Signed-off-by: Vishal Rana <vr@labstack.com>
This commit is contained in:
commit
7c9a0b6489
22
LICENSE
Normal file
22
LICENSE
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
The MIT License (MIT)
|
||||||
|
|
||||||
|
Copyright (c) 2014 labstack
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
78
README.md
Normal file
78
README.md
Normal file
@ -0,0 +1,78 @@
|
|||||||
|
# Bolt
|
||||||
|
Multi transport, multi format REST style network library
|
||||||
|
|
||||||
|
## Socket Specification
|
||||||
|
|
||||||
|
### Trasport
|
||||||
|
- WebSocket
|
||||||
|
- TCP
|
||||||
|
|
||||||
|
### Command (*1-byte*)
|
||||||
|
- INIT (**1**)
|
||||||
|
- AUTH (**2**)
|
||||||
|
- HTTP (**3**)
|
||||||
|
- PUB (**4**)
|
||||||
|
- MPUB (**5**)
|
||||||
|
- SUB (**6**)
|
||||||
|
- USUB (**7**)
|
||||||
|
|
||||||
|
### INIT
|
||||||
|
> Request
|
||||||
|
|
||||||
|
```sh
|
||||||
|
1 # Command (1-byte)
|
||||||
|
99 # Correlation ID (4-byte)
|
||||||
|
8 # Config length (2-byte)
|
||||||
|
{} # Config as JSON (n-byte)
|
||||||
|
```
|
||||||
|
> Config
|
||||||
|
```js
|
||||||
|
{
|
||||||
|
"Format":
|
||||||
|
}
|
||||||
|
```
|
||||||
|
> Response
|
||||||
|
|
||||||
|
```sh
|
||||||
|
99 # Correlation ID (4-byte)
|
||||||
|
200 # Status code (2-byte)
|
||||||
|
```
|
||||||
|
|
||||||
|
### AUTH
|
||||||
|
> Request
|
||||||
|
|
||||||
|
```sh
|
||||||
|
2 # Command (1-byte)
|
||||||
|
99 # Correlation ID (4-byte)
|
||||||
|
30 # Token length (2-byte)
|
||||||
|
1g42*jMG!a?D3eF>Xwt!dI05]Y9egP # Token (n-byte)
|
||||||
|
```
|
||||||
|
> Response
|
||||||
|
|
||||||
|
```sh
|
||||||
|
99 # Correlation ID (4-byte)
|
||||||
|
200 # Status code (2-byte)
|
||||||
|
```
|
||||||
|
|
||||||
|
### HTTP
|
||||||
|
> Request
|
||||||
|
|
||||||
|
```sh
|
||||||
|
3 # Command (1-byte)
|
||||||
|
99 # Correlation ID (4-byte)
|
||||||
|
GET\n # Method (n-byte)
|
||||||
|
/users\n # Path (n-byte)
|
||||||
|
- # Headers
|
||||||
|
64 # Body length (8-byte)
|
||||||
|
- # Body (n-byte)
|
||||||
|
```
|
||||||
|
> Response
|
||||||
|
|
||||||
|
```sh
|
||||||
|
3 # Command (1-byte)
|
||||||
|
200 # Status code (2-byte)
|
||||||
|
|
||||||
|
# For POST, PUT & PATCH
|
||||||
|
64 # Body length (8-byte)
|
||||||
|
- # Body (n-byte)
|
||||||
|
```
|
259
bolt.go
Normal file
259
bolt.go
Normal file
@ -0,0 +1,259 @@
|
|||||||
|
package bolt
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"io"
|
||||||
|
"log"
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"golang.org/x/net/websocket"
|
||||||
|
)
|
||||||
|
|
||||||
|
type (
|
||||||
|
Bolt struct {
|
||||||
|
Router *router
|
||||||
|
handlers []HandlerFunc
|
||||||
|
maxParam byte
|
||||||
|
notFoundHandler HandlerFunc
|
||||||
|
methodNotAllowedHandler HandlerFunc
|
||||||
|
tcpListener *net.TCPListener
|
||||||
|
// udpConn *net.UDPConn
|
||||||
|
pool sync.Pool
|
||||||
|
}
|
||||||
|
|
||||||
|
command byte
|
||||||
|
format byte
|
||||||
|
transport byte
|
||||||
|
|
||||||
|
HandlerFunc func(*Context)
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
CmdINIT command = 1 + iota
|
||||||
|
CmdAUTH
|
||||||
|
CmdHTTP
|
||||||
|
CmdPUB
|
||||||
|
CmdSUB
|
||||||
|
CmdUSUB
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
TrnspHTTP transport = 1 + iota
|
||||||
|
TrnspWS
|
||||||
|
TrnspTCP
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
FmtJSON format = 1 + iota
|
||||||
|
FmtMsgPack
|
||||||
|
FmtBinary = 20
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
MIME_JSON = "application/json"
|
||||||
|
MIME_MP = "application/x-msgpack"
|
||||||
|
|
||||||
|
HdrAccept = "Accept"
|
||||||
|
HdrContentDisposition = "Content-Disposition"
|
||||||
|
HdrContentLength = "Content-Length"
|
||||||
|
HdrContentType = "Content-Type"
|
||||||
|
)
|
||||||
|
|
||||||
|
var MethodMap = map[string]uint8{
|
||||||
|
"CONNECT": 1,
|
||||||
|
"DELETE": 2,
|
||||||
|
"GET": 3,
|
||||||
|
"HEAD": 4,
|
||||||
|
"OPTIONS": 5,
|
||||||
|
"PATCH": 6,
|
||||||
|
"POST": 7,
|
||||||
|
"PUT": 8,
|
||||||
|
"TRACE": 9,
|
||||||
|
}
|
||||||
|
|
||||||
|
func New(opts ...func(*Bolt)) (b *Bolt) {
|
||||||
|
b = &Bolt{
|
||||||
|
maxParam: 5,
|
||||||
|
notFoundHandler: func(c *Context) {
|
||||||
|
http.Error(c.Writer, http.StatusText(http.StatusNotFound), http.StatusNotFound)
|
||||||
|
},
|
||||||
|
methodNotAllowedHandler: func(c *Context) {
|
||||||
|
http.Error(c.Writer, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
b.Router = NewRouter(b)
|
||||||
|
b.pool.New = func() interface{} {
|
||||||
|
return &Context{
|
||||||
|
Writer: NewResponse(nil),
|
||||||
|
Socket: new(Socket),
|
||||||
|
params: make(Params, b.maxParam),
|
||||||
|
store: make(store),
|
||||||
|
i: -1,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set options
|
||||||
|
for _, o := range opts {
|
||||||
|
o(b)
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func MaxParam(n uint8) func(*Bolt) {
|
||||||
|
return func(b *Bolt) {
|
||||||
|
b.maxParam = n
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func NotFoundHandler(h HandlerFunc) func(*Bolt) {
|
||||||
|
return func(b *Bolt) {
|
||||||
|
b.notFoundHandler = h
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func MethodNotAllowedHandler(h HandlerFunc) func(*Bolt) {
|
||||||
|
return func(b *Bolt) {
|
||||||
|
b.methodNotAllowedHandler = h
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use adds middleware(s) in the chain.
|
||||||
|
func (b *Bolt) Use(h ...HandlerFunc) {
|
||||||
|
b.handlers = append(b.handlers, h...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Bolt) Connect(path string, h ...HandlerFunc) {
|
||||||
|
b.Handle("CONNECT", path, h)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Bolt) Delete(path string, h ...HandlerFunc) {
|
||||||
|
b.Handle("DELETE", path, h)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Bolt) Get(path string, h ...HandlerFunc) {
|
||||||
|
b.Handle("GET", path, h)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Bolt) Head(path string, h ...HandlerFunc) {
|
||||||
|
b.Handle("HEAD", path, h)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Bolt) Options(path string, h ...HandlerFunc) {
|
||||||
|
b.Handle("OPTIONS", path, h)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Bolt) Patch(path string, h ...HandlerFunc) {
|
||||||
|
b.Handle("PATCH", path, h)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Bolt) Post(path string, h ...HandlerFunc) {
|
||||||
|
b.Handle("POST", path, h)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Bolt) Put(path string, h ...HandlerFunc) {
|
||||||
|
b.Handle("PUT", path, h)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Bolt) Trace(path string, h ...HandlerFunc) {
|
||||||
|
b.Handle("TRACE", path, h)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Bolt) Handle(method, path string, h []HandlerFunc) {
|
||||||
|
h = append(b.handlers, h...)
|
||||||
|
l := len(h)
|
||||||
|
b.Router.Add(method, path, func(c *Context) {
|
||||||
|
c.handlers = h
|
||||||
|
c.l = l
|
||||||
|
c.i = -1
|
||||||
|
c.Next()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Bolt) ServeHTTP(rw http.ResponseWriter, r *http.Request) {
|
||||||
|
// Find and execute handler
|
||||||
|
h, c, s := b.Router.Find(r.Method, r.URL.Path)
|
||||||
|
if h != nil {
|
||||||
|
c.Transport = TrnspHTTP
|
||||||
|
c.Writer.ResponseWriter = rw
|
||||||
|
c.Request = r
|
||||||
|
h(c)
|
||||||
|
} else {
|
||||||
|
if s == NotFound {
|
||||||
|
b.notFoundHandler(c)
|
||||||
|
} else if s == NotAllowed {
|
||||||
|
b.methodNotAllowedHandler(c)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
b.pool.Put(c)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Bolt) handleSocket(conn io.ReadWriteCloser, tp transport) {
|
||||||
|
// TODO: From pool?
|
||||||
|
defer conn.Close()
|
||||||
|
s := &Socket{
|
||||||
|
Transport: tp,
|
||||||
|
config: Config{},
|
||||||
|
conn: conn,
|
||||||
|
Reader: bufio.NewReader(conn),
|
||||||
|
Writer: bufio.NewWriter(conn),
|
||||||
|
bolt: b,
|
||||||
|
}
|
||||||
|
Loop:
|
||||||
|
for {
|
||||||
|
c, err := s.Reader.ReadByte() // Command
|
||||||
|
if err != nil {
|
||||||
|
log.Println(err, 222, c)
|
||||||
|
}
|
||||||
|
cmd := command(c)
|
||||||
|
println(cmd)
|
||||||
|
switch cmd {
|
||||||
|
case CmdINIT:
|
||||||
|
s.Init()
|
||||||
|
case CmdHTTP:
|
||||||
|
s.HTTP()
|
||||||
|
default:
|
||||||
|
break Loop
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Bolt) RunHTTP(addr string) {
|
||||||
|
log.Fatal(http.ListenAndServe(addr, b))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Bolt) RunWS(addr string) {
|
||||||
|
http.Handle("/", websocket.Handler(func(ws *websocket.Conn) {
|
||||||
|
b.handleSocket(ws, TrnspWS)
|
||||||
|
}))
|
||||||
|
log.Fatal(http.ListenAndServe(addr, nil))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Bolt) RunTCP(addr string) {
|
||||||
|
a, _ := net.ResolveTCPAddr("tcp", addr)
|
||||||
|
l, err := net.ListenTCP("tcp", a)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("bolt: %v", err)
|
||||||
|
}
|
||||||
|
b.tcpListener = l
|
||||||
|
go b.serve("tcp")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Bolt) serve(net string) {
|
||||||
|
switch net {
|
||||||
|
case "ws":
|
||||||
|
case "tcp":
|
||||||
|
for {
|
||||||
|
conn, err := b.tcpListener.Accept()
|
||||||
|
if err != nil {
|
||||||
|
log.Print(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
go b.handleSocket(conn, TrnspTCP)
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
// TODO: handle it!
|
||||||
|
}
|
||||||
|
}
|
145
bolt_test.go
Normal file
145
bolt_test.go
Normal file
@ -0,0 +1,145 @@
|
|||||||
|
package bolt
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/binary"
|
||||||
|
"encoding/json"
|
||||||
|
"io"
|
||||||
|
"net"
|
||||||
|
"sync"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
var u = user{
|
||||||
|
Id: "1",
|
||||||
|
Name: "Joe",
|
||||||
|
}
|
||||||
|
|
||||||
|
func startTCPServer() (b *Bolt, addr string) {
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
b = New()
|
||||||
|
a, _ := net.ResolveTCPAddr("tcp", "localhost:0")
|
||||||
|
wg.Add(1)
|
||||||
|
go func() {
|
||||||
|
defer wg.Done()
|
||||||
|
b.RunTCP(a.String())
|
||||||
|
}()
|
||||||
|
wg.Wait()
|
||||||
|
addr = b.tcpListener.Addr().String()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func connectTCPServer(addr string) (conn net.Conn, err error) {
|
||||||
|
conn, err = net.DialTimeout("tcp", addr, time.Second)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSocketInit(t *testing.T) {
|
||||||
|
b, addr := startTCPServer()
|
||||||
|
conn, err := connectTCPServer(addr)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer conn.Close()
|
||||||
|
defer b.tcpListener.Close()
|
||||||
|
|
||||||
|
// Request
|
||||||
|
buf := new(bytes.Buffer)
|
||||||
|
buf.WriteByte(byte(CmdINIT)) // Command
|
||||||
|
cfg := &Config{
|
||||||
|
Format: FmtJSON,
|
||||||
|
}
|
||||||
|
bt, err := json.Marshal(cfg)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
binary.Write(buf, binary.BigEndian, uint16(len(bt))) // Config length
|
||||||
|
buf.Write(bt) // Config
|
||||||
|
buf.WriteTo(conn)
|
||||||
|
|
||||||
|
// Response
|
||||||
|
var n uint16
|
||||||
|
err = binary.Read(conn, binary.BigEndian, &n) // Status code
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if n != 200 {
|
||||||
|
t.Errorf("status code should be 200, found %d", n)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSocketHTTP(t *testing.T) {
|
||||||
|
b, addr := startTCPServer()
|
||||||
|
conn, err := connectTCPServer(addr)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer conn.Close()
|
||||||
|
defer b.tcpListener.Close()
|
||||||
|
|
||||||
|
// GET
|
||||||
|
b.Get("/users", func(c *Context) {
|
||||||
|
c.Render(200, FmtJSON, u)
|
||||||
|
})
|
||||||
|
buf := new(bytes.Buffer)
|
||||||
|
buf.WriteByte(byte(CmdHTTP)) // Command
|
||||||
|
buf.WriteString("GET\n") // Method
|
||||||
|
buf.WriteString("/users\n") // Path
|
||||||
|
buf.WriteTo(conn)
|
||||||
|
var n uint16
|
||||||
|
err = binary.Read(conn, binary.BigEndian, &n) // Status code
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if n != 200 {
|
||||||
|
t.Errorf("status code should be 200, found %d", n)
|
||||||
|
}
|
||||||
|
verifyUser(conn, t)
|
||||||
|
|
||||||
|
// POST
|
||||||
|
b.Post("/users", func(c *Context) {
|
||||||
|
c.Bind(c.Socket.config.Format, &user{})
|
||||||
|
c.Render(201, FmtJSON, u)
|
||||||
|
})
|
||||||
|
buf.Reset()
|
||||||
|
buf.WriteByte(byte(CmdHTTP)) // Command
|
||||||
|
buf.WriteString("POST\n") // Method
|
||||||
|
buf.WriteString("/users\n") // Path
|
||||||
|
bt, err := json.Marshal(u)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
binary.Write(buf, binary.BigEndian, int64(len(bt))) // Body length
|
||||||
|
buf.Write(bt) // Body
|
||||||
|
buf.WriteTo(conn)
|
||||||
|
err = binary.Read(conn, binary.BigEndian, &n) // Status code
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if n != 201 {
|
||||||
|
t.Errorf("status code should be 201, found %d", n)
|
||||||
|
}
|
||||||
|
verifyUser(conn, t)
|
||||||
|
}
|
||||||
|
|
||||||
|
func verifyUser(rd io.Reader, t *testing.T) {
|
||||||
|
var l int64
|
||||||
|
err := binary.Read(rd, binary.BigEndian, &l) // Body length
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
bd := io.LimitReader(rd, l) // Body
|
||||||
|
u2 := new(user)
|
||||||
|
dec := json.NewDecoder(bd)
|
||||||
|
err = dec.Decode(u2)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if u2.Id != u.Id {
|
||||||
|
t.Error("user id should be %s, found %s", u.Id, u2.Id)
|
||||||
|
}
|
||||||
|
if u2.Name != u.Name {
|
||||||
|
t.Error("user name should be %s, found %s", u.Name, u2.Name)
|
||||||
|
}
|
||||||
|
}
|
171
client.go
Normal file
171
client.go
Normal file
@ -0,0 +1,171 @@
|
|||||||
|
package bolt
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"encoding/binary"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"log"
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"sync"
|
||||||
|
)
|
||||||
|
|
||||||
|
type (
|
||||||
|
Client struct {
|
||||||
|
host string
|
||||||
|
port string
|
||||||
|
transport transport
|
||||||
|
socket io.ReadWriteCloser
|
||||||
|
reader *bufio.Reader
|
||||||
|
writer *bufio.Writer
|
||||||
|
httpClient HttpClient
|
||||||
|
pool sync.Pool
|
||||||
|
connected bool
|
||||||
|
}
|
||||||
|
HttpClient interface {
|
||||||
|
Do(*http.Request) (*http.Response, error)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
func NewClient(opts ...func(*Client)) (c *Client) {
|
||||||
|
c = &Client{
|
||||||
|
host: "localhost",
|
||||||
|
port: "80",
|
||||||
|
transport: TrnspHTTP,
|
||||||
|
httpClient: &http.Client{},
|
||||||
|
}
|
||||||
|
c.pool.New = func() interface{} {
|
||||||
|
return &Context{
|
||||||
|
Request: &http.Request{
|
||||||
|
URL: new(url.URL),
|
||||||
|
},
|
||||||
|
// Response: new(http.Response),
|
||||||
|
// Socket: &Socket{
|
||||||
|
// Header: SocketHeader{},
|
||||||
|
// },
|
||||||
|
client: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Set options
|
||||||
|
for _, o := range opts {
|
||||||
|
o(c)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func Transport(t transport) func(*Client) {
|
||||||
|
return func(c *Client) {
|
||||||
|
c.transport = t
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Host(h string) func(*Client) {
|
||||||
|
return func(c *Client) {
|
||||||
|
c.host = h
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Port(p string) func(*Client) {
|
||||||
|
return func(c *Client) {
|
||||||
|
c.port = p
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) Open(cfg *Config) (err error) {
|
||||||
|
switch c.transport {
|
||||||
|
case TrnspWS:
|
||||||
|
case TrnspTCP:
|
||||||
|
c.socket, err = net.Dial("tcp", net.JoinHostPort(c.host, c.port))
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("bolt: %v", err)
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return errors.New("bolt: transport not supported")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Request
|
||||||
|
c.writer = bufio.NewWriter(c.socket)
|
||||||
|
c.reader = bufio.NewReader(c.socket)
|
||||||
|
c.writer.WriteByte(byte(CmdINIT)) // Command
|
||||||
|
var cid uint32 = 98
|
||||||
|
if err = binary.Write(c.writer, binary.BigEndian, cid); err != nil { // Correlation ID
|
||||||
|
log.Println(err)
|
||||||
|
}
|
||||||
|
b, err := json.Marshal(cfg)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("bolt: %v", err)
|
||||||
|
}
|
||||||
|
if err = binary.Write(c.writer, binary.BigEndian, uint16(len(b))); err != nil { // Config length
|
||||||
|
}
|
||||||
|
c.writer.Write(b) // Config
|
||||||
|
c.writer.Flush()
|
||||||
|
|
||||||
|
for {
|
||||||
|
var cid uint32
|
||||||
|
binary.Read(c.reader, binary.BigEndian, &cid) // Correlation ID
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("bolt: %v", err)
|
||||||
|
}
|
||||||
|
println(cid)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
// Response
|
||||||
|
var n uint16
|
||||||
|
err = binary.Read(c.reader, binary.BigEndian, &n) // Status code
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("bolt: %v", err)
|
||||||
|
}
|
||||||
|
if n != 200 {
|
||||||
|
return fmt.Errorf("bolt: status=%d", n)
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) Auth() {
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) Connect(path string) {
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) Delete() {
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) Get(path string, hdr Header, hl HandlerFunc) error {
|
||||||
|
return c.Request("GET", path, hdr, hl)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) Request(method, path string, hd Header, hl HandlerFunc) (err error) {
|
||||||
|
ctx := c.pool.Get().(*Context)
|
||||||
|
ctx.Transport = c.transport
|
||||||
|
switch c.transport {
|
||||||
|
case TrnspHTTP:
|
||||||
|
ctx.Request.Method = method
|
||||||
|
// ctx.Request.Header = hd
|
||||||
|
ctx.Request.URL.Scheme = "http"
|
||||||
|
ctx.Request.URL.Host = net.JoinHostPort(c.host, c.port)
|
||||||
|
ctx.Request.URL.Path = path
|
||||||
|
ctx.Response, err = c.httpClient.Do(ctx.Request)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// ctx.Header = ctx.Response.Header
|
||||||
|
hl(ctx)
|
||||||
|
case TrnspWS, TrnspTCP:
|
||||||
|
c.writer.WriteByte(byte(CmdHTTP)) // Command
|
||||||
|
c.writer.WriteString(method + "\n") // Method
|
||||||
|
c.writer.WriteString(path + "\n") // Path
|
||||||
|
if method == "POST" || method == "PUT" || method == "PATCH" {
|
||||||
|
// binary.Write(c.writer, binary.BigEndian, uint16(len(b))) // Header length
|
||||||
|
// wt.Write(b) // Header
|
||||||
|
}
|
||||||
|
c.writer.Flush()
|
||||||
|
hl(ctx)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
102
client_test.go
Normal file
102
client_test.go
Normal file
@ -0,0 +1,102 @@
|
|||||||
|
package bolt
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
"io/ioutil"
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type (
|
||||||
|
httpClient struct{}
|
||||||
|
user struct {
|
||||||
|
Id string
|
||||||
|
Name string
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
func mockTcpServer(method, path string) (b *Bolt) {
|
||||||
|
b = New()
|
||||||
|
b.Handle(method, path, nil)
|
||||||
|
b.RunTCP(":9999")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func mockTcpReq(status uint16, hd Header, body []byte) (c *Client) {
|
||||||
|
c = NewClient()
|
||||||
|
c.transport = TrnspTCP
|
||||||
|
// c.socket = new(bytes.Buffer)
|
||||||
|
c.socket = os.Stdout
|
||||||
|
// l := int64(len(body))
|
||||||
|
// hd.Set("Content-Length", strconv.FormatInt(l, 10))
|
||||||
|
c.socket.Write(body)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *httpClient) Do(req *http.Request) (res *http.Response, err error) {
|
||||||
|
res = &http.Response{}
|
||||||
|
if req.Method == "GET" {
|
||||||
|
res.StatusCode = 200
|
||||||
|
u := &user{"1", "Joe"}
|
||||||
|
d, _ := json.Marshal(u)
|
||||||
|
res.Body = ioutil.NopCloser(bytes.NewBuffer(d))
|
||||||
|
res.Header = make(http.Header)
|
||||||
|
res.Header.Add("Accept", MIME_JSON)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestClientPost(t *testing.T) {
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestClientHttpGet(t *testing.T) {
|
||||||
|
c := NewClient()
|
||||||
|
c.httpClient = &httpClient{}
|
||||||
|
hd := make(Header)
|
||||||
|
// hd.Set("Accept", MIME_JSON)
|
||||||
|
if err := c.Get("/users/1", hd, func(ctx *Context) {
|
||||||
|
u := new(user)
|
||||||
|
ctx.Decode(u)
|
||||||
|
if u.Id != "1" {
|
||||||
|
t.Error()
|
||||||
|
}
|
||||||
|
}); err != nil {
|
||||||
|
t.Error(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTCPClient(t *testing.T) {
|
||||||
|
b, addr := startTCPServer()
|
||||||
|
defer b.tcpListener.Close()
|
||||||
|
|
||||||
|
// Open
|
||||||
|
host, port, err := net.SplitHostPort(addr)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
c := NewClient(Transport(TrnspTCP), Host(host), Port(port))
|
||||||
|
go func() {
|
||||||
|
err = c.Open(&Config{
|
||||||
|
Format: FmtJSON,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
time.Sleep(32 * time.Millisecond)
|
||||||
|
|
||||||
|
// Get
|
||||||
|
// b.Get("/users", func(c *Context) {
|
||||||
|
// c.Render(200, FmtJSON, u)
|
||||||
|
// })
|
||||||
|
// err = c.Get("/users", nil, func(c *Context) {
|
||||||
|
// })
|
||||||
|
// time.Sleep(32 * time.Millisecond)
|
||||||
|
// if err != nil {
|
||||||
|
// t.Fatal(err)
|
||||||
|
// }
|
||||||
|
}
|
134
context.go
Normal file
134
context.go
Normal file
@ -0,0 +1,134 @@
|
|||||||
|
package bolt
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/binary"
|
||||||
|
"encoding/json"
|
||||||
|
"io"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
)
|
||||||
|
|
||||||
|
type (
|
||||||
|
Context struct {
|
||||||
|
Transport transport
|
||||||
|
Request *http.Request
|
||||||
|
Writer *response
|
||||||
|
Response *http.Response
|
||||||
|
Socket *Socket
|
||||||
|
params Params
|
||||||
|
handlers []HandlerFunc
|
||||||
|
store map[string]interface{}
|
||||||
|
l int // Handlers' length
|
||||||
|
i int // Current handler index
|
||||||
|
client bool
|
||||||
|
}
|
||||||
|
store map[string]interface{}
|
||||||
|
)
|
||||||
|
|
||||||
|
func (c *Context) P(i uint8) string {
|
||||||
|
return c.params[i].Value
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Context) Param(n string) string {
|
||||||
|
return c.params.Get(n)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Context) Bind(f format, i interface{}) (err error) {
|
||||||
|
var bd io.ReadCloser
|
||||||
|
switch c.Transport {
|
||||||
|
case TrnspHTTP:
|
||||||
|
bd = c.Request.Body
|
||||||
|
case TrnspWS, TrnspTCP:
|
||||||
|
bd = c.Socket.Body
|
||||||
|
}
|
||||||
|
switch f {
|
||||||
|
case FmtJSON:
|
||||||
|
dec := json.NewDecoder(bd)
|
||||||
|
if err = dec.Decode(i); err != nil {
|
||||||
|
log.Printf("bolt: %s", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Context) Decode(i interface{}) (err error) {
|
||||||
|
var rd io.Reader
|
||||||
|
|
||||||
|
switch c.Transport {
|
||||||
|
case TrnspHTTP:
|
||||||
|
rd = c.Request.Body
|
||||||
|
if c.client {
|
||||||
|
rd = c.Response.Body
|
||||||
|
defer rd.(io.Closer).Close()
|
||||||
|
}
|
||||||
|
case TrnspWS, TrnspTCP:
|
||||||
|
var cl int64
|
||||||
|
cl, err = strconv.ParseInt(c.Request.Header.Get(HdrContentLength), 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
rd = io.LimitReader(c.Socket.Reader, cl)
|
||||||
|
}
|
||||||
|
|
||||||
|
t := c.Request.Header.Get("Content-Type")
|
||||||
|
if c.client {
|
||||||
|
t = c.Request.Header.Get("Accept")
|
||||||
|
}
|
||||||
|
|
||||||
|
switch t {
|
||||||
|
case MIME_MP:
|
||||||
|
default: // JSON
|
||||||
|
dec := json.NewDecoder(rd)
|
||||||
|
if err = dec.Decode(i); err != nil {
|
||||||
|
log.Println(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Streaming?
|
||||||
|
func (c *Context) Encode(f format, i interface{}) (b []byte, err error) {
|
||||||
|
switch f {
|
||||||
|
case FmtJSON:
|
||||||
|
b, err = json.Marshal(i)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: return error
|
||||||
|
func (c *Context) Render(n int, f format, i interface{}) {
|
||||||
|
bd, err := c.Encode(f, i)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("bolt: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
switch c.Transport {
|
||||||
|
default:
|
||||||
|
// c.Writer.Header().Set(HEADER_CONTENT_TYPE, MIME_JSON)
|
||||||
|
// c.Writer.WriteHeader(int(n))
|
||||||
|
// c.Writer.Write(body)
|
||||||
|
case TrnspWS, TrnspTCP:
|
||||||
|
binary.Write(c.Socket.Writer, binary.BigEndian, uint16(n)) // Status code
|
||||||
|
binary.Write(c.Socket.Writer, binary.BigEndian, int64(len(bd))) // Body length
|
||||||
|
c.Socket.Writer.Write(bd) // Body
|
||||||
|
c.Socket.Writer.Flush()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Next executes the next handler in the chain.
|
||||||
|
func (c *Context) Next() {
|
||||||
|
c.i++
|
||||||
|
if c.i < c.l {
|
||||||
|
c.handlers[c.i](c)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Context) Get(key string) interface{} {
|
||||||
|
return c.store[key]
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Context) Set(key string, val interface{}) {
|
||||||
|
c.store[key] = val
|
||||||
|
}
|
24
example/h2o.conf
Normal file
24
example/h2o.conf
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
# to find out the configuration commands, run: h2o --help
|
||||||
|
|
||||||
|
listen: 8080
|
||||||
|
listen:
|
||||||
|
port: 8081
|
||||||
|
ssl:
|
||||||
|
certificate-file: examples/h2o/server.crt
|
||||||
|
key-file: examples/h2o/server.key
|
||||||
|
hosts:
|
||||||
|
"127.0.0.1.xip.io:8080":
|
||||||
|
paths:
|
||||||
|
/:
|
||||||
|
file.dir: examples/doc_root
|
||||||
|
access-log: /dev/stdout
|
||||||
|
"alternate.127.0.0.1.xip.io:8081":
|
||||||
|
listen:
|
||||||
|
port: 8081
|
||||||
|
ssl:
|
||||||
|
certificate-file: examples/h2o/alternate.crt
|
||||||
|
key-file: examples/h2o/alternate.key
|
||||||
|
paths:
|
||||||
|
/:
|
||||||
|
file.dir: examples/doc_root.alternate
|
||||||
|
access-log: /dev/stdout
|
43
example/main.go
Normal file
43
example/main.go
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/labstack/bolt"
|
||||||
|
)
|
||||||
|
|
||||||
|
type user struct {
|
||||||
|
Id string
|
||||||
|
Name string
|
||||||
|
}
|
||||||
|
|
||||||
|
var users map[string]*user
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
users = map[string]*user{
|
||||||
|
"1": &user{
|
||||||
|
Id: "1",
|
||||||
|
Name: "Wreck-It Ralph",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func createUser(c *bolt.Context) {
|
||||||
|
}
|
||||||
|
|
||||||
|
func getUsers(c *bolt.Context) {
|
||||||
|
c.Render(http.StatusOK, bolt.FMT_JSON, users)
|
||||||
|
}
|
||||||
|
|
||||||
|
func getUser(c *bolt.Context) {
|
||||||
|
c.Render(http.StatusOK, bolt.FMT_JSON, users[c.P(0)])
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
b := bolt.New()
|
||||||
|
b.Get("/users", getUsers)
|
||||||
|
b.Get("/users/:id", getUser)
|
||||||
|
// go b.RunHttp(":8080")
|
||||||
|
// go b.RunWebSocket(":8081")
|
||||||
|
b.RunTcp(":8082")
|
||||||
|
}
|
69
middleware/auth.go
Normal file
69
middleware/auth.go
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
package middleware
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/base64"
|
||||||
|
"errors"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/dgrijalva/jwt-go"
|
||||||
|
"github.com/labstack/bolt"
|
||||||
|
)
|
||||||
|
|
||||||
|
type (
|
||||||
|
BasicAuthFunc func(usr, pwd string) bool
|
||||||
|
AuthorizedHandler bolt.HandlerFunc
|
||||||
|
UnauthorizedHandler func(c *bolt.Context, err error)
|
||||||
|
JwtKeyFunc func(kid string) ([]byte, error)
|
||||||
|
Claims map[string]interface{}
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
ErrBasicAuth = errors.New("bolt: basic auth error")
|
||||||
|
ErrJwtAuth = errors.New("bolt: jwt auth error")
|
||||||
|
)
|
||||||
|
|
||||||
|
func BasicAuth(ah AuthorizedHandler, uah UnauthorizedHandler, fn BasicAuthFunc) bolt.HandlerFunc {
|
||||||
|
return func(c *bolt.Context) {
|
||||||
|
auth := strings.Fields(c.Request.Header.Get("Authorization"))
|
||||||
|
if len(auth) == 2 {
|
||||||
|
scheme := auth[0]
|
||||||
|
s, err := base64.StdEncoding.DecodeString(auth[1])
|
||||||
|
if err != nil {
|
||||||
|
uah(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
cred := strings.Split(string(s), ":")
|
||||||
|
if scheme == "Basic" && len(cred) == 2 {
|
||||||
|
if ok := fn(cred[0], cred[1]); ok {
|
||||||
|
ah(c)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
uah(c, ErrBasicAuth)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func JwtAuth(ah AuthorizedHandler, uah UnauthorizedHandler, fn JwtKeyFunc) bolt.HandlerFunc {
|
||||||
|
return func(c *bolt.Context) {
|
||||||
|
auth := strings.Fields(c.Request.Header.Get("Authorization"))
|
||||||
|
if len(auth) == 2 {
|
||||||
|
t, err := jwt.Parse(auth[1], func(token *jwt.Token) (interface{}, error) {
|
||||||
|
if kid := token.Header["kid"]; kid != nil {
|
||||||
|
return fn(kid.(string))
|
||||||
|
}
|
||||||
|
return fn("")
|
||||||
|
})
|
||||||
|
if t.Valid {
|
||||||
|
c.Set("claims", Claims(t.Claims))
|
||||||
|
ah(c)
|
||||||
|
c.Next()
|
||||||
|
} else {
|
||||||
|
// TODO: capture errors
|
||||||
|
uah(c, err)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
uah(c, ErrJwtAuth)
|
||||||
|
}
|
||||||
|
}
|
32
middleware/logger.go
Normal file
32
middleware/logger.go
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
package middleware
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/labstack/bolt"
|
||||||
|
"labstack.com/common/utils"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Logger() bolt.HandlerFunc {
|
||||||
|
return func(c *bolt.Context) {
|
||||||
|
start := time.Now()
|
||||||
|
c.Next()
|
||||||
|
end := time.Now()
|
||||||
|
color := utils.Green
|
||||||
|
m := c.Request.Method
|
||||||
|
p := c.Request.URL.Path
|
||||||
|
s := c.Response.Status()
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case s >= 500:
|
||||||
|
color = utils.Red
|
||||||
|
case s >= 400:
|
||||||
|
color = utils.Yellow
|
||||||
|
case s >= 300:
|
||||||
|
color = utils.Cyan
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("%s %s %s %s", m, p, color(s), end.Sub(start))
|
||||||
|
}
|
||||||
|
}
|
1
middleware/middleware.go
Normal file
1
middleware/middleware.go
Normal file
@ -0,0 +1 @@
|
|||||||
|
package middleware
|
67
response.go
Normal file
67
response.go
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
package bolt
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"errors"
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
type (
|
||||||
|
// ResponseWriter interface {
|
||||||
|
// }
|
||||||
|
|
||||||
|
response struct {
|
||||||
|
http.ResponseWriter
|
||||||
|
status int
|
||||||
|
size int
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
func NewResponse(rw http.ResponseWriter) *response {
|
||||||
|
return &response{
|
||||||
|
ResponseWriter: rw,
|
||||||
|
status: http.StatusOK,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *response) WriteHeader(c int) {
|
||||||
|
r.status = c
|
||||||
|
r.ResponseWriter.WriteHeader(c)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *response) Write(b []byte) (n int, err error) {
|
||||||
|
n, err = r.ResponseWriter.Write(b)
|
||||||
|
r.size += n
|
||||||
|
return n, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *response) CloseNotify() <-chan bool {
|
||||||
|
cn, ok := r.ResponseWriter.(http.CloseNotifier)
|
||||||
|
if !ok {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return cn.CloseNotify()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *response) Flusher() {
|
||||||
|
if f, ok := r.ResponseWriter.(http.Flusher); ok {
|
||||||
|
f.Flush()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *response) Hijack() (net.Conn, *bufio.ReadWriter, error) {
|
||||||
|
h, ok := r.ResponseWriter.(http.Hijacker)
|
||||||
|
if !ok {
|
||||||
|
return nil, nil, errors.New("hijacker interface not supported")
|
||||||
|
}
|
||||||
|
return h.Hijack()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *response) Status() int {
|
||||||
|
return r.status
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *response) Size() int {
|
||||||
|
return r.size
|
||||||
|
}
|
305
router.go
Normal file
305
router.go
Normal file
@ -0,0 +1,305 @@
|
|||||||
|
package bolt
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"sort"
|
||||||
|
"sync"
|
||||||
|
)
|
||||||
|
|
||||||
|
type (
|
||||||
|
router struct {
|
||||||
|
bolt *Bolt
|
||||||
|
pool sync.Pool
|
||||||
|
root *node
|
||||||
|
}
|
||||||
|
node struct {
|
||||||
|
label byte
|
||||||
|
prefix string
|
||||||
|
has ntype // Type of node(s)
|
||||||
|
handlers []HandlerFunc
|
||||||
|
edges edges
|
||||||
|
}
|
||||||
|
edges []*node
|
||||||
|
ntype byte
|
||||||
|
param struct {
|
||||||
|
Name string
|
||||||
|
Value string
|
||||||
|
}
|
||||||
|
Params []param
|
||||||
|
Status uint16
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
snode ntype = iota // Static node
|
||||||
|
pnode // Param node
|
||||||
|
wnode // Wildcard node
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
OK Status = iota
|
||||||
|
NotFound
|
||||||
|
NotAllowed
|
||||||
|
)
|
||||||
|
|
||||||
|
func NewRouter(b *Bolt) (r *router) {
|
||||||
|
r = &router{
|
||||||
|
bolt: b,
|
||||||
|
root: &node{
|
||||||
|
prefix: "",
|
||||||
|
handlers: make([]HandlerFunc, len(MethodMap)),
|
||||||
|
edges: edges{},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
r.pool.New = func() interface{} {
|
||||||
|
return make(Params, 0, b.maxParam)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *node) findEdge(l byte) *node {
|
||||||
|
for _, e := range n.edges {
|
||||||
|
if e.label == l {
|
||||||
|
return e
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e edges) Len() int {
|
||||||
|
return len(e)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e edges) Swap(i, j int) {
|
||||||
|
e[i], e[j] = e[j], e[i]
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e edges) Less(i, j int) bool {
|
||||||
|
return e[i].label < e[j].label
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e edges) Sort() {
|
||||||
|
sort.Sort(e)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *router) Add(method, path string, h HandlerFunc) {
|
||||||
|
i := 0
|
||||||
|
l := len(path)
|
||||||
|
for ; i < l; i++ {
|
||||||
|
if path[i] == ':' {
|
||||||
|
r.insert(method, path[:i], nil, pnode)
|
||||||
|
for ; i < l && path[i] != '/'; i++ {
|
||||||
|
}
|
||||||
|
if i == l {
|
||||||
|
r.insert(method, path[:i], h, snode)
|
||||||
|
return
|
||||||
|
} else {
|
||||||
|
r.insert(method, path[:i], nil, snode)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
r.insert(method, path, h, snode)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *router) insert(method, path string, h HandlerFunc, has ntype) {
|
||||||
|
cn := r.root // Current node
|
||||||
|
search := path
|
||||||
|
|
||||||
|
for {
|
||||||
|
sl := len(search)
|
||||||
|
pl := len(cn.prefix)
|
||||||
|
l := lcp(search, cn.prefix)
|
||||||
|
|
||||||
|
if l == 0 {
|
||||||
|
// At root node
|
||||||
|
cn.label = search[0]
|
||||||
|
cn.prefix = search
|
||||||
|
cn.has = has
|
||||||
|
if h != nil {
|
||||||
|
cn.handlers[MethodMap[method]] = h
|
||||||
|
}
|
||||||
|
return
|
||||||
|
} else if l < pl {
|
||||||
|
// Split the node
|
||||||
|
n := newNode(cn.prefix[l:], cn.has, cn.handlers, cn.edges)
|
||||||
|
cn.edges = edges{n} // Add to parent
|
||||||
|
|
||||||
|
// Reset parent node
|
||||||
|
cn.label = cn.prefix[0]
|
||||||
|
cn.prefix = cn.prefix[:l]
|
||||||
|
cn.has = snode
|
||||||
|
cn.handlers = make([]HandlerFunc, len(MethodMap))
|
||||||
|
|
||||||
|
if l == sl {
|
||||||
|
// At parent node
|
||||||
|
cn.handlers[MethodMap[method]] = h
|
||||||
|
} else {
|
||||||
|
// Need to fork a node
|
||||||
|
n = newNode(search[l:], has, nil, nil)
|
||||||
|
n.handlers[MethodMap[method]] = h
|
||||||
|
cn.edges = append(cn.edges, n)
|
||||||
|
}
|
||||||
|
break
|
||||||
|
} else if l < sl {
|
||||||
|
search = search[l:]
|
||||||
|
e := cn.findEdge(search[0])
|
||||||
|
if e == nil {
|
||||||
|
n := newNode(search, has, nil, nil)
|
||||||
|
if h != nil {
|
||||||
|
n.handlers[MethodMap[method]] = h
|
||||||
|
}
|
||||||
|
cn.edges = append(cn.edges, n)
|
||||||
|
break
|
||||||
|
} else {
|
||||||
|
cn = e
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Node already exists
|
||||||
|
if h != nil {
|
||||||
|
cn.handlers[MethodMap[method]] = h
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func newNode(pfx string, has ntype, h []HandlerFunc, e edges) (n *node) {
|
||||||
|
n = &node{
|
||||||
|
label: pfx[0],
|
||||||
|
prefix: pfx,
|
||||||
|
has: has,
|
||||||
|
handlers: h,
|
||||||
|
edges: e,
|
||||||
|
}
|
||||||
|
if h == nil {
|
||||||
|
n.handlers = make([]HandlerFunc, len(MethodMap))
|
||||||
|
}
|
||||||
|
if e == nil {
|
||||||
|
n.edges = edges{}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *router) Find(method, path string) (handler HandlerFunc, c *Context, s Status) {
|
||||||
|
c = r.bolt.pool.Get().(*Context)
|
||||||
|
cn := r.root // Current node
|
||||||
|
search := path
|
||||||
|
n := 0 // Param count
|
||||||
|
|
||||||
|
for {
|
||||||
|
if search == "" || search == cn.prefix {
|
||||||
|
// Node found
|
||||||
|
h := cn.handlers[MethodMap[method]]
|
||||||
|
if h != nil {
|
||||||
|
// Handler found
|
||||||
|
handler = h
|
||||||
|
} else {
|
||||||
|
s = NotAllowed
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
pl := len(cn.prefix)
|
||||||
|
l := lcp(search, cn.prefix)
|
||||||
|
|
||||||
|
if l == pl {
|
||||||
|
search = search[l:]
|
||||||
|
switch cn.has {
|
||||||
|
case pnode:
|
||||||
|
cn = cn.edges[0]
|
||||||
|
i := 0
|
||||||
|
l = len(search)
|
||||||
|
|
||||||
|
for ; i < l && search[i] != '/'; i++ {
|
||||||
|
}
|
||||||
|
p := c.params[:n+1]
|
||||||
|
p[n].Name = cn.prefix[1:]
|
||||||
|
p[n].Value = search[:i]
|
||||||
|
n++
|
||||||
|
|
||||||
|
search = search[i:]
|
||||||
|
|
||||||
|
if i == l {
|
||||||
|
// All param read
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
e := cn.findEdge(search[0])
|
||||||
|
if e == nil {
|
||||||
|
// Not found
|
||||||
|
s = NotFound
|
||||||
|
return
|
||||||
|
} else {
|
||||||
|
cn = e
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Not found
|
||||||
|
s = NotFound
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Length of longest common prefix
|
||||||
|
func lcp(a, b string) (i int) {
|
||||||
|
max := len(a)
|
||||||
|
l := len(b)
|
||||||
|
if l < max {
|
||||||
|
max = l
|
||||||
|
}
|
||||||
|
for ; i < max && a[i] == b[i]; i++ {
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *router) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
|
||||||
|
h, c, rep := r.Find(req.Method, req.URL.Path)
|
||||||
|
c.Writer.ResponseWriter = rw
|
||||||
|
if h != nil {
|
||||||
|
h(c)
|
||||||
|
} else {
|
||||||
|
if rep == NotFound {
|
||||||
|
r.bolt.notFoundHandler(c)
|
||||||
|
} else if rep == NotAllowed {
|
||||||
|
r.bolt.methodNotAllowedHandler(c)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
r.bolt.pool.Put(c)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ps Params) Get(n string) (v string) {
|
||||||
|
for _, p := range ps {
|
||||||
|
if p.Name == n {
|
||||||
|
v = p.Value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *router) printTree() {
|
||||||
|
r.root.printTree("", true)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *node) printTree(pfx string, tail bool) {
|
||||||
|
p := prefix(tail, pfx, "└── ", "├── ")
|
||||||
|
fmt.Printf("%s%s has=%d, len=%d\n", p, n.prefix, n.has, len(n.handlers))
|
||||||
|
|
||||||
|
nodes := n.edges
|
||||||
|
l := len(nodes)
|
||||||
|
p = prefix(tail, pfx, " ", "│ ")
|
||||||
|
for i := 0; i < l-1; i++ {
|
||||||
|
nodes[i].printTree(p, false)
|
||||||
|
}
|
||||||
|
if l > 0 {
|
||||||
|
nodes[l-1].printTree(p, true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func prefix(tail bool, p, on, off string) string {
|
||||||
|
if tail {
|
||||||
|
return fmt.Sprintf("%s%s", p, on)
|
||||||
|
} else {
|
||||||
|
return fmt.Sprintf("%s%s", p, off)
|
||||||
|
}
|
||||||
|
}
|
46
router_test.go
Normal file
46
router_test.go
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
package bolt
|
||||||
|
|
||||||
|
import "testing"
|
||||||
|
|
||||||
|
func TestStatic(t *testing.T) {
|
||||||
|
r := New().Router
|
||||||
|
r.Add("GET", "/users/joe/books", func(c *Context) {})
|
||||||
|
h, _, _ := r.Find("GET", "/users/joe/books")
|
||||||
|
if h == nil {
|
||||||
|
t.Fatal("handle not found")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParam(t *testing.T) {
|
||||||
|
r := New().Router
|
||||||
|
r.Add("GET", "/users/:name", func(c *Context) {})
|
||||||
|
h, c, _ := r.Find("GET", "/users/joe")
|
||||||
|
if h == nil {
|
||||||
|
t.Fatal("handle not found")
|
||||||
|
}
|
||||||
|
p := c.Param("name")
|
||||||
|
if p != "joe" {
|
||||||
|
t.Errorf("name should be equal to joe, found %s", p)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMicroParam(t *testing.T) {
|
||||||
|
r := New().Router
|
||||||
|
r.Add("GET", "/:a/:b/:c", func(c *Context) {})
|
||||||
|
h, c, _ := r.Find("GET", "/a/b/c")
|
||||||
|
if h == nil {
|
||||||
|
t.Fatal("handle not found")
|
||||||
|
}
|
||||||
|
p1 := c.P(0)
|
||||||
|
if p1 != "a" {
|
||||||
|
t.Errorf("p1 should be equal to a, found %s", p1)
|
||||||
|
}
|
||||||
|
p2 := c.P(1)
|
||||||
|
if p2 != "b" {
|
||||||
|
t.Errorf("p2 should be equal to b, found %s", p2)
|
||||||
|
}
|
||||||
|
p3 := c.P(2)
|
||||||
|
if p3 != "c" {
|
||||||
|
t.Errorf("p3 should be equal to c, found %s", p3)
|
||||||
|
}
|
||||||
|
}
|
101
socket.go
Normal file
101
socket.go
Normal file
@ -0,0 +1,101 @@
|
|||||||
|
package bolt
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"encoding/binary"
|
||||||
|
"encoding/json"
|
||||||
|
"io"
|
||||||
|
"io/ioutil"
|
||||||
|
"log"
|
||||||
|
)
|
||||||
|
|
||||||
|
type (
|
||||||
|
Socket struct {
|
||||||
|
Transport transport
|
||||||
|
Header Header
|
||||||
|
Body io.ReadCloser
|
||||||
|
config Config
|
||||||
|
conn io.ReadWriteCloser
|
||||||
|
Reader *bufio.Reader
|
||||||
|
Writer *bufio.Writer
|
||||||
|
bolt *Bolt
|
||||||
|
initialized bool
|
||||||
|
}
|
||||||
|
Config struct {
|
||||||
|
Format format `json:"format,omitempty"`
|
||||||
|
}
|
||||||
|
Header map[string]string
|
||||||
|
)
|
||||||
|
|
||||||
|
func (s *Socket) Init() {
|
||||||
|
// Request
|
||||||
|
var cid uint32 // Correlation ID
|
||||||
|
err := binary.Read(s.Reader, binary.BigEndian, &cid)
|
||||||
|
if err != nil {
|
||||||
|
log.Println(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var l uint16 // Config length
|
||||||
|
err = binary.Read(s.Reader, binary.BigEndian, &l)
|
||||||
|
if err != nil {
|
||||||
|
log.Println(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
rd := io.LimitReader(s.Reader, int64(l)) // Config
|
||||||
|
dec := json.NewDecoder(rd)
|
||||||
|
if err = dec.Decode(&s.config); err != nil {
|
||||||
|
log.Println(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Response
|
||||||
|
if err = binary.Write(s.Writer, binary.BigEndian, cid); err != nil { // Correlation ID
|
||||||
|
log.Println(err)
|
||||||
|
}
|
||||||
|
if err = binary.Write(s.Writer, binary.BigEndian, uint16(200)); err != nil { // Status code
|
||||||
|
log.Println(err)
|
||||||
|
}
|
||||||
|
s.Writer.Flush()
|
||||||
|
s.initialized = true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Socket) Auth() {
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Socket) HTTP() {
|
||||||
|
// Method
|
||||||
|
m, err := s.Reader.ReadString('\n')
|
||||||
|
if err != nil {
|
||||||
|
log.Println(err)
|
||||||
|
}
|
||||||
|
m = m[:len(m)-1]
|
||||||
|
|
||||||
|
// Path
|
||||||
|
p, err := s.Reader.ReadString('\n')
|
||||||
|
if err != nil {
|
||||||
|
log.Println(err)
|
||||||
|
}
|
||||||
|
p = p[:len(p)-1]
|
||||||
|
|
||||||
|
if m == "POST" || m == "PUT" || m == "PATCH" {
|
||||||
|
var l int64
|
||||||
|
err = binary.Read(s.Reader, binary.BigEndian, &l) // Body length
|
||||||
|
if err != nil {
|
||||||
|
log.Println(err)
|
||||||
|
}
|
||||||
|
s.Body = ioutil.NopCloser(io.LimitReader(s.Reader, l)) // Body
|
||||||
|
}
|
||||||
|
|
||||||
|
h, c, st := s.bolt.Router.Find(m, p)
|
||||||
|
c.Socket = s
|
||||||
|
c.Transport = s.Transport
|
||||||
|
if h != nil {
|
||||||
|
h(c)
|
||||||
|
} else {
|
||||||
|
if st == NotFound {
|
||||||
|
s.bolt.notFoundHandler(c)
|
||||||
|
} else if st == NotAllowed {
|
||||||
|
s.bolt.methodNotAllowedHandler(c)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
s.bolt.pool.Put(c)
|
||||||
|
}
|
20
utils.go
Normal file
20
utils.go
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
package bolt
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"io"
|
||||||
|
)
|
||||||
|
|
||||||
|
type nopCloser struct {
|
||||||
|
*bytes.Buffer
|
||||||
|
}
|
||||||
|
|
||||||
|
func (nopCloser) Close() error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// NopCloser returns a ReadWriteCloser with a no-op Close method wrapping
|
||||||
|
// the provided Buffer.
|
||||||
|
func NopCloser(b *bytes.Buffer) io.ReadWriteCloser {
|
||||||
|
return nopCloser{b}
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user