init project
This commit is contained in:
commit
db3e80dcf7
|
@ -0,0 +1,47 @@
|
||||||
|
bin/
|
||||||
|
|
||||||
|
.svn/
|
||||||
|
.godeps
|
||||||
|
./build
|
||||||
|
.cover/
|
||||||
|
dist
|
||||||
|
_site
|
||||||
|
_posts
|
||||||
|
*.dat
|
||||||
|
*.db
|
||||||
|
.DS_Store
|
||||||
|
.vscode
|
||||||
|
vendor
|
||||||
|
|
||||||
|
|
||||||
|
# Go.gitignore
|
||||||
|
|
||||||
|
# Compiled Object files, Static and Dynamic libs (Shared Objects)
|
||||||
|
*.o
|
||||||
|
*.a
|
||||||
|
*.so
|
||||||
|
|
||||||
|
# Folders
|
||||||
|
_obj
|
||||||
|
_test
|
||||||
|
storage
|
||||||
|
.idea
|
||||||
|
|
||||||
|
# Architecture specific extensions/prefixes
|
||||||
|
*.[568vq]
|
||||||
|
[568vq].out
|
||||||
|
|
||||||
|
*.cgo1.go
|
||||||
|
*.cgo2.c
|
||||||
|
_cgo_defun.c
|
||||||
|
_cgo_gotypes.go
|
||||||
|
_cgo_export.*
|
||||||
|
|
||||||
|
_testmain.go
|
||||||
|
|
||||||
|
*.exe
|
||||||
|
|
||||||
|
profile
|
||||||
|
|
||||||
|
# vim stuff
|
||||||
|
*.sw[op]
|
|
@ -0,0 +1,34 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"embed"
|
||||||
|
"flag"
|
||||||
|
"git.nspix.com/golang/kos"
|
||||||
|
"git.nspix.com/golang/kos/pkg/log"
|
||||||
|
)
|
||||||
|
|
||||||
|
//go:embed web
|
||||||
|
var webDir embed.FS
|
||||||
|
|
||||||
|
type subServer struct {
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *subServer) Start(ctx context.Context) (err error) {
|
||||||
|
kos.Http().Embed("/ui/web", "web", webDir)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *subServer) Stop() (err error) {
|
||||||
|
log.Debugf("stopxxx")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
flag.Parse()
|
||||||
|
svr := kos.Init(
|
||||||
|
kos.WithName("git.nspix.com/golang/test", "0.0.1"),
|
||||||
|
kos.WithServer(&subServer{}),
|
||||||
|
)
|
||||||
|
svr.Run()
|
||||||
|
}
|
|
@ -0,0 +1,3 @@
|
||||||
|
h1{
|
||||||
|
font-size: 20px;
|
||||||
|
}
|
|
@ -0,0 +1,14 @@
|
||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport"
|
||||||
|
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
|
||||||
|
<meta http-equiv="X-UA-Compatible" content="ie=edge">
|
||||||
|
<title>Document</title>
|
||||||
|
<link rel="stylesheet" href="css/index.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>Hello</h1>
|
||||||
|
</body>
|
||||||
|
</html>
|
|
@ -0,0 +1,15 @@
|
||||||
|
package kos
|
||||||
|
|
||||||
|
const (
|
||||||
|
EnvAppName = "VOX_NAME"
|
||||||
|
EnvAppVersion = "VOX_VERSION"
|
||||||
|
EnvAppPort = "VOX_PORT"
|
||||||
|
EnvAppAddress = "VOX_ADDRESS"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
StateHealthy = "Healthy"
|
||||||
|
StateNoAccepting = "NoAccepting"
|
||||||
|
StateNoProgress = "NoProgress"
|
||||||
|
StateUnavailable = "Unavailable"
|
||||||
|
)
|
|
@ -0,0 +1,8 @@
|
||||||
|
package kos
|
||||||
|
|
||||||
|
type Context interface {
|
||||||
|
Bind(v any) (err error)
|
||||||
|
Param(s string) string
|
||||||
|
Success(v any) (err error)
|
||||||
|
Error(code int, reason string) (err error)
|
||||||
|
}
|
|
@ -0,0 +1,257 @@
|
||||||
|
package cli
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"github.com/peterh/liner"
|
||||||
|
"io"
|
||||||
|
"math"
|
||||||
|
"net"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"sync/atomic"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Client struct {
|
||||||
|
name string
|
||||||
|
ctx context.Context
|
||||||
|
address string
|
||||||
|
sequence uint16
|
||||||
|
conn net.Conn
|
||||||
|
liner *liner.State
|
||||||
|
mutex sync.Mutex
|
||||||
|
exitChan chan struct{}
|
||||||
|
readyChan chan struct{}
|
||||||
|
commandChan chan *Frame
|
||||||
|
completerChan chan *Frame
|
||||||
|
Timeout time.Duration
|
||||||
|
exitFlag int32
|
||||||
|
}
|
||||||
|
|
||||||
|
func (client *Client) getSequence() uint16 {
|
||||||
|
client.mutex.Lock()
|
||||||
|
defer client.mutex.Unlock()
|
||||||
|
if client.sequence >= math.MaxUint16 {
|
||||||
|
client.sequence = 0
|
||||||
|
}
|
||||||
|
client.sequence++
|
||||||
|
n := client.sequence
|
||||||
|
return n
|
||||||
|
}
|
||||||
|
|
||||||
|
func (client *Client) dialContext(ctx context.Context, address string) (conn net.Conn, err error) {
|
||||||
|
var (
|
||||||
|
pos int
|
||||||
|
network string
|
||||||
|
dialer net.Dialer
|
||||||
|
)
|
||||||
|
if pos = strings.Index(address, "://"); pos > -1 {
|
||||||
|
network = address[:pos]
|
||||||
|
address = address[pos+3:]
|
||||||
|
} else {
|
||||||
|
network = "tcp"
|
||||||
|
}
|
||||||
|
if conn, err = dialer.DialContext(ctx, network, address); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (client *Client) renderBanner(info *Info) {
|
||||||
|
client.name = info.Name
|
||||||
|
fmt.Printf("Welcome to the %s(%s) monitor\n", info.Name, info.Version)
|
||||||
|
fmt.Printf("Your connection id is %d\n", info.ID)
|
||||||
|
fmt.Printf("Last login: %s from %s\n", info.ServerTime.Format(time.RFC822), info.RemoteAddr)
|
||||||
|
fmt.Printf("Type 'help' for help. Type 'exit' for quit. Type 'cls' to clear input statement.\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (client *Client) ioLoop(r io.Reader) {
|
||||||
|
defer func() {
|
||||||
|
_ = client.Close()
|
||||||
|
}()
|
||||||
|
for {
|
||||||
|
frame, err := readFrame(r)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
switch frame.Type {
|
||||||
|
case PacketTypeHandshake:
|
||||||
|
info := &Info{}
|
||||||
|
if err = json.Unmarshal(frame.Data, info); err == nil {
|
||||||
|
client.renderBanner(info)
|
||||||
|
}
|
||||||
|
select {
|
||||||
|
case client.readyChan <- struct{}{}:
|
||||||
|
case <-client.exitChan:
|
||||||
|
return
|
||||||
|
}
|
||||||
|
case PacketTypeCompleter:
|
||||||
|
select {
|
||||||
|
case client.completerChan <- frame:
|
||||||
|
case <-client.exitChan:
|
||||||
|
return
|
||||||
|
}
|
||||||
|
case PacketTypeCommand:
|
||||||
|
select {
|
||||||
|
case client.commandChan <- frame:
|
||||||
|
case <-client.exitChan:
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (client *Client) waitResponse(seq uint16, timeout time.Duration) {
|
||||||
|
timer := time.NewTimer(timeout)
|
||||||
|
defer timer.Stop()
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-timer.C:
|
||||||
|
fmt.Println("timeout waiting for response")
|
||||||
|
return
|
||||||
|
case <-client.exitChan:
|
||||||
|
return
|
||||||
|
case res, ok := <-client.commandChan:
|
||||||
|
if !ok {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if res.Seq == seq {
|
||||||
|
if res.Error != "" {
|
||||||
|
fmt.Print(res.Error)
|
||||||
|
} else {
|
||||||
|
fmt.Print(string(res.Data))
|
||||||
|
}
|
||||||
|
if res.Flag == FlagComplete {
|
||||||
|
fmt.Println("")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (client *Client) completer(str string) (ss []string) {
|
||||||
|
var (
|
||||||
|
err error
|
||||||
|
seq uint16
|
||||||
|
)
|
||||||
|
ss = make([]string, 0)
|
||||||
|
seq = client.getSequence()
|
||||||
|
if err = writeFrame(client.conn, newFrame(PacketTypeCompleter, FlagComplete, seq, []byte(str))); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
select {
|
||||||
|
case <-time.After(time.Second * 5):
|
||||||
|
case frame, ok := <-client.completerChan:
|
||||||
|
if ok {
|
||||||
|
err = json.Unmarshal(frame.Data, &ss)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (client *Client) Execute(s string) (err error) {
|
||||||
|
var (
|
||||||
|
seq uint16
|
||||||
|
)
|
||||||
|
if client.conn, err = client.dialContext(client.ctx, client.address); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer func() {
|
||||||
|
_ = client.Close()
|
||||||
|
}()
|
||||||
|
go client.ioLoop(client.conn)
|
||||||
|
seq = client.getSequence()
|
||||||
|
if err = writeFrame(client.conn, newFrame(PacketTypeCommand, FlagComplete, seq, []byte(s))); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
client.waitResponse(seq, time.Second*30)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (client *Client) Shell() (err error) {
|
||||||
|
var (
|
||||||
|
seq uint16
|
||||||
|
line string
|
||||||
|
)
|
||||||
|
client.liner.SetCtrlCAborts(true)
|
||||||
|
if client.conn, err = client.dialContext(client.ctx, client.address); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer func() {
|
||||||
|
_ = client.Close()
|
||||||
|
}()
|
||||||
|
if err = writeFrame(client.conn, newFrame(PacketTypeHandshake, FlagComplete, client.getSequence(), nil)); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
go client.ioLoop(client.conn)
|
||||||
|
select {
|
||||||
|
case <-client.readyChan:
|
||||||
|
case <-client.ctx.Done():
|
||||||
|
return
|
||||||
|
}
|
||||||
|
client.liner.SetCompleter(client.completer)
|
||||||
|
for {
|
||||||
|
if line, err = client.liner.Prompt(client.name + "> "); err != nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if atomic.LoadInt32(&client.exitFlag) == 1 {
|
||||||
|
fmt.Println(Bye)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
line = strings.TrimSpace(line)
|
||||||
|
if line == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if strings.ToLower(line) == "exit" || strings.ToLower(line) == "quit" {
|
||||||
|
fmt.Println(Bye)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if strings.ToLower(line) == "clear" || strings.ToLower(line) == "cls" {
|
||||||
|
fmt.Print("\033[2J")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
seq = client.getSequence()
|
||||||
|
if err = writeFrame(client.conn, newFrame(PacketTypeCommand, FlagComplete, seq, []byte(line))); err != nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
client.liner.AppendHistory(line)
|
||||||
|
client.waitResponse(seq, client.Timeout)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (client *Client) Close() (err error) {
|
||||||
|
if !atomic.CompareAndSwapInt32(&client.exitFlag, 0, 1) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
close(client.exitChan)
|
||||||
|
if client.conn != nil {
|
||||||
|
err = client.conn.Close()
|
||||||
|
}
|
||||||
|
if client.liner != nil {
|
||||||
|
err = client.liner.Close()
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewClient(ctx context.Context, addr string) *Client {
|
||||||
|
if ctx == nil {
|
||||||
|
ctx = context.Background()
|
||||||
|
}
|
||||||
|
return &Client{
|
||||||
|
ctx: ctx,
|
||||||
|
address: addr,
|
||||||
|
name: filepath.Base(os.Args[0]),
|
||||||
|
Timeout: time.Second * 30,
|
||||||
|
liner: liner.NewLiner(),
|
||||||
|
readyChan: make(chan struct{}, 1),
|
||||||
|
exitChan: make(chan struct{}),
|
||||||
|
commandChan: make(chan *Frame, 5),
|
||||||
|
completerChan: make(chan *Frame, 5),
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,12 @@
|
||||||
|
package cli
|
||||||
|
|
||||||
|
var (
|
||||||
|
Feature = []byte{67, 76, 73}
|
||||||
|
OK = []byte("OK")
|
||||||
|
Bye = "Bye Bye"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
errNotFound = 4004
|
||||||
|
errExecuteFailed = 4005
|
||||||
|
)
|
|
@ -0,0 +1,110 @@
|
||||||
|
package cli
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"math"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Context struct {
|
||||||
|
Id int64
|
||||||
|
seq uint16
|
||||||
|
wc io.WriteCloser
|
||||||
|
params map[string]string
|
||||||
|
args []string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ctx *Context) reset(id int64, wc io.WriteCloser) {
|
||||||
|
ctx.Id = id
|
||||||
|
ctx.wc = wc
|
||||||
|
ctx.seq = 0
|
||||||
|
ctx.args = make([]string, 0)
|
||||||
|
ctx.params = make(map[string]string)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ctx *Context) setArgs(args []string) {
|
||||||
|
ctx.args = args
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ctx *Context) setParam(ps map[string]string) {
|
||||||
|
ctx.params = ps
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ctx *Context) Bind(v any) (err error) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ctx *Context) Argument(index int) string {
|
||||||
|
if index >= len(ctx.args) || index < 0 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return ctx.args[index]
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ctx *Context) Param(s string) string {
|
||||||
|
if v, ok := ctx.params[s]; ok {
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ctx *Context) Success(v any) (err error) {
|
||||||
|
return ctx.send(responsePayload{Type: PacketTypeCommand, Data: v})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ctx *Context) Error(code int, reason string) (err error) {
|
||||||
|
return ctx.send(responsePayload{Type: PacketTypeCommand, Code: code, Reason: reason})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ctx *Context) Close() (err error) {
|
||||||
|
return ctx.wc.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ctx *Context) send(res responsePayload) (err error) {
|
||||||
|
var (
|
||||||
|
ok bool
|
||||||
|
buf []byte
|
||||||
|
marshal encoder
|
||||||
|
)
|
||||||
|
if res.Code > 0 {
|
||||||
|
err = writeFrame(ctx.wc, &Frame{
|
||||||
|
Feature: Feature,
|
||||||
|
Type: res.Type,
|
||||||
|
Seq: ctx.seq,
|
||||||
|
Flag: FlagComplete,
|
||||||
|
Error: fmt.Sprintf("ERROR(%d): %s", res.Code, res.Reason),
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if res.Data == nil {
|
||||||
|
buf = OK
|
||||||
|
goto __END
|
||||||
|
}
|
||||||
|
if marshal, ok = res.Data.(encoder); ok {
|
||||||
|
buf, err = marshal.Marshal()
|
||||||
|
goto __END
|
||||||
|
}
|
||||||
|
buf, err = serialize(res.Data)
|
||||||
|
__END:
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
offset := 0
|
||||||
|
chunkSize := math.MaxInt16 - 1
|
||||||
|
n := len(buf) / chunkSize
|
||||||
|
for i := 0; i < n; i++ {
|
||||||
|
if err = writeFrame(ctx.wc, newFrame(res.Type, FlagPortion, ctx.seq, buf[offset:chunkSize+offset])); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
offset += chunkSize
|
||||||
|
}
|
||||||
|
err = writeFrame(ctx.wc, newFrame(res.Type, FlagComplete, ctx.seq, buf[offset:]))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func newContext(id int64, wc io.WriteCloser) *Context {
|
||||||
|
return &Context{
|
||||||
|
Id: id,
|
||||||
|
wc: wc,
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,154 @@
|
||||||
|
package cli
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/binary"
|
||||||
|
"io"
|
||||||
|
"math"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
PacketTypeCompleter byte = 0x01
|
||||||
|
PacketTypeCommand = 0x02
|
||||||
|
PacketTypeHandshake = 0x03
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
FlagPortion = 0x00
|
||||||
|
FlagComplete = 0x01
|
||||||
|
)
|
||||||
|
|
||||||
|
type (
|
||||||
|
Frame struct {
|
||||||
|
Feature []byte
|
||||||
|
Type byte `json:"type"`
|
||||||
|
Flag byte `json:"flag"`
|
||||||
|
Seq uint16 `json:"seq"`
|
||||||
|
Data []byte `json:"data"`
|
||||||
|
Error string `json:"error"`
|
||||||
|
Timestamp int64 `json:"timestamp"`
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
func readFrame(r io.Reader) (frame *Frame, err error) {
|
||||||
|
var (
|
||||||
|
n int
|
||||||
|
dataLength uint16
|
||||||
|
errorLength uint16
|
||||||
|
errBuf []byte
|
||||||
|
)
|
||||||
|
frame = &Frame{Feature: make([]byte, 3)}
|
||||||
|
if _, err = io.ReadFull(r, frame.Feature); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if !bytes.Equal(frame.Feature, Feature) {
|
||||||
|
err = io.ErrUnexpectedEOF
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err = binary.Read(r, binary.LittleEndian, &frame.Type); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err = binary.Read(r, binary.LittleEndian, &frame.Flag); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err = binary.Read(r, binary.LittleEndian, &frame.Seq); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err = binary.Read(r, binary.LittleEndian, &frame.Timestamp); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err = binary.Read(r, binary.LittleEndian, &dataLength); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err = binary.Read(r, binary.LittleEndian, &errorLength); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if dataLength > 0 {
|
||||||
|
frame.Data = make([]byte, dataLength)
|
||||||
|
if n, err = io.ReadFull(r, frame.Data); err == nil {
|
||||||
|
if n < int(dataLength) {
|
||||||
|
err = io.ErrShortBuffer
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if errorLength > 0 {
|
||||||
|
errBuf = make([]byte, errorLength)
|
||||||
|
if n, err = io.ReadFull(r, errBuf); err == nil {
|
||||||
|
if n < int(dataLength) {
|
||||||
|
err = io.ErrShortBuffer
|
||||||
|
} else {
|
||||||
|
frame.Error = string(errBuf)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func writeFrame(w io.Writer, frame *Frame) (err error) {
|
||||||
|
var (
|
||||||
|
n int
|
||||||
|
dl int
|
||||||
|
dataLength uint16
|
||||||
|
errorLength uint16
|
||||||
|
errBuf []byte
|
||||||
|
)
|
||||||
|
if _, err = w.Write(Feature); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if frame.Data != nil {
|
||||||
|
dl = len(frame.Data)
|
||||||
|
if dl > math.MaxUint16 {
|
||||||
|
return io.ErrNoProgress
|
||||||
|
}
|
||||||
|
dataLength = uint16(dl)
|
||||||
|
}
|
||||||
|
if frame.Error != "" {
|
||||||
|
errBuf = []byte(frame.Error)
|
||||||
|
errorLength = uint16(len(errBuf))
|
||||||
|
}
|
||||||
|
if err = binary.Write(w, binary.LittleEndian, frame.Type); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err = binary.Write(w, binary.LittleEndian, frame.Flag); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err = binary.Write(w, binary.LittleEndian, frame.Seq); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err = binary.Write(w, binary.LittleEndian, frame.Timestamp); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err = binary.Write(w, binary.LittleEndian, dataLength); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err = binary.Write(w, binary.LittleEndian, errorLength); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if dataLength > 0 {
|
||||||
|
if n, err = w.Write(frame.Data); err == nil {
|
||||||
|
if n < int(dataLength) {
|
||||||
|
err = io.ErrShortWrite
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if errorLength > 0 {
|
||||||
|
if n, err = w.Write(errBuf); err == nil {
|
||||||
|
if n < int(errorLength) {
|
||||||
|
err = io.ErrShortWrite
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func newFrame(t, f byte, seq uint16, data []byte) *Frame {
|
||||||
|
return &Frame{
|
||||||
|
Feature: Feature,
|
||||||
|
Type: t,
|
||||||
|
Flag: f,
|
||||||
|
Seq: seq,
|
||||||
|
Data: data,
|
||||||
|
Timestamp: time.Now().Unix(),
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,177 @@
|
||||||
|
package cli
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"github.com/mattn/go-runewidth"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
ErrNotFound = errors.New("not found")
|
||||||
|
)
|
||||||
|
|
||||||
|
type Router struct {
|
||||||
|
name string
|
||||||
|
path []string
|
||||||
|
children []*Router
|
||||||
|
command Command
|
||||||
|
params []string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Router) getChildren(name string) *Router {
|
||||||
|
for _, child := range r.children {
|
||||||
|
if child.name == name {
|
||||||
|
return child
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Router) Completer(tokens ...string) []string {
|
||||||
|
ss := make([]string, 0, 10)
|
||||||
|
if len(tokens) == 0 {
|
||||||
|
for _, child := range r.children {
|
||||||
|
ss = append(ss, strings.Join(child.path, " "))
|
||||||
|
}
|
||||||
|
return ss
|
||||||
|
}
|
||||||
|
children := r.getChildren(tokens[0])
|
||||||
|
if children == nil {
|
||||||
|
token := tokens[0]
|
||||||
|
for _, child := range r.children {
|
||||||
|
if strings.HasPrefix(child.name, token) {
|
||||||
|
ss = append(ss, strings.Join(child.path, " "))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ss
|
||||||
|
}
|
||||||
|
return children.Completer(tokens[1:]...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Router) Usage() string {
|
||||||
|
if len(r.path) <= 0 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
var (
|
||||||
|
sb strings.Builder
|
||||||
|
)
|
||||||
|
sb.WriteString("Usage: ")
|
||||||
|
sb.WriteString(strings.Join(r.path, " "))
|
||||||
|
if len(r.params) > 0 {
|
||||||
|
for _, s := range r.params {
|
||||||
|
sb.WriteString(" {" + s + "}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return sb.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Router) Handle(path string, command Command) {
|
||||||
|
var (
|
||||||
|
pos int
|
||||||
|
name string
|
||||||
|
)
|
||||||
|
if strings.HasSuffix(path, "/") {
|
||||||
|
path = strings.TrimSuffix(path, "/")
|
||||||
|
}
|
||||||
|
if strings.HasPrefix(path, "/") {
|
||||||
|
path = strings.TrimPrefix(path, "/")
|
||||||
|
}
|
||||||
|
if path == "" {
|
||||||
|
r.command = command
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if path[0] == ':' {
|
||||||
|
ss := strings.Split(path, "/")
|
||||||
|
for _, s := range ss {
|
||||||
|
r.params = append(r.params, strings.TrimPrefix(s, ":"))
|
||||||
|
}
|
||||||
|
r.command = command
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if pos = strings.IndexByte(path, '/'); pos > -1 {
|
||||||
|
name = path[:pos]
|
||||||
|
path = path[pos:]
|
||||||
|
} else {
|
||||||
|
name = path
|
||||||
|
path = ""
|
||||||
|
}
|
||||||
|
children := r.getChildren(name)
|
||||||
|
if children == nil {
|
||||||
|
children = newRouter(name)
|
||||||
|
if len(r.path) == 0 {
|
||||||
|
children.path = append(children.path, name)
|
||||||
|
} else {
|
||||||
|
children.path = append(children.path, r.path...)
|
||||||
|
children.path = append(children.path, name)
|
||||||
|
}
|
||||||
|
r.children = append(r.children, children)
|
||||||
|
}
|
||||||
|
if children.command.Handle != nil {
|
||||||
|
panic("a handle is already registered for path /" + strings.Join(children.path, "/"))
|
||||||
|
}
|
||||||
|
children.Handle(path, command)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Router) Lookup(tokens []string) (router *Router, args []string, err error) {
|
||||||
|
if len(tokens) > 0 {
|
||||||
|
children := r.getChildren(tokens[0])
|
||||||
|
if children != nil {
|
||||||
|
return children.Lookup(tokens[1:])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if r.command.Handle == nil {
|
||||||
|
err = ErrNotFound
|
||||||
|
return
|
||||||
|
}
|
||||||
|
router = r
|
||||||
|
args = tokens
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Router) String() string {
|
||||||
|
var (
|
||||||
|
sb strings.Builder
|
||||||
|
width int
|
||||||
|
maxWidth int
|
||||||
|
walkFunc func(router *Router) []commander
|
||||||
|
)
|
||||||
|
walkFunc = func(router *Router) []commander {
|
||||||
|
vs := make([]commander, 0, 5)
|
||||||
|
if router.command.Handle != nil {
|
||||||
|
vs = append(vs, commander{
|
||||||
|
Name: router.name,
|
||||||
|
Path: strings.Join(router.path, " "),
|
||||||
|
Description: router.command.Description,
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
if len(router.children) > 0 {
|
||||||
|
for _, child := range router.children {
|
||||||
|
vs = append(vs, walkFunc(child)...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return vs
|
||||||
|
}
|
||||||
|
vs := walkFunc(r)
|
||||||
|
for _, v := range vs {
|
||||||
|
width = runewidth.StringWidth(v.Path)
|
||||||
|
if width > maxWidth {
|
||||||
|
maxWidth = width
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, v := range vs {
|
||||||
|
sb.WriteString(fmt.Sprintf("%-"+strconv.Itoa(maxWidth+4)+"s %s\n", v.Path, v.Description))
|
||||||
|
}
|
||||||
|
return sb.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func newRouter(name string) *Router {
|
||||||
|
return &Router{
|
||||||
|
name: name,
|
||||||
|
path: make([]string, 0, 4),
|
||||||
|
params: make([]string, 0, 4),
|
||||||
|
children: make([]*Router, 0, 10),
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,250 @@
|
||||||
|
package cli
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"git.nspix.com/golang/kos/util/pool"
|
||||||
|
"github.com/mattn/go-runewidth"
|
||||||
|
"reflect"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func isNormalKind(kind reflect.Kind) bool {
|
||||||
|
normalKinds := []reflect.Kind{
|
||||||
|
reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, reflect.Int,
|
||||||
|
reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uint,
|
||||||
|
reflect.Float32, reflect.Float64,
|
||||||
|
reflect.String,
|
||||||
|
}
|
||||||
|
for _, k := range normalKinds {
|
||||||
|
if k == kind {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func serializeMap(val map[any]any) ([]byte, error) {
|
||||||
|
var (
|
||||||
|
canFormat bool
|
||||||
|
width int
|
||||||
|
maxWidth int
|
||||||
|
)
|
||||||
|
canFormat = true
|
||||||
|
for k, v := range val {
|
||||||
|
if !isNormalKind(reflect.Indirect(reflect.ValueOf(k)).Kind()) || !isNormalKind(reflect.Indirect(reflect.ValueOf(v)).Kind()) {
|
||||||
|
canFormat = false
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !canFormat {
|
||||||
|
return json.MarshalIndent(val, "", "\t")
|
||||||
|
}
|
||||||
|
ms := make(map[string]string)
|
||||||
|
for k, v := range val {
|
||||||
|
sk := fmt.Sprint(k)
|
||||||
|
ms[sk] = fmt.Sprint(v)
|
||||||
|
width = runewidth.StringWidth(sk)
|
||||||
|
if width > maxWidth {
|
||||||
|
maxWidth = width
|
||||||
|
}
|
||||||
|
}
|
||||||
|
buffer := pool.GetBuffer()
|
||||||
|
defer pool.PutBuffer(buffer)
|
||||||
|
for k, v := range ms {
|
||||||
|
buffer.WriteString(fmt.Sprintf("%-"+strconv.Itoa(maxWidth+4)+"s %s\n", k, v))
|
||||||
|
}
|
||||||
|
return buffer.Bytes(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func printBorder(w *bytes.Buffer, ws []int) {
|
||||||
|
for _, l := range ws {
|
||||||
|
w.WriteString("+")
|
||||||
|
w.WriteString(strings.Repeat("-", l+2))
|
||||||
|
}
|
||||||
|
w.WriteString("+\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
func toString(v any) string {
|
||||||
|
switch t := v.(type) {
|
||||||
|
case float32, float64:
|
||||||
|
return fmt.Sprintf("%.2f", t)
|
||||||
|
case time.Time:
|
||||||
|
return t.Format("2006-01-02 15:04:05")
|
||||||
|
default:
|
||||||
|
return fmt.Sprint(v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func printArray(vals [][]any) (buf []byte) {
|
||||||
|
var (
|
||||||
|
cell string
|
||||||
|
str string
|
||||||
|
widths []int
|
||||||
|
maxLength int
|
||||||
|
width int
|
||||||
|
rows [][]string
|
||||||
|
)
|
||||||
|
rows = make([][]string, 0, len(vals))
|
||||||
|
for _, value := range vals {
|
||||||
|
if len(value) > maxLength {
|
||||||
|
maxLength = len(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
widths = make([]int, maxLength)
|
||||||
|
for _, vs := range vals {
|
||||||
|
rl := len(vs)
|
||||||
|
row := make([]string, rl)
|
||||||
|
for i, val := range vs {
|
||||||
|
str = toString(val)
|
||||||
|
if rl > 1 {
|
||||||
|
width = runewidth.StringWidth(str)
|
||||||
|
if width > widths[i] {
|
||||||
|
widths[i] = width
|
||||||
|
}
|
||||||
|
}
|
||||||
|
row[i] = str
|
||||||
|
}
|
||||||
|
rows = append(rows, row)
|
||||||
|
}
|
||||||
|
buffer := pool.GetBuffer()
|
||||||
|
defer pool.PutBuffer(buffer)
|
||||||
|
printBorder(buffer, widths)
|
||||||
|
for index, row := range rows {
|
||||||
|
size := len(row)
|
||||||
|
for i, w := range widths {
|
||||||
|
cell = ""
|
||||||
|
buffer.WriteString("|")
|
||||||
|
if size > i {
|
||||||
|
cell = row[i]
|
||||||
|
}
|
||||||
|
buffer.WriteString(" ")
|
||||||
|
buffer.WriteString(cell)
|
||||||
|
cl := runewidth.StringWidth(cell)
|
||||||
|
if w > cl {
|
||||||
|
buffer.WriteString(strings.Repeat(" ", w-cl))
|
||||||
|
}
|
||||||
|
buffer.WriteString(" ")
|
||||||
|
}
|
||||||
|
buffer.WriteString("|\n")
|
||||||
|
if index == 0 {
|
||||||
|
printBorder(buffer, widths)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
printBorder(buffer, widths)
|
||||||
|
return buffer.Bytes()
|
||||||
|
}
|
||||||
|
|
||||||
|
func serializeArray(val []any) (buf []byte, err error) {
|
||||||
|
var (
|
||||||
|
ok bool
|
||||||
|
vs [][]any
|
||||||
|
normalFormat bool
|
||||||
|
isArrayElement bool
|
||||||
|
isStructElement bool
|
||||||
|
columnName string
|
||||||
|
)
|
||||||
|
normalFormat = true
|
||||||
|
for _, row := range val {
|
||||||
|
kind := reflect.Indirect(reflect.ValueOf(row)).Kind()
|
||||||
|
if !isNormalKind(kind) {
|
||||||
|
normalFormat = false
|
||||||
|
}
|
||||||
|
if kind == reflect.Array || kind == reflect.Slice {
|
||||||
|
isArrayElement = true
|
||||||
|
}
|
||||||
|
if kind == reflect.Struct {
|
||||||
|
isStructElement = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if normalFormat {
|
||||||
|
goto __END
|
||||||
|
}
|
||||||
|
if isArrayElement {
|
||||||
|
vs = make([][]any, 0, len(val))
|
||||||
|
for _, v := range val {
|
||||||
|
rv := reflect.Indirect(reflect.ValueOf(v))
|
||||||
|
if rv.Kind() == reflect.Array || rv.Kind() == reflect.Slice {
|
||||||
|
row := make([]any, 0, rv.Len())
|
||||||
|
for i := 0; i < rv.Len(); i++ {
|
||||||
|
if isNormalKind(rv.Index(i).Elem().Kind()) || rv.Index(i).Interface() == nil {
|
||||||
|
row = append(row, rv.Index(i).Interface())
|
||||||
|
} else {
|
||||||
|
goto __END
|
||||||
|
}
|
||||||
|
}
|
||||||
|
vs = append(vs, row)
|
||||||
|
} else {
|
||||||
|
goto __END
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if isStructElement {
|
||||||
|
vs = make([][]any, 0, len(val))
|
||||||
|
for i, v := range val {
|
||||||
|
rv := reflect.Indirect(reflect.ValueOf(v))
|
||||||
|
if rv.Kind() == reflect.Struct {
|
||||||
|
if i == 0 {
|
||||||
|
row := make([]any, 0, rv.Type().NumField())
|
||||||
|
for j := 0; j < rv.Type().NumField(); j++ {
|
||||||
|
st := rv.Type().Field(j).Tag
|
||||||
|
if columnName, ok = st.Lookup("name"); !ok {
|
||||||
|
columnName = strings.ToUpper(rv.Type().Field(j).Name)
|
||||||
|
}
|
||||||
|
row = append(row, columnName)
|
||||||
|
}
|
||||||
|
vs = append(vs, row)
|
||||||
|
}
|
||||||
|
row := make([]any, 0, rv.Type().NumField())
|
||||||
|
for j := 0; j < rv.Type().NumField(); j++ {
|
||||||
|
row = append(row, rv.Field(j).Interface())
|
||||||
|
}
|
||||||
|
vs = append(vs, row)
|
||||||
|
} else {
|
||||||
|
goto __END
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
buf = printArray(vs)
|
||||||
|
return
|
||||||
|
__END:
|
||||||
|
return json.MarshalIndent(val, "", "\t")
|
||||||
|
}
|
||||||
|
|
||||||
|
func serialize(val any) (buf []byte, err error) {
|
||||||
|
var (
|
||||||
|
refVal reflect.Value
|
||||||
|
)
|
||||||
|
refVal = reflect.Indirect(reflect.ValueOf(val))
|
||||||
|
switch refVal.Kind() {
|
||||||
|
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
|
||||||
|
buf = []byte(strconv.FormatInt(refVal.Int(), 10))
|
||||||
|
case reflect.Float32, reflect.Float64:
|
||||||
|
buf = []byte(strconv.FormatFloat(refVal.Float(), 'f', -1, 64))
|
||||||
|
case reflect.String:
|
||||||
|
buf = []byte(refVal.String())
|
||||||
|
case reflect.Slice, reflect.Array:
|
||||||
|
if refVal.Type().Elem().Kind() == reflect.Uint8 {
|
||||||
|
buf = refVal.Bytes()
|
||||||
|
} else {
|
||||||
|
as := make([]any, 0, refVal.Len())
|
||||||
|
for i := 0; i < refVal.Len(); i++ {
|
||||||
|
as = append(as, refVal.Index(i).Interface())
|
||||||
|
}
|
||||||
|
buf, err = serializeArray(as)
|
||||||
|
}
|
||||||
|
case reflect.Map:
|
||||||
|
ms := make(map[any]any)
|
||||||
|
keys := refVal.MapKeys()
|
||||||
|
for _, key := range keys {
|
||||||
|
ms[key.Interface()] = refVal.MapIndex(key).Interface()
|
||||||
|
}
|
||||||
|
buf, err = serializeMap(ms)
|
||||||
|
default:
|
||||||
|
buf, err = json.MarshalIndent(refVal.Interface(), "", "\t")
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
|
@ -0,0 +1,201 @@
|
||||||
|
package cli
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"git.nspix.com/golang/kos/util/env"
|
||||||
|
"github.com/sourcegraph/conc"
|
||||||
|
"net"
|
||||||
|
"path"
|
||||||
|
"runtime"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"sync/atomic"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
ctxPool sync.Pool
|
||||||
|
)
|
||||||
|
|
||||||
|
type Server struct {
|
||||||
|
ctx context.Context
|
||||||
|
sequence int64
|
||||||
|
ctxMap sync.Map
|
||||||
|
waitGroup conc.WaitGroup
|
||||||
|
middleware []Middleware
|
||||||
|
router *Router
|
||||||
|
l net.Listener
|
||||||
|
}
|
||||||
|
|
||||||
|
func (svr *Server) applyContext() *Context {
|
||||||
|
if v := ctxPool.Get(); v != nil {
|
||||||
|
if ctx, ok := v.(*Context); ok {
|
||||||
|
return ctx
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return &Context{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (svr *Server) releaseContext(ctx *Context) {
|
||||||
|
ctxPool.Put(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (svr *Server) handle(ctx *Context, frame *Frame) {
|
||||||
|
var (
|
||||||
|
err error
|
||||||
|
params map[string]string
|
||||||
|
tokens []string
|
||||||
|
args []string
|
||||||
|
r *Router
|
||||||
|
)
|
||||||
|
cmd := string(frame.Data)
|
||||||
|
tokens = strings.Fields(cmd)
|
||||||
|
if r, args, err = svr.router.Lookup(tokens); err != nil {
|
||||||
|
if errors.Is(err, ErrNotFound) {
|
||||||
|
err = ctx.Error(errNotFound, fmt.Sprintf("Command %s not found", cmd))
|
||||||
|
} else {
|
||||||
|
err = ctx.Error(errExecuteFailed, err.Error())
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if len(r.params) > len(args) {
|
||||||
|
err = ctx.Error(errExecuteFailed, r.Usage())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if len(r.params) > 0 {
|
||||||
|
params = make(map[string]string)
|
||||||
|
for i, s := range r.params {
|
||||||
|
params[s] = args[i]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ctx.setArgs(args)
|
||||||
|
ctx.setParam(params)
|
||||||
|
err = r.command.Handle(ctx)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (svr *Server) process(conn net.Conn) {
|
||||||
|
var (
|
||||||
|
err error
|
||||||
|
ctx *Context
|
||||||
|
frame *Frame
|
||||||
|
)
|
||||||
|
ctx = svr.applyContext()
|
||||||
|
ctx.reset(atomic.AddInt64(&svr.sequence, 1), conn)
|
||||||
|
svr.ctxMap.Store(ctx.Id, ctx)
|
||||||
|
defer func() {
|
||||||
|
_ = conn.Close()
|
||||||
|
svr.ctxMap.Delete(ctx.Id)
|
||||||
|
svr.releaseContext(ctx)
|
||||||
|
}()
|
||||||
|
for {
|
||||||
|
if frame, err = readFrame(conn); err != nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
//reset frame
|
||||||
|
ctx.seq = frame.Seq
|
||||||
|
switch frame.Type {
|
||||||
|
case PacketTypeHandshake:
|
||||||
|
if err = ctx.send(responsePayload{
|
||||||
|
Type: PacketTypeHandshake,
|
||||||
|
Data: &Info{
|
||||||
|
ID: ctx.Id,
|
||||||
|
Name: env.Get("VOX_NAME", ""),
|
||||||
|
Version: env.Get("VOX_VERSION", ""),
|
||||||
|
OS: runtime.GOOS,
|
||||||
|
ServerTime: time.Now(),
|
||||||
|
RemoteAddr: conn.RemoteAddr().String(),
|
||||||
|
},
|
||||||
|
}); err != nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case PacketTypeCompleter:
|
||||||
|
if err = ctx.send(responsePayload{
|
||||||
|
Type: PacketTypeCompleter,
|
||||||
|
Data: svr.router.Completer(strings.Fields(string(frame.Data))...),
|
||||||
|
}); err != nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case PacketTypeCommand:
|
||||||
|
svr.handle(ctx, frame)
|
||||||
|
default:
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (svr *Server) serve() {
|
||||||
|
for {
|
||||||
|
conn, err := svr.l.Accept()
|
||||||
|
if err != nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
svr.waitGroup.Go(func() {
|
||||||
|
svr.process(conn)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (svr *Server) wrapHandle(pathname, desc string, cb HandleFunc, middleware ...Middleware) Command {
|
||||||
|
h := func(ctx *Context) (err error) {
|
||||||
|
for i := len(svr.middleware) - 1; i >= 0; i-- {
|
||||||
|
cb = svr.middleware[i](cb)
|
||||||
|
}
|
||||||
|
for i := len(middleware) - 1; i >= 0; i-- {
|
||||||
|
cb = middleware[i](cb)
|
||||||
|
}
|
||||||
|
return cb(ctx)
|
||||||
|
}
|
||||||
|
if desc == "" {
|
||||||
|
desc = strings.Join(strings.Split(strings.TrimPrefix(pathname, "/"), "/"), " ")
|
||||||
|
}
|
||||||
|
return Command{
|
||||||
|
Path: pathname,
|
||||||
|
Handle: h,
|
||||||
|
Description: desc,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (svr *Server) Use(middleware ...Middleware) {
|
||||||
|
svr.middleware = append(svr.middleware, middleware...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (svr *Server) Group(prefix string, commands []Command, middleware ...Middleware) {
|
||||||
|
for _, cmd := range commands {
|
||||||
|
svr.Handle(path.Join(prefix, cmd.Path), cmd.Description, cmd.Handle, middleware...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (svr *Server) Handle(pathname string, desc string, cb HandleFunc, middleware ...Middleware) {
|
||||||
|
svr.router.Handle(pathname, svr.wrapHandle(pathname, desc, cb, middleware...))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (svr *Server) Serve(l net.Listener) (err error) {
|
||||||
|
svr.l = l
|
||||||
|
svr.Handle("/help", "Display help information", func(ctx *Context) (err error) {
|
||||||
|
return ctx.Success(svr.router.String())
|
||||||
|
})
|
||||||
|
svr.serve()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (svr *Server) Shutdown() (err error) {
|
||||||
|
err = svr.l.Close()
|
||||||
|
svr.ctxMap.Range(func(key, value any) bool {
|
||||||
|
if ctx, ok := value.(*Context); ok {
|
||||||
|
err = ctx.Close()
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
svr.waitGroup.Wait()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func New(ctx context.Context) *Server {
|
||||||
|
return &Server{
|
||||||
|
ctx: ctx,
|
||||||
|
router: newRouter(""),
|
||||||
|
middleware: make([]Middleware, 0, 10),
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,46 @@
|
||||||
|
package cli
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
|
||||||
|
type Param struct {
|
||||||
|
Key string
|
||||||
|
Value string
|
||||||
|
}
|
||||||
|
|
||||||
|
type Params []Param
|
||||||
|
|
||||||
|
type HandleFunc func(ctx *Context) (err error)
|
||||||
|
|
||||||
|
type Middleware func(next HandleFunc) HandleFunc
|
||||||
|
|
||||||
|
type responsePayload struct {
|
||||||
|
Type uint8 `json:"-"`
|
||||||
|
Code int `json:"code"`
|
||||||
|
Reason string `json:"reason,omitempty"`
|
||||||
|
Data any `json:"data,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Info struct {
|
||||||
|
ID int64 `json:"id"`
|
||||||
|
OS string `json:"os"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Version string `json:"version"`
|
||||||
|
ServerTime time.Time `json:"server_time"`
|
||||||
|
RemoteAddr string `json:"remote_addr"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Command struct {
|
||||||
|
Path string
|
||||||
|
Handle HandleFunc
|
||||||
|
Description string
|
||||||
|
}
|
||||||
|
|
||||||
|
type commander struct {
|
||||||
|
Name string
|
||||||
|
Path string
|
||||||
|
Description string
|
||||||
|
}
|
||||||
|
|
||||||
|
type encoder interface {
|
||||||
|
Marshal() ([]byte, error)
|
||||||
|
}
|
|
@ -0,0 +1,72 @@
|
||||||
|
package entry
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net"
|
||||||
|
"sync/atomic"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Conn struct {
|
||||||
|
buf []byte
|
||||||
|
conn net.Conn
|
||||||
|
exitFlag int32
|
||||||
|
state *State
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Conn) Read(b []byte) (n int, err error) {
|
||||||
|
var m int
|
||||||
|
if len(c.buf) > 0 {
|
||||||
|
if len(b) >= len(c.buf) {
|
||||||
|
m = copy(b[:], c.buf[:])
|
||||||
|
c.buf = c.buf[m:]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
n, err = c.conn.Read(b[m:])
|
||||||
|
n += m
|
||||||
|
atomic.AddInt64(&c.state.Traffic.In, int64(n))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Conn) Write(b []byte) (n int, err error) {
|
||||||
|
n, err = c.conn.Write(b)
|
||||||
|
atomic.AddInt64(&c.state.Traffic.Out, int64(n))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Conn) Close() error {
|
||||||
|
if atomic.CompareAndSwapInt32(&c.exitFlag, 0, 1) {
|
||||||
|
atomic.AddInt32(&c.state.Concurrency, -1)
|
||||||
|
atomic.AddInt64(&c.state.Request.Processed, 1)
|
||||||
|
return c.conn.Close()
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Conn) LocalAddr() net.Addr {
|
||||||
|
return c.conn.LocalAddr()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Conn) RemoteAddr() net.Addr {
|
||||||
|
return c.conn.RemoteAddr()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Conn) SetDeadline(t time.Time) error {
|
||||||
|
return c.conn.SetDeadline(t)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Conn) SetReadDeadline(t time.Time) error {
|
||||||
|
return c.conn.SetReadDeadline(t)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Conn) SetWriteDeadline(t time.Time) error {
|
||||||
|
return c.conn.SetWriteDeadline(t)
|
||||||
|
}
|
||||||
|
|
||||||
|
func wrapConn(c net.Conn, state *State, buf []byte) net.Conn {
|
||||||
|
conn := &Conn{conn: c, buf: buf, state: state}
|
||||||
|
if buf != nil {
|
||||||
|
conn.buf = make([]byte, len(buf))
|
||||||
|
copy(conn.buf, buf)
|
||||||
|
}
|
||||||
|
return conn
|
||||||
|
}
|
|
@ -0,0 +1,191 @@
|
||||||
|
package entry
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"github.com/sourcegraph/conc"
|
||||||
|
"io"
|
||||||
|
"net"
|
||||||
|
"sync/atomic"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
minFeatureLength = 3
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
ErrShortFeature = errors.New("short feature")
|
||||||
|
ErrInvalidListener = errors.New("invalid listener")
|
||||||
|
)
|
||||||
|
|
||||||
|
type (
|
||||||
|
Feature []byte
|
||||||
|
|
||||||
|
listenerEntity struct {
|
||||||
|
feature Feature
|
||||||
|
listener *Listener
|
||||||
|
}
|
||||||
|
|
||||||
|
Gateway struct {
|
||||||
|
ctx context.Context
|
||||||
|
cancelFunc context.CancelCauseFunc
|
||||||
|
l net.Listener
|
||||||
|
ch chan net.Conn
|
||||||
|
address string
|
||||||
|
state *State
|
||||||
|
waitGroup conc.WaitGroup
|
||||||
|
listeners []*listenerEntity
|
||||||
|
exitFlag int32
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
func (gw *Gateway) handle(conn net.Conn) {
|
||||||
|
var (
|
||||||
|
n int
|
||||||
|
err error
|
||||||
|
successed int32
|
||||||
|
feature = make([]byte, minFeatureLength)
|
||||||
|
)
|
||||||
|
atomic.AddInt32(&gw.state.Concurrency, 1)
|
||||||
|
defer func() {
|
||||||
|
if atomic.LoadInt32(&successed) != 1 {
|
||||||
|
atomic.AddInt32(&gw.state.Concurrency, -1)
|
||||||
|
atomic.AddInt64(&gw.state.Request.Discarded, 1)
|
||||||
|
_ = conn.Close()
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
//set deadline
|
||||||
|
if err = conn.SetReadDeadline(time.Now().Add(time.Second * 30)); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
//read feature
|
||||||
|
if n, err = io.ReadFull(conn, feature); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
//reset deadline
|
||||||
|
if err = conn.SetReadDeadline(time.Time{}); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for _, l := range gw.listeners {
|
||||||
|
if bytes.Compare(feature[:n], l.feature[:n]) == 0 {
|
||||||
|
atomic.StoreInt32(&successed, 1)
|
||||||
|
l.listener.Receive(wrapConn(conn, gw.state, feature[:n]))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (gw *Gateway) accept() {
|
||||||
|
atomic.StoreInt32(&gw.state.Accepting, 1)
|
||||||
|
defer func() {
|
||||||
|
atomic.StoreInt32(&gw.state.Accepting, 0)
|
||||||
|
}()
|
||||||
|
for {
|
||||||
|
if conn, err := gw.l.Accept(); err != nil {
|
||||||
|
break
|
||||||
|
} else {
|
||||||
|
select {
|
||||||
|
case gw.ch <- conn:
|
||||||
|
atomic.AddInt64(&gw.state.Request.Total, 1)
|
||||||
|
case <-gw.ctx.Done():
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (gw *Gateway) worker() {
|
||||||
|
atomic.StoreInt32(&gw.state.Processing, 1)
|
||||||
|
defer func() {
|
||||||
|
atomic.StoreInt32(&gw.state.Processing, 0)
|
||||||
|
}()
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-gw.ctx.Done():
|
||||||
|
return
|
||||||
|
case conn, ok := <-gw.ch:
|
||||||
|
if ok {
|
||||||
|
gw.handle(conn)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (gw *Gateway) Bind(feature Feature, listener net.Listener) (err error) {
|
||||||
|
var (
|
||||||
|
ok bool
|
||||||
|
ls *Listener
|
||||||
|
)
|
||||||
|
if len(feature) < minFeatureLength {
|
||||||
|
return ErrShortFeature
|
||||||
|
}
|
||||||
|
if ls, ok = listener.(*Listener); !ok {
|
||||||
|
return ErrInvalidListener
|
||||||
|
}
|
||||||
|
for _, l := range gw.listeners {
|
||||||
|
if bytes.Compare(l.feature, feature) == 0 {
|
||||||
|
l.listener = ls
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
gw.listeners = append(gw.listeners, &listenerEntity{
|
||||||
|
feature: feature,
|
||||||
|
listener: ls,
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (gw *Gateway) Apply(feature ...Feature) (listener net.Listener, err error) {
|
||||||
|
listener = newListener(gw.l.Addr())
|
||||||
|
for _, code := range feature {
|
||||||
|
if len(code) < minFeatureLength {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
err = gw.Bind(code, listener)
|
||||||
|
}
|
||||||
|
return listener, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (gw *Gateway) Release(feature Feature) {
|
||||||
|
for i, l := range gw.listeners {
|
||||||
|
if bytes.Compare(l.feature, feature) == 0 {
|
||||||
|
gw.listeners = append(gw.listeners[:i], gw.listeners[i+1:]...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (gw *Gateway) State() *State {
|
||||||
|
return gw.state
|
||||||
|
}
|
||||||
|
|
||||||
|
func (gw *Gateway) Start(ctx context.Context) (err error) {
|
||||||
|
gw.ctx, gw.cancelFunc = context.WithCancelCause(ctx)
|
||||||
|
if gw.l, err = net.Listen("tcp", gw.address); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
gw.waitGroup.Go(gw.worker)
|
||||||
|
gw.waitGroup.Go(gw.accept)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (gw *Gateway) Stop() (err error) {
|
||||||
|
if !atomic.CompareAndSwapInt32(&gw.exitFlag, 0, 1) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
gw.cancelFunc(io.ErrClosedPipe)
|
||||||
|
err = gw.l.Close()
|
||||||
|
gw.waitGroup.Wait()
|
||||||
|
close(gw.ch)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func New(address string) *Gateway {
|
||||||
|
gw := &Gateway{
|
||||||
|
address: address,
|
||||||
|
state: &State{},
|
||||||
|
ch: make(chan net.Conn, 10),
|
||||||
|
}
|
||||||
|
return gw
|
||||||
|
}
|
|
@ -0,0 +1,271 @@
|
||||||
|
package http
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"encoding/xml"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"reflect"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
type (
|
||||||
|
// Binder is the interface that wraps the Bind method.
|
||||||
|
Binder interface {
|
||||||
|
Bind(i interface{}, req *http.Request) error
|
||||||
|
}
|
||||||
|
|
||||||
|
// DefaultBinder is the default implementation of the Binder interface.
|
||||||
|
DefaultBinder struct{}
|
||||||
|
|
||||||
|
// BindUnmarshaler is the interface used to wrap the UnmarshalParam method.
|
||||||
|
BindUnmarshaler interface {
|
||||||
|
// UnmarshalParam decodes and assigns a value from an form or query param.
|
||||||
|
UnmarshalParam(param string) error
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
// Bind implements the `Binder#Bind` function.
|
||||||
|
func (b *DefaultBinder) Bind(i any, req *http.Request) (err error) {
|
||||||
|
if req.ContentLength == 0 {
|
||||||
|
if req.Method == http.MethodGet || req.Method == http.MethodDelete {
|
||||||
|
if err = b.bindData(i, req.URL.Query(), "query"); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
return errors.New("request body can't be empty")
|
||||||
|
}
|
||||||
|
ctype := req.Header.Get("Content-Type")
|
||||||
|
switch {
|
||||||
|
case strings.HasPrefix(ctype, "application/json"):
|
||||||
|
if err = json.NewDecoder(req.Body).Decode(i); err != nil {
|
||||||
|
if ute, ok := err.(*json.UnmarshalTypeError); ok {
|
||||||
|
return fmt.Errorf("unmarshal type error: expected=%v, got=%v, field=%v, offset=%v", ute.Type, ute.Value, ute.Field, ute.Offset)
|
||||||
|
} else if se, ok := err.(*json.SyntaxError); ok {
|
||||||
|
return fmt.Errorf("syntax error: offset=%v, error=%v", se.Offset, se.Error())
|
||||||
|
} else {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case strings.HasPrefix(ctype, "application/xml"), strings.HasPrefix(ctype, "text/xml"):
|
||||||
|
if err = xml.NewDecoder(req.Body).Decode(i); err != nil {
|
||||||
|
if ute, ok := err.(*xml.UnsupportedTypeError); ok {
|
||||||
|
return fmt.Errorf("unsupported type error: type=%v, error=%v", ute.Type, ute.Error())
|
||||||
|
} else if se, ok := err.(*xml.SyntaxError); ok {
|
||||||
|
return fmt.Errorf("syntax error: line=%v, error=%v", se.Line, se.Error())
|
||||||
|
} else {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case strings.HasPrefix(ctype, "application/x-www-form-urlencoded"), strings.HasPrefix(ctype, "multipart/form-data"):
|
||||||
|
if strings.HasPrefix(req.Header.Get("Content-Type"), "multipart/form-data") {
|
||||||
|
if err := req.ParseMultipartForm(32 << 20); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if err := req.ParseForm(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err = b.bindData(i, req.Form, "form"); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return errors.New("unsupported formatter")
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *DefaultBinder) bindData(ptr interface{}, data map[string][]string, tag string) error {
|
||||||
|
typ := reflect.TypeOf(ptr).Elem()
|
||||||
|
val := reflect.ValueOf(ptr).Elem()
|
||||||
|
if typ.Kind() != reflect.Struct {
|
||||||
|
return errors.New("binding element must be a struct")
|
||||||
|
}
|
||||||
|
for i := 0; i < typ.NumField(); i++ {
|
||||||
|
typeField := typ.Field(i)
|
||||||
|
structField := val.Field(i)
|
||||||
|
if !structField.CanSet() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
structFieldKind := structField.Kind()
|
||||||
|
inputFieldName := typeField.Tag.Get(tag)
|
||||||
|
if inputFieldName == "" {
|
||||||
|
inputFieldName = typeField.Name
|
||||||
|
// If tag is nil, we inspect if the field is a struct.
|
||||||
|
if _, ok := bindUnmarshaler(structField); !ok && structFieldKind == reflect.Struct {
|
||||||
|
if err := b.bindData(structField.Addr().Interface(), data, tag); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
inputValue, exists := data[inputFieldName]
|
||||||
|
if !exists {
|
||||||
|
// Go json.Unmarshal supports case insensitive binding. However the
|
||||||
|
// url params are bound case sensitive which is inconsistent. To
|
||||||
|
// fix this we must check all of the map values in a
|
||||||
|
// case-insensitive search.
|
||||||
|
inputFieldName = strings.ToLower(inputFieldName)
|
||||||
|
for k, v := range data {
|
||||||
|
if strings.ToLower(k) == inputFieldName {
|
||||||
|
inputValue = v
|
||||||
|
exists = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !exists {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
// Call this first, in case we're dealing with an alias to an array type
|
||||||
|
if ok, err := unmarshalField(typeField.Type.Kind(), inputValue[0], structField); ok {
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
numElems := len(inputValue)
|
||||||
|
if structFieldKind == reflect.Slice && numElems > 0 {
|
||||||
|
sliceOf := structField.Type().Elem().Kind()
|
||||||
|
slice := reflect.MakeSlice(structField.Type(), numElems, numElems)
|
||||||
|
for j := 0; j < numElems; j++ {
|
||||||
|
if err := setWithProperType(sliceOf, inputValue[j], slice.Index(j)); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val.Field(i).Set(slice)
|
||||||
|
} else if err := setWithProperType(typeField.Type.Kind(), inputValue[0], structField); err != nil {
|
||||||
|
return err
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func setWithProperType(valueKind reflect.Kind, val string, structField reflect.Value) error {
|
||||||
|
// But also call it here, in case we're dealing with an array of BindUnmarshalers
|
||||||
|
if ok, err := unmarshalField(valueKind, val, structField); ok {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
switch valueKind {
|
||||||
|
case reflect.Ptr:
|
||||||
|
return setWithProperType(structField.Elem().Kind(), val, structField.Elem())
|
||||||
|
case reflect.Int:
|
||||||
|
return setIntField(val, 0, structField)
|
||||||
|
case reflect.Int8:
|
||||||
|
return setIntField(val, 8, structField)
|
||||||
|
case reflect.Int16:
|
||||||
|
return setIntField(val, 16, structField)
|
||||||
|
case reflect.Int32:
|
||||||
|
return setIntField(val, 32, structField)
|
||||||
|
case reflect.Int64:
|
||||||
|
return setIntField(val, 64, structField)
|
||||||
|
case reflect.Uint:
|
||||||
|
return setUintField(val, 0, structField)
|
||||||
|
case reflect.Uint8:
|
||||||
|
return setUintField(val, 8, structField)
|
||||||
|
case reflect.Uint16:
|
||||||
|
return setUintField(val, 16, structField)
|
||||||
|
case reflect.Uint32:
|
||||||
|
return setUintField(val, 32, structField)
|
||||||
|
case reflect.Uint64:
|
||||||
|
return setUintField(val, 64, structField)
|
||||||
|
case reflect.Bool:
|
||||||
|
return setBoolField(val, structField)
|
||||||
|
case reflect.Float32:
|
||||||
|
return setFloatField(val, 32, structField)
|
||||||
|
case reflect.Float64:
|
||||||
|
return setFloatField(val, 64, structField)
|
||||||
|
case reflect.String:
|
||||||
|
structField.SetString(val)
|
||||||
|
default:
|
||||||
|
return errors.New("unknown type")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func unmarshalField(valueKind reflect.Kind, val string, field reflect.Value) (bool, error) {
|
||||||
|
switch valueKind {
|
||||||
|
case reflect.Ptr:
|
||||||
|
return unmarshalFieldPtr(val, field)
|
||||||
|
default:
|
||||||
|
return unmarshalFieldNonPtr(val, field)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// bindUnmarshaler attempts to unmarshal a reflect.Value into a BindUnmarshaler
|
||||||
|
func bindUnmarshaler(field reflect.Value) (BindUnmarshaler, bool) {
|
||||||
|
ptr := reflect.New(field.Type())
|
||||||
|
if ptr.CanInterface() {
|
||||||
|
iface := ptr.Interface()
|
||||||
|
if unmarshaler, ok := iface.(BindUnmarshaler); ok {
|
||||||
|
return unmarshaler, ok
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
|
||||||
|
func unmarshalFieldNonPtr(value string, field reflect.Value) (bool, error) {
|
||||||
|
if unmarshaler, ok := bindUnmarshaler(field); ok {
|
||||||
|
err := unmarshaler.UnmarshalParam(value)
|
||||||
|
field.Set(reflect.ValueOf(unmarshaler).Elem())
|
||||||
|
return true, err
|
||||||
|
}
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func unmarshalFieldPtr(value string, field reflect.Value) (bool, error) {
|
||||||
|
if field.IsNil() {
|
||||||
|
// Initialize the pointer to a nil value
|
||||||
|
field.Set(reflect.New(field.Type().Elem()))
|
||||||
|
}
|
||||||
|
return unmarshalFieldNonPtr(value, field.Elem())
|
||||||
|
}
|
||||||
|
|
||||||
|
func setIntField(value string, bitSize int, field reflect.Value) error {
|
||||||
|
if value == "" {
|
||||||
|
value = "0"
|
||||||
|
}
|
||||||
|
intVal, err := strconv.ParseInt(value, 10, bitSize)
|
||||||
|
if err == nil {
|
||||||
|
field.SetInt(intVal)
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func setUintField(value string, bitSize int, field reflect.Value) error {
|
||||||
|
if value == "" {
|
||||||
|
value = "0"
|
||||||
|
}
|
||||||
|
uintVal, err := strconv.ParseUint(value, 10, bitSize)
|
||||||
|
if err == nil {
|
||||||
|
field.SetUint(uintVal)
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func setBoolField(value string, field reflect.Value) error {
|
||||||
|
if value == "" {
|
||||||
|
value = "false"
|
||||||
|
}
|
||||||
|
boolVal, err := strconv.ParseBool(value)
|
||||||
|
if err == nil {
|
||||||
|
field.SetBool(boolVal)
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func setFloatField(value string, bitSize int, field reflect.Value) error {
|
||||||
|
if value == "" {
|
||||||
|
value = "0.0"
|
||||||
|
}
|
||||||
|
floatVal, err := strconv.ParseFloat(value, bitSize)
|
||||||
|
if err == nil {
|
||||||
|
field.SetFloat(floatVal)
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
|
@ -0,0 +1,107 @@
|
||||||
|
package http
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"path"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
defaultBinder = &DefaultBinder{}
|
||||||
|
)
|
||||||
|
|
||||||
|
type Context struct {
|
||||||
|
ctx context.Context
|
||||||
|
req *http.Request
|
||||||
|
res http.ResponseWriter
|
||||||
|
params map[string]string
|
||||||
|
statusCode int
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ctx *Context) reset(req *http.Request, res http.ResponseWriter, ps map[string]string) {
|
||||||
|
ctx.statusCode = http.StatusOK
|
||||||
|
ctx.req, ctx.res, ctx.params = req, res, ps
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ctx *Context) Request() *http.Request {
|
||||||
|
return ctx.req
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ctx *Context) Response() http.ResponseWriter {
|
||||||
|
return ctx.res
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ctx *Context) Context() context.Context {
|
||||||
|
if ctx.Request().Context() != nil {
|
||||||
|
return ctx.Request().Context()
|
||||||
|
}
|
||||||
|
return ctx.ctx
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ctx *Context) Bind(v any) (err error) {
|
||||||
|
return defaultBinder.Bind(v, ctx.Request())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ctx *Context) Query(k string) string {
|
||||||
|
return ctx.Request().FormValue(k)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ctx *Context) Param(k string) string {
|
||||||
|
var (
|
||||||
|
ok bool
|
||||||
|
v string
|
||||||
|
)
|
||||||
|
if v, ok = ctx.params[k]; ok {
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
return ctx.Request().FormValue(k)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ctx *Context) send(res responsePayload) (err error) {
|
||||||
|
ctx.Response().Header().Set("Content-Type", "application/json")
|
||||||
|
encoder := json.NewEncoder(ctx.Response())
|
||||||
|
if strings.HasPrefix(ctx.Request().Header.Get("User-Agent"), "curl") {
|
||||||
|
encoder.SetIndent("", "\t")
|
||||||
|
}
|
||||||
|
return encoder.Encode(res)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ctx *Context) Success(v any) (err error) {
|
||||||
|
return ctx.send(responsePayload{Data: v})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ctx *Context) Status(code int) {
|
||||||
|
ctx.statusCode = code
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ctx *Context) Error(code int, reason string) (err error) {
|
||||||
|
return ctx.send(responsePayload{Code: code, Reason: reason})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ctx *Context) Redirect(url string, code int) {
|
||||||
|
if code != http.StatusFound && code != http.StatusMovedPermanently {
|
||||||
|
code = http.StatusMovedPermanently
|
||||||
|
}
|
||||||
|
http.Redirect(ctx.Response(), ctx.Request(), url, code)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ctx *Context) SetCookie(cookie *http.Cookie) {
|
||||||
|
http.SetCookie(ctx.Response(), cookie)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ctx *Context) SendFile(filename string) (err error) {
|
||||||
|
var (
|
||||||
|
fi os.FileInfo
|
||||||
|
fp *os.File
|
||||||
|
)
|
||||||
|
if fi, err = os.Stat(filename); err == nil {
|
||||||
|
if fp, err = os.Open(filename); err == nil {
|
||||||
|
http.ServeContent(ctx.Response(), ctx.Request(), path.Base(filename), fi.ModTime(), fp)
|
||||||
|
err = fp.Close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
|
@ -0,0 +1,30 @@
|
||||||
|
package http
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
type (
|
||||||
|
NotFound struct {
|
||||||
|
}
|
||||||
|
NotAllowed struct {
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
func (n NotFound) ServeHTTP(writer http.ResponseWriter, request *http.Request) {
|
||||||
|
writer.WriteHeader(http.StatusNotFound)
|
||||||
|
json.NewEncoder(writer).Encode(responsePayload{
|
||||||
|
Code: http.StatusNotFound,
|
||||||
|
Reason: fmt.Sprintf("requested URL %s was not found on this server", request.URL.Path),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n NotAllowed) ServeHTTP(writer http.ResponseWriter, request *http.Request) {
|
||||||
|
writer.WriteHeader(http.StatusMethodNotAllowed)
|
||||||
|
json.NewEncoder(writer).Encode(responsePayload{
|
||||||
|
Code: http.StatusMethodNotAllowed,
|
||||||
|
Reason: fmt.Sprintf("%s URL %s was not allow on this server", request.Method, request.URL.Path),
|
||||||
|
})
|
||||||
|
}
|
|
@ -0,0 +1,15 @@
|
||||||
|
package http
|
||||||
|
|
||||||
|
import "net/http"
|
||||||
|
|
||||||
|
const (
|
||||||
|
MethodGet = http.MethodGet
|
||||||
|
MethodHead = http.MethodHead
|
||||||
|
MethodPost = http.MethodPost
|
||||||
|
MethodPut = http.MethodPut
|
||||||
|
MethodPatch = http.MethodPatch // RFC 5789
|
||||||
|
MethodDelete = http.MethodDelete
|
||||||
|
MethodConnect = http.MethodConnect
|
||||||
|
MethodOptions = http.MethodOptions
|
||||||
|
MethodTrace = http.MethodTrace
|
||||||
|
)
|
|
@ -0,0 +1,145 @@
|
||||||
|
package router
|
||||||
|
|
||||||
|
// 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 {
|
||||||
|
const stackBufSize = 128
|
||||||
|
|
||||||
|
// Turn empty string into "/"
|
||||||
|
if p == "" {
|
||||||
|
return "/"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reasonably sized buffer on stack to avoid allocations in the common case.
|
||||||
|
// If a larger buffer is required, it gets allocated dynamically.
|
||||||
|
buf := make([]byte, 0, stackBufSize)
|
||||||
|
|
||||||
|
n := len(p)
|
||||||
|
|
||||||
|
// 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
|
||||||
|
|
||||||
|
if n+1 > stackBufSize {
|
||||||
|
buf = make([]byte, n+1)
|
||||||
|
} else {
|
||||||
|
buf = buf[:n+1]
|
||||||
|
}
|
||||||
|
buf[0] = '/'
|
||||||
|
}
|
||||||
|
|
||||||
|
trailing := n > 1 && p[n-1] == '/'
|
||||||
|
|
||||||
|
// A bit more clunky without a 'lazybuf' like the path package, but the loop
|
||||||
|
// gets completely inlined (bufApp calls).
|
||||||
|
// So in contrast to the path package this loop has no expensive function
|
||||||
|
// calls (except make, if needed).
|
||||||
|
|
||||||
|
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 += 2
|
||||||
|
|
||||||
|
case p[r] == '.' && p[r+1] == '.' && (r+2 == n || p[r+2] == '/'):
|
||||||
|
// .. element: remove to last /
|
||||||
|
r += 3
|
||||||
|
|
||||||
|
if w > 1 {
|
||||||
|
// can backtrack
|
||||||
|
w--
|
||||||
|
|
||||||
|
if len(buf) == 0 {
|
||||||
|
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 the original string was not modified (or only shortened at the end),
|
||||||
|
// return the respective substring of the original string.
|
||||||
|
// Otherwise return a new string from the buffer.
|
||||||
|
if len(buf) == 0 {
|
||||||
|
return p[:w]
|
||||||
|
}
|
||||||
|
return string(buf[:w])
|
||||||
|
}
|
||||||
|
|
||||||
|
// Internal helper to lazily create a buffer if necessary.
|
||||||
|
// Calls to this function get inlined.
|
||||||
|
func bufApp(buf *[]byte, s string, w int, c byte) {
|
||||||
|
b := *buf
|
||||||
|
if len(b) == 0 {
|
||||||
|
// No modification of the original string so far.
|
||||||
|
// If the next character is the same as in the original string, we do
|
||||||
|
// not yet have to allocate a buffer.
|
||||||
|
if s[w] == c {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Otherwise use either the stack buffer, if it is large enough, or
|
||||||
|
// allocate a new buffer on the heap, and copy all previous characters.
|
||||||
|
if l := len(s); l > cap(b) {
|
||||||
|
*buf = make([]byte, len(s))
|
||||||
|
} else {
|
||||||
|
*buf = (*buf)[:l]
|
||||||
|
}
|
||||||
|
b = *buf
|
||||||
|
|
||||||
|
copy(b, s[:w])
|
||||||
|
}
|
||||||
|
b[w] = c
|
||||||
|
}
|
|
@ -0,0 +1,489 @@
|
||||||
|
package router
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
)
|
||||||
|
|
||||||
|
// 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 (path 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 _, p := range ps {
|
||||||
|
if p.Key == name {
|
||||||
|
return p.Value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
type paramsKey struct{}
|
||||||
|
|
||||||
|
// ParamsKey is the request context key under which URL params are stored.
|
||||||
|
var ParamsKey = paramsKey{}
|
||||||
|
|
||||||
|
// ParamsFromContext pulls the URL parameters from a request context,
|
||||||
|
// or returns nil if none are present.
|
||||||
|
func ParamsFromContext(ctx context.Context) Params {
|
||||||
|
p, _ := ctx.Value(ParamsKey).(Params)
|
||||||
|
return p
|
||||||
|
}
|
||||||
|
|
||||||
|
// MatchedRoutePathParam is the Param name under which the path of the matched
|
||||||
|
// route is stored, if Router.SaveMatchedRoutePath is set.
|
||||||
|
var MatchedRoutePathParam = "$matchedRoutePath"
|
||||||
|
|
||||||
|
// MatchedRoutePath retrieves the path of the matched route.
|
||||||
|
// Router.SaveMatchedRoutePath must have been enabled when the respective
|
||||||
|
// handler was added, otherwise this function always returns an empty string.
|
||||||
|
func (ps Params) MatchedRoutePath() string {
|
||||||
|
return ps.ByName(MatchedRoutePathParam)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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
|
||||||
|
|
||||||
|
paramsPool sync.Pool
|
||||||
|
maxParams uint16
|
||||||
|
|
||||||
|
// If enabled, adds the matched route path onto the http.Request context
|
||||||
|
// before invoking the handler.
|
||||||
|
// The matched route path is only added to handlers of routes that were
|
||||||
|
// registered when this option was enabled.
|
||||||
|
SaveMatchedRoutePath bool
|
||||||
|
|
||||||
|
// 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 308 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 308 for
|
||||||
|
// all other request methods.
|
||||||
|
// For example /FOO and /..//Foo could be redirected to /foo.
|
||||||
|
// RedirectTrailingSlash is independent of this option.
|
||||||
|
RedirectFixedPath bool
|
||||||
|
|
||||||
|
// If enabled, the router checks if another method is allowed for the
|
||||||
|
// current route, if the current request can not be routed.
|
||||||
|
// If this is the case, the request is answered with 'Method Not Allowed'
|
||||||
|
// and HTTP status code 405.
|
||||||
|
// If no other Method is allowed, the request is delegated to the NotFound
|
||||||
|
// handler.
|
||||||
|
HandleMethodNotAllowed bool
|
||||||
|
|
||||||
|
// If enabled, the router automatically replies to OPTIONS requests.
|
||||||
|
// Custom OPTIONS handlers take priority over automatic replies.
|
||||||
|
HandleOPTIONS bool
|
||||||
|
|
||||||
|
// An optional http.Handler that is called on automatic OPTIONS requests.
|
||||||
|
// The handler is only called if HandleOPTIONS is true and no OPTIONS
|
||||||
|
// handler for the specific path was set.
|
||||||
|
// The "Allowed" header is set before calling the handler.
|
||||||
|
GlobalOPTIONS http.Handler
|
||||||
|
|
||||||
|
// Cached value of global (*) allowed methods
|
||||||
|
globalAllowed string
|
||||||
|
|
||||||
|
// Configurable http.Handler which is called when no matching route is
|
||||||
|
// found. If it is not set, http.NotFound is used.
|
||||||
|
NotFound http.Handler
|
||||||
|
|
||||||
|
// Configurable http.Handler which is called when a request
|
||||||
|
// cannot be routed and HandleMethodNotAllowed is true.
|
||||||
|
// If it is not set, http.Error with http.StatusMethodNotAllowed is used.
|
||||||
|
// The "Allow" header with allowed request methods is set before the handler
|
||||||
|
// is called.
|
||||||
|
MethodNotAllowed http.Handler
|
||||||
|
|
||||||
|
// 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,
|
||||||
|
HandleMethodNotAllowed: true,
|
||||||
|
HandleOPTIONS: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Router) getParams() *Params {
|
||||||
|
ps, _ := r.paramsPool.Get().(*Params)
|
||||||
|
*ps = (*ps)[0:0] // reset slice
|
||||||
|
return ps
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Router) putParams(ps *Params) {
|
||||||
|
if ps != nil {
|
||||||
|
r.paramsPool.Put(ps)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Router) saveMatchedRoutePath(path string, handle Handle) Handle {
|
||||||
|
return func(w http.ResponseWriter, req *http.Request, ps Params) {
|
||||||
|
if ps == nil {
|
||||||
|
psp := r.getParams()
|
||||||
|
ps = (*psp)[0:1]
|
||||||
|
ps[0] = Param{Key: MatchedRoutePathParam, Value: path}
|
||||||
|
handle(w, req, ps)
|
||||||
|
r.putParams(psp)
|
||||||
|
} else {
|
||||||
|
ps = append(ps, Param{Key: MatchedRoutePathParam, Value: path})
|
||||||
|
handle(w, req, ps)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GET is a shortcut for router.Handle(http.MethodGet, path, handle)
|
||||||
|
func (r *Router) GET(path string, handle Handle) {
|
||||||
|
r.Handle(http.MethodGet, path, handle)
|
||||||
|
}
|
||||||
|
|
||||||
|
// HEAD is a shortcut for router.Handle(http.MethodHead, path, handle)
|
||||||
|
func (r *Router) HEAD(path string, handle Handle) {
|
||||||
|
r.Handle(http.MethodHead, path, handle)
|
||||||
|
}
|
||||||
|
|
||||||
|
// OPTIONS is a shortcut for router.Handle(http.MethodOptions, path, handle)
|
||||||
|
func (r *Router) OPTIONS(path string, handle Handle) {
|
||||||
|
r.Handle(http.MethodOptions, path, handle)
|
||||||
|
}
|
||||||
|
|
||||||
|
// POST is a shortcut for router.Handle(http.MethodPost, path, handle)
|
||||||
|
func (r *Router) POST(path string, handle Handle) {
|
||||||
|
r.Handle(http.MethodPost, path, handle)
|
||||||
|
}
|
||||||
|
|
||||||
|
// PUT is a shortcut for router.Handle(http.MethodPut, path, handle)
|
||||||
|
func (r *Router) PUT(path string, handle Handle) {
|
||||||
|
r.Handle(http.MethodPut, path, handle)
|
||||||
|
}
|
||||||
|
|
||||||
|
// PATCH is a shortcut for router.Handle(http.MethodPatch, path, handle)
|
||||||
|
func (r *Router) PATCH(path string, handle Handle) {
|
||||||
|
r.Handle(http.MethodPatch, path, handle)
|
||||||
|
}
|
||||||
|
|
||||||
|
// DELETE is a shortcut for router.Handle(http.MethodDelete, path, handle)
|
||||||
|
func (r *Router) DELETE(path string, handle Handle) {
|
||||||
|
r.Handle(http.MethodDelete, path, handle)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Replace registers or replace 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) Replace(method, path string, handle Handle) {
|
||||||
|
r.addRoute(method, path, true, 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) {
|
||||||
|
r.addRoute(method, path, false, handle)
|
||||||
|
}
|
||||||
|
|
||||||
|
// addRoute 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) addRoute(method, path string, replace bool, handle Handle) {
|
||||||
|
varsCount := uint16(0)
|
||||||
|
|
||||||
|
if method == "" {
|
||||||
|
panic("method must not be empty")
|
||||||
|
}
|
||||||
|
if len(path) < 1 || path[0] != '/' {
|
||||||
|
panic("path must begin with '/' in path '" + path + "'")
|
||||||
|
}
|
||||||
|
if handle == nil {
|
||||||
|
panic("handle must not be nil")
|
||||||
|
}
|
||||||
|
|
||||||
|
if r.SaveMatchedRoutePath {
|
||||||
|
varsCount++
|
||||||
|
handle = r.saveMatchedRoutePath(path, handle)
|
||||||
|
}
|
||||||
|
|
||||||
|
if r.trees == nil {
|
||||||
|
r.trees = make(map[string]*node)
|
||||||
|
}
|
||||||
|
|
||||||
|
root := r.trees[method]
|
||||||
|
if root == nil {
|
||||||
|
root = new(node)
|
||||||
|
r.trees[method] = root
|
||||||
|
|
||||||
|
r.globalAllowed = r.allowed("*", "")
|
||||||
|
}
|
||||||
|
|
||||||
|
root.addRoute(path, handle, replace)
|
||||||
|
|
||||||
|
// Update maxParams
|
||||||
|
if paramsCount := countParams(path); paramsCount+varsCount > r.maxParams {
|
||||||
|
r.maxParams = paramsCount + varsCount
|
||||||
|
}
|
||||||
|
|
||||||
|
// Lazy-init paramsPool alloc func
|
||||||
|
if r.paramsPool.New == nil && r.maxParams > 0 {
|
||||||
|
r.paramsPool.New = func() interface{} {
|
||||||
|
ps := make(Params, 0, r.maxParams)
|
||||||
|
return &ps
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handler is an adapter which allows the usage of an http.Handler as a
|
||||||
|
// request handle.
|
||||||
|
// The Params are available in the request context under ParamsKey.
|
||||||
|
func (r *Router) Handler(method, path string, handler http.Handler) {
|
||||||
|
r.Handle(method, path,
|
||||||
|
func(w http.ResponseWriter, req *http.Request, p Params) {
|
||||||
|
if len(p) > 0 {
|
||||||
|
ctx := req.Context()
|
||||||
|
ctx = context.WithValue(ctx, ParamsKey, p)
|
||||||
|
req = req.WithContext(ctx)
|
||||||
|
}
|
||||||
|
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.Handler(method, path, handler)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 in path '" + path + "'")
|
||||||
|
}
|
||||||
|
|
||||||
|
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.
|
||||||
|
// If the path was found, it returns the handle function and the path parameter
|
||||||
|
// values. Otherwise the third return value indicates whether a redirection to
|
||||||
|
// the same path with an extra / without the trailing slash should be performed.
|
||||||
|
func (r *Router) Lookup(method, path string) (Handle, Params, bool) {
|
||||||
|
if root := r.trees[method]; root != nil {
|
||||||
|
handle, ps, tsr := root.getValue(path, r.getParams)
|
||||||
|
if handle == nil {
|
||||||
|
r.putParams(ps)
|
||||||
|
return nil, nil, tsr
|
||||||
|
}
|
||||||
|
if ps == nil {
|
||||||
|
return handle, nil, tsr
|
||||||
|
}
|
||||||
|
return handle, *ps, tsr
|
||||||
|
}
|
||||||
|
return nil, nil, false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Router) allowed(path, reqMethod string) (allow string) {
|
||||||
|
allowed := make([]string, 0, 9)
|
||||||
|
|
||||||
|
if path == "*" { // server-wide
|
||||||
|
// empty method is used for internal calls to refresh the cache
|
||||||
|
if reqMethod == "" {
|
||||||
|
for method := range r.trees {
|
||||||
|
if method == http.MethodOptions {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
// Add request method to list of allowed methods
|
||||||
|
allowed = append(allowed, method)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return r.globalAllowed
|
||||||
|
}
|
||||||
|
} else { // specific path
|
||||||
|
for method := range r.trees {
|
||||||
|
// Skip the requested method - we already tried this one
|
||||||
|
if method == reqMethod || method == http.MethodOptions {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
handle, _, _ := r.trees[method].getValue(path, nil)
|
||||||
|
if handle != nil {
|
||||||
|
// Add request method to list of allowed methods
|
||||||
|
allowed = append(allowed, method)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(allowed) > 0 {
|
||||||
|
// Add request method to list of allowed methods
|
||||||
|
allowed = append(allowed, http.MethodOptions)
|
||||||
|
|
||||||
|
// Sort allowed methods.
|
||||||
|
// sort.Strings(allowed) unfortunately causes unnecessary allocations
|
||||||
|
// due to allowed being moved to the heap and interface conversion
|
||||||
|
for i, l := 1, len(allowed); i < l; i++ {
|
||||||
|
for j := i; j > 0 && allowed[j] < allowed[j-1]; j-- {
|
||||||
|
allowed[j], allowed[j-1] = allowed[j-1], allowed[j]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// return as comma separated list
|
||||||
|
return strings.Join(allowed, ", ")
|
||||||
|
}
|
||||||
|
|
||||||
|
return allow
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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)
|
||||||
|
}
|
||||||
|
|
||||||
|
path := req.URL.Path
|
||||||
|
|
||||||
|
if root := r.trees[req.Method]; root != nil {
|
||||||
|
if handle, ps, tsr := root.getValue(path, r.getParams); handle != nil {
|
||||||
|
if ps != nil {
|
||||||
|
handle(w, req, *ps)
|
||||||
|
r.putParams(ps)
|
||||||
|
} else {
|
||||||
|
handle(w, req, nil)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
} else if req.Method != http.MethodConnect && path != "/" {
|
||||||
|
// Moved Permanently, request with GET method
|
||||||
|
code := http.StatusMovedPermanently
|
||||||
|
if req.Method != http.MethodGet {
|
||||||
|
// Permanent Redirect, request with same method
|
||||||
|
code = http.StatusPermanentRedirect
|
||||||
|
}
|
||||||
|
|
||||||
|
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 = fixedPath
|
||||||
|
http.Redirect(w, req, req.URL.String(), code)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if req.Method == http.MethodOptions && r.HandleOPTIONS {
|
||||||
|
// Handle OPTIONS requests
|
||||||
|
if allow := r.allowed(path, http.MethodOptions); allow != "" {
|
||||||
|
w.Header().Set("Allow", allow)
|
||||||
|
if r.GlobalOPTIONS != nil {
|
||||||
|
r.GlobalOPTIONS.ServeHTTP(w, req)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
} else if r.HandleMethodNotAllowed { // Handle 405
|
||||||
|
if allow := r.allowed(path, req.Method); allow != "" {
|
||||||
|
w.Header().Set("Allow", allow)
|
||||||
|
if r.MethodNotAllowed != nil {
|
||||||
|
r.MethodNotAllowed.ServeHTTP(w, req)
|
||||||
|
} else {
|
||||||
|
http.Error(w,
|
||||||
|
http.StatusText(http.StatusMethodNotAllowed),
|
||||||
|
http.StatusMethodNotAllowed,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle 404
|
||||||
|
if r.NotFound != nil {
|
||||||
|
r.NotFound.ServeHTTP(w, req)
|
||||||
|
} else {
|
||||||
|
http.NotFound(w, req)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,683 @@
|
||||||
|
package router
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
"unicode"
|
||||||
|
"unicode/utf8"
|
||||||
|
)
|
||||||
|
|
||||||
|
func min(a, b int) int {
|
||||||
|
if a <= b {
|
||||||
|
return a
|
||||||
|
}
|
||||||
|
return b
|
||||||
|
}
|
||||||
|
|
||||||
|
func longestCommonPrefix(a, b string) int {
|
||||||
|
i := 0
|
||||||
|
max := min(len(a), len(b))
|
||||||
|
for i < max && a[i] == b[i] {
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
return i
|
||||||
|
}
|
||||||
|
|
||||||
|
// Search for a wildcard segment and check the name for invalid characters.
|
||||||
|
// Returns -1 as index, if no wildcard was found.
|
||||||
|
func findWildcard(path string) (wilcard string, i int, valid bool) {
|
||||||
|
// Find start
|
||||||
|
for start, c := range []byte(path) {
|
||||||
|
// A wildcard starts with ':' (param) or '*' (catch-all)
|
||||||
|
if c != ':' && c != '*' {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find end and check for invalid characters
|
||||||
|
valid = true
|
||||||
|
for end, c := range []byte(path[start+1:]) {
|
||||||
|
switch c {
|
||||||
|
case '/':
|
||||||
|
return path[start : start+1+end], start, valid
|
||||||
|
case ':', '*':
|
||||||
|
valid = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return path[start:], start, valid
|
||||||
|
}
|
||||||
|
return "", -1, false
|
||||||
|
}
|
||||||
|
|
||||||
|
func countParams(path string) uint16 {
|
||||||
|
var n uint
|
||||||
|
for i := range []byte(path) {
|
||||||
|
switch path[i] {
|
||||||
|
case ':', '*':
|
||||||
|
n++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return uint16(n)
|
||||||
|
}
|
||||||
|
|
||||||
|
type nodeType uint8
|
||||||
|
|
||||||
|
const (
|
||||||
|
static nodeType = iota // default
|
||||||
|
root
|
||||||
|
param
|
||||||
|
catchAll
|
||||||
|
)
|
||||||
|
|
||||||
|
type node struct {
|
||||||
|
path string
|
||||||
|
indices string
|
||||||
|
wildChild bool
|
||||||
|
nType nodeType
|
||||||
|
priority uint32
|
||||||
|
children []*node
|
||||||
|
handle Handle
|
||||||
|
}
|
||||||
|
|
||||||
|
// Increments priority of the given child and reorders if necessary
|
||||||
|
func (n *node) incrementChildPrio(pos int) int {
|
||||||
|
cs := n.children
|
||||||
|
cs[pos].priority++
|
||||||
|
prio := cs[pos].priority
|
||||||
|
|
||||||
|
// Adjust position (move to front)
|
||||||
|
newPos := pos
|
||||||
|
for ; newPos > 0 && cs[newPos-1].priority < prio; newPos-- {
|
||||||
|
// Swap node positions
|
||||||
|
cs[newPos-1], cs[newPos] = cs[newPos], cs[newPos-1]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build new index char string
|
||||||
|
if newPos != pos {
|
||||||
|
n.indices = n.indices[:newPos] + // Unchanged prefix, might be empty
|
||||||
|
n.indices[pos:pos+1] + // The index char we move
|
||||||
|
n.indices[newPos:pos] + n.indices[pos+1:] // Rest without char at 'pos'
|
||||||
|
}
|
||||||
|
|
||||||
|
return newPos
|
||||||
|
}
|
||||||
|
|
||||||
|
// addRoute adds a node with the given handle to the path.
|
||||||
|
// Not concurrency-safe!
|
||||||
|
func (n *node) addRoute(path string, handle Handle, replace bool) {
|
||||||
|
fullPath := path
|
||||||
|
n.priority++
|
||||||
|
|
||||||
|
// Empty tree
|
||||||
|
if n.path == "" && n.indices == "" {
|
||||||
|
n.insertChild(path, fullPath, handle)
|
||||||
|
n.nType = root
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
walk:
|
||||||
|
for {
|
||||||
|
// Find the longest common prefix.
|
||||||
|
// This also implies that the common prefix contains no ':' or '*'
|
||||||
|
// since the existing key can't contain those chars.
|
||||||
|
i := longestCommonPrefix(path, n.path)
|
||||||
|
|
||||||
|
// Split edge
|
||||||
|
if i < len(n.path) {
|
||||||
|
child := node{
|
||||||
|
path: n.path[i:],
|
||||||
|
wildChild: n.wildChild,
|
||||||
|
nType: static,
|
||||||
|
indices: n.indices,
|
||||||
|
children: n.children,
|
||||||
|
handle: n.handle,
|
||||||
|
priority: n.priority - 1,
|
||||||
|
}
|
||||||
|
|
||||||
|
n.children = []*node{&child}
|
||||||
|
// []byte for proper unicode char conversion, see #65
|
||||||
|
n.indices = string([]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++
|
||||||
|
|
||||||
|
// Check if the wildcard matches
|
||||||
|
if len(path) >= len(n.path) && n.path == path[:len(n.path)] &&
|
||||||
|
// Adding a child to a catchAll is not possible
|
||||||
|
n.nType != catchAll &&
|
||||||
|
// Check for longer wildcard, e.g. :name and :names
|
||||||
|
(len(n.path) >= len(path) || path[len(n.path)] == '/') {
|
||||||
|
continue walk
|
||||||
|
} else {
|
||||||
|
// Wildcard conflict
|
||||||
|
pathSeg := path
|
||||||
|
if n.nType != catchAll {
|
||||||
|
pathSeg = strings.SplitN(pathSeg, "/", 2)[0]
|
||||||
|
}
|
||||||
|
prefix := fullPath[:strings.Index(fullPath, pathSeg)] + n.path
|
||||||
|
panic("'" + pathSeg +
|
||||||
|
"' in new path '" + fullPath +
|
||||||
|
"' conflicts with existing wildcard '" + n.path +
|
||||||
|
"' in existing prefix '" + prefix +
|
||||||
|
"'")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
idxc := path[0]
|
||||||
|
|
||||||
|
// '/' after param
|
||||||
|
if n.nType == param && idxc == '/' && len(n.children) == 1 {
|
||||||
|
n = n.children[0]
|
||||||
|
n.priority++
|
||||||
|
continue walk
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if a child with the next path byte exists
|
||||||
|
for i, c := range []byte(n.indices) {
|
||||||
|
if c == idxc {
|
||||||
|
i = n.incrementChildPrio(i)
|
||||||
|
n = n.children[i]
|
||||||
|
continue walk
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Otherwise insert it
|
||||||
|
if idxc != ':' && idxc != '*' {
|
||||||
|
// []byte for proper unicode char conversion, see #65
|
||||||
|
n.indices += string([]byte{idxc})
|
||||||
|
child := &node{}
|
||||||
|
n.children = append(n.children, child)
|
||||||
|
n.incrementChildPrio(len(n.indices) - 1)
|
||||||
|
n = child
|
||||||
|
}
|
||||||
|
n.insertChild(path, fullPath, handle)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Otherwise add handle to current node
|
||||||
|
if !replace {
|
||||||
|
if n.handle != nil {
|
||||||
|
panic("a handle is already registered for path '" + fullPath + "'")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
n.handle = handle
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *node) insertChild(path, fullPath string, handle Handle) {
|
||||||
|
for {
|
||||||
|
// Find prefix until first wildcard
|
||||||
|
wildcard, i, valid := findWildcard(path)
|
||||||
|
if i < 0 { // No wilcard found
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
// The wildcard name must not contain ':' and '*'
|
||||||
|
if !valid {
|
||||||
|
panic("only one wildcard per path segment is allowed, has: '" +
|
||||||
|
wildcard + "' in path '" + fullPath + "'")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the wildcard has a name
|
||||||
|
if len(wildcard) < 2 {
|
||||||
|
panic("wildcards must be named with a non-empty name in path '" + fullPath + "'")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if this node has existing children which would be
|
||||||
|
// unreachable if we insert the wildcard here
|
||||||
|
if len(n.children) > 0 {
|
||||||
|
panic("wildcard segment '" + wildcard +
|
||||||
|
"' conflicts with existing children in path '" + fullPath + "'")
|
||||||
|
}
|
||||||
|
|
||||||
|
// param
|
||||||
|
if wildcard[0] == ':' {
|
||||||
|
if i > 0 {
|
||||||
|
// Insert prefix before the current wildcard
|
||||||
|
n.path = path[:i]
|
||||||
|
path = path[i:]
|
||||||
|
}
|
||||||
|
|
||||||
|
n.wildChild = true
|
||||||
|
child := &node{
|
||||||
|
nType: param,
|
||||||
|
path: wildcard,
|
||||||
|
}
|
||||||
|
n.children = []*node{child}
|
||||||
|
n = child
|
||||||
|
n.priority++
|
||||||
|
|
||||||
|
// If the path doesn't end with the wildcard, then there
|
||||||
|
// will be another non-wildcard subpath starting with '/'
|
||||||
|
if len(wildcard) < len(path) {
|
||||||
|
path = path[len(wildcard):]
|
||||||
|
child := &node{
|
||||||
|
priority: 1,
|
||||||
|
}
|
||||||
|
n.children = []*node{child}
|
||||||
|
n = child
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Otherwise we're done. Insert the handle in the new leaf
|
||||||
|
n.handle = handle
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// catchAll
|
||||||
|
if i+len(wildcard) != len(path) {
|
||||||
|
panic("catch-all routes are only allowed at the end of the path in path '" + fullPath + "'")
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(n.path) > 0 && n.path[len(n.path)-1] == '/' {
|
||||||
|
panic("catch-all conflicts with existing handle for the path segment root in path '" + fullPath + "'")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Currently fixed width 1 for '/'
|
||||||
|
i--
|
||||||
|
if path[i] != '/' {
|
||||||
|
panic("no / before catch-all in path '" + fullPath + "'")
|
||||||
|
}
|
||||||
|
|
||||||
|
n.path = path[:i]
|
||||||
|
|
||||||
|
// First node: catchAll node with empty path
|
||||||
|
child := &node{
|
||||||
|
wildChild: true,
|
||||||
|
nType: catchAll,
|
||||||
|
}
|
||||||
|
n.children = []*node{child}
|
||||||
|
n.indices = string('/')
|
||||||
|
n = child
|
||||||
|
n.priority++
|
||||||
|
|
||||||
|
// Second node: node holding the variable
|
||||||
|
child = &node{
|
||||||
|
path: path[i:],
|
||||||
|
nType: catchAll,
|
||||||
|
handle: handle,
|
||||||
|
priority: 1,
|
||||||
|
}
|
||||||
|
n.children = []*node{child}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// If no wildcard was found, simply insert the path and handle
|
||||||
|
n.path = path
|
||||||
|
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, params func() *Params) (handle Handle, ps *Params, tsr bool) {
|
||||||
|
walk: // Outer loop for walking the tree
|
||||||
|
for {
|
||||||
|
prefix := n.path
|
||||||
|
if len(path) > len(prefix) {
|
||||||
|
if path[:len(prefix)] == prefix {
|
||||||
|
path = path[len(prefix):]
|
||||||
|
|
||||||
|
// 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 {
|
||||||
|
idxc := path[0]
|
||||||
|
for i, c := range []byte(n.indices) {
|
||||||
|
if c == idxc {
|
||||||
|
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 params != nil {
|
||||||
|
if ps == nil {
|
||||||
|
ps = params()
|
||||||
|
}
|
||||||
|
// Expand slice within preallocated capacity
|
||||||
|
i := len(*ps)
|
||||||
|
*ps = (*ps)[:i+1]
|
||||||
|
(*ps)[i] = Param{
|
||||||
|
Key: n.path[1:],
|
||||||
|
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) || (n.path == "" && n.indices == "/")
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
|
||||||
|
case catchAll:
|
||||||
|
// Save param value
|
||||||
|
if params != nil {
|
||||||
|
if ps == nil {
|
||||||
|
ps = params()
|
||||||
|
}
|
||||||
|
// Expand slice within preallocated capacity
|
||||||
|
i := len(*ps)
|
||||||
|
*ps = (*ps)[:i+1]
|
||||||
|
(*ps)[i] = Param{
|
||||||
|
Key: n.path[2:],
|
||||||
|
Value: path,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handle = n.handle
|
||||||
|
return
|
||||||
|
|
||||||
|
default:
|
||||||
|
panic("invalid node type")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if path == prefix {
|
||||||
|
// We should have reached the node containing the handle.
|
||||||
|
// Check if this node has a handle registered.
|
||||||
|
if handle = n.handle; handle != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// If there is no handle for this route, but this route has a
|
||||||
|
// wildcard child, there must be a handle for this path with an
|
||||||
|
// additional trailing slash
|
||||||
|
if path == "/" && n.wildChild && n.nType != root {
|
||||||
|
tsr = true
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if path == "/" && n.nType == static {
|
||||||
|
tsr = true
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// No handle found. Check if a handle for this path + a
|
||||||
|
// trailing slash exists for trailing slash recommendation
|
||||||
|
for i, c := range []byte(n.indices) {
|
||||||
|
if c == '/' {
|
||||||
|
n = n.children[i]
|
||||||
|
tsr = (len(n.path) == 1 && 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(prefix) == len(path)+1 && prefix[len(path)] == '/' &&
|
||||||
|
path == prefix[:len(prefix)-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 whether the lookup
|
||||||
|
// was successful.
|
||||||
|
func (n *node) findCaseInsensitivePath(path string, fixTrailingSlash bool) (fixedPath string, found bool) {
|
||||||
|
const stackBufSize = 128
|
||||||
|
|
||||||
|
// Use a static sized buffer on the stack in the common case.
|
||||||
|
// If the path is too long, allocate a buffer on the heap instead.
|
||||||
|
buf := make([]byte, 0, stackBufSize)
|
||||||
|
if l := len(path) + 1; l > stackBufSize {
|
||||||
|
buf = make([]byte, 0, l)
|
||||||
|
}
|
||||||
|
|
||||||
|
ciPath := n.findCaseInsensitivePathRec(
|
||||||
|
path,
|
||||||
|
buf, // Preallocate enough memory for new path
|
||||||
|
[4]byte{}, // Empty rune buffer
|
||||||
|
fixTrailingSlash,
|
||||||
|
)
|
||||||
|
|
||||||
|
return string(ciPath), ciPath != nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Shift bytes in array by n bytes left
|
||||||
|
func shiftNRuneBytes(rb [4]byte, n int) [4]byte {
|
||||||
|
switch n {
|
||||||
|
case 0:
|
||||||
|
return rb
|
||||||
|
case 1:
|
||||||
|
return [4]byte{rb[1], rb[2], rb[3], 0}
|
||||||
|
case 2:
|
||||||
|
return [4]byte{rb[2], rb[3]}
|
||||||
|
case 3:
|
||||||
|
return [4]byte{rb[3]}
|
||||||
|
default:
|
||||||
|
return [4]byte{}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Recursive case-insensitive lookup function used by n.findCaseInsensitivePath
|
||||||
|
func (n *node) findCaseInsensitivePathRec(path string, ciPath []byte, rb [4]byte, fixTrailingSlash bool) []byte {
|
||||||
|
npLen := len(n.path)
|
||||||
|
|
||||||
|
walk: // Outer loop for walking the tree
|
||||||
|
for len(path) >= npLen && (npLen == 0 || strings.EqualFold(path[1:npLen], n.path[1:])) {
|
||||||
|
// Add common prefix to result
|
||||||
|
oldPath := path
|
||||||
|
path = path[npLen:]
|
||||||
|
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 {
|
||||||
|
// Skip rune bytes already processed
|
||||||
|
rb = shiftNRuneBytes(rb, npLen)
|
||||||
|
|
||||||
|
if rb[0] != 0 {
|
||||||
|
// Old rune not finished
|
||||||
|
idxc := rb[0]
|
||||||
|
for i, c := range []byte(n.indices) {
|
||||||
|
if c == idxc {
|
||||||
|
// continue with child node
|
||||||
|
n = n.children[i]
|
||||||
|
npLen = len(n.path)
|
||||||
|
continue walk
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Process a new rune
|
||||||
|
var rv rune
|
||||||
|
|
||||||
|
// Find rune start.
|
||||||
|
// Runes are up to 4 byte long,
|
||||||
|
// -4 would definitely be another rune.
|
||||||
|
var off int
|
||||||
|
for max := min(npLen, 3); off < max; off++ {
|
||||||
|
if i := npLen - off; utf8.RuneStart(oldPath[i]) {
|
||||||
|
// read rune from cached path
|
||||||
|
rv, _ = utf8.DecodeRuneInString(oldPath[i:])
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate lowercase bytes of current rune
|
||||||
|
lo := unicode.ToLower(rv)
|
||||||
|
utf8.EncodeRune(rb[:], lo)
|
||||||
|
|
||||||
|
// Skip already processed bytes
|
||||||
|
rb = shiftNRuneBytes(rb, off)
|
||||||
|
|
||||||
|
idxc := rb[0]
|
||||||
|
for i, c := range []byte(n.indices) {
|
||||||
|
// Lowercase matches
|
||||||
|
if c == idxc {
|
||||||
|
// must use a recursive approach since both the
|
||||||
|
// uppercase byte and the lowercase byte might exist
|
||||||
|
// as an index
|
||||||
|
if out := n.children[i].findCaseInsensitivePathRec(
|
||||||
|
path, ciPath, rb, fixTrailingSlash,
|
||||||
|
); out != nil {
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we found no match, the same for the uppercase rune,
|
||||||
|
// if it differs
|
||||||
|
if up := unicode.ToUpper(rv); up != lo {
|
||||||
|
utf8.EncodeRune(rb[:], up)
|
||||||
|
rb = shiftNRuneBytes(rb, off)
|
||||||
|
|
||||||
|
idxc := rb[0]
|
||||||
|
for i, c := range []byte(n.indices) {
|
||||||
|
// Uppercase matches
|
||||||
|
if c == idxc {
|
||||||
|
// Continue with child node
|
||||||
|
n = n.children[i]
|
||||||
|
npLen = len(n.path)
|
||||||
|
continue walk
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Nothing found. We can recommend to redirect to the same URL
|
||||||
|
// without a trailing slash if a leaf exists for that path
|
||||||
|
if fixTrailingSlash && path == "/" && n.handle != nil {
|
||||||
|
return ciPath
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
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++
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add param value to case insensitive path
|
||||||
|
ciPath = append(ciPath, path[:end]...)
|
||||||
|
|
||||||
|
// We need to go deeper!
|
||||||
|
if end < len(path) {
|
||||||
|
if len(n.children) > 0 {
|
||||||
|
// Continue with child node
|
||||||
|
n = n.children[0]
|
||||||
|
npLen = len(n.path)
|
||||||
|
path = path[end:]
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// ... but we can't
|
||||||
|
if fixTrailingSlash && len(path) == end+1 {
|
||||||
|
return ciPath
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if n.handle != nil {
|
||||||
|
return ciPath
|
||||||
|
} 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, '/')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
|
||||||
|
case catchAll:
|
||||||
|
return append(ciPath, path...)
|
||||||
|
|
||||||
|
default:
|
||||||
|
panic("invalid 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
|
||||||
|
}
|
||||||
|
|
||||||
|
// No handle found.
|
||||||
|
// Try to fix the path by adding a trailing slash
|
||||||
|
if fixTrailingSlash {
|
||||||
|
for i, c := range []byte(n.indices) {
|
||||||
|
if c == '/' {
|
||||||
|
n = n.children[i]
|
||||||
|
if (len(n.path) == 1 && n.handle != nil) ||
|
||||||
|
(n.nType == catchAll && n.children[0].handle != nil) {
|
||||||
|
return append(ciPath, '/')
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Nothing found.
|
||||||
|
// Try to fix the path by adding / removing a trailing slash
|
||||||
|
if fixTrailingSlash {
|
||||||
|
if path == "/" {
|
||||||
|
return ciPath
|
||||||
|
}
|
||||||
|
if len(path)+1 == npLen && n.path[len(path)] == '/' &&
|
||||||
|
strings.EqualFold(path[1:], n.path[1:len(path)]) && n.handle != nil {
|
||||||
|
return append(ciPath, n.path...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -0,0 +1,183 @@
|
||||||
|
package http
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"embed"
|
||||||
|
"git.nspix.com/golang/kos/entry/http/router"
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
"path"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
ctxPool sync.Pool
|
||||||
|
)
|
||||||
|
|
||||||
|
type Server struct {
|
||||||
|
ctx context.Context
|
||||||
|
serve *http.Server
|
||||||
|
router *router.Router
|
||||||
|
middleware []Middleware
|
||||||
|
}
|
||||||
|
|
||||||
|
func (svr *Server) applyContext() *Context {
|
||||||
|
if v := ctxPool.Get(); v != nil {
|
||||||
|
if ctx, ok := v.(*Context); ok {
|
||||||
|
return ctx
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return &Context{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (svr *Server) releaseContext(ctx *Context) {
|
||||||
|
ctxPool.Put(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (svr *Server) wrapHandle(cb HandleFunc, middleware ...Middleware) router.Handle {
|
||||||
|
return func(writer http.ResponseWriter, request *http.Request, params router.Params) {
|
||||||
|
ctx := svr.applyContext()
|
||||||
|
defer func() {
|
||||||
|
svr.releaseContext(ctx)
|
||||||
|
}()
|
||||||
|
ps := make(map[string]string)
|
||||||
|
for _, v := range params {
|
||||||
|
ps[v.Key] = v.Value
|
||||||
|
}
|
||||||
|
ctx.reset(request, writer, ps)
|
||||||
|
for i := len(svr.middleware) - 1; i >= 0; i-- {
|
||||||
|
cb = svr.middleware[i](cb)
|
||||||
|
}
|
||||||
|
for i := len(middleware) - 1; i >= 0; i-- {
|
||||||
|
cb = middleware[i](cb)
|
||||||
|
}
|
||||||
|
if err := cb(ctx); err != nil {
|
||||||
|
ctx.Status(http.StatusServiceUnavailable)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (svr *Server) Use(middleware ...Middleware) {
|
||||||
|
svr.middleware = append(svr.middleware, middleware...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (svr *Server) Handle(method string, path string, cb HandleFunc, middleware ...Middleware) {
|
||||||
|
if method == "" {
|
||||||
|
method = http.MethodPost
|
||||||
|
}
|
||||||
|
if path == "" {
|
||||||
|
path = "/"
|
||||||
|
}
|
||||||
|
if !strings.HasPrefix(path, "/") {
|
||||||
|
path = "/" + path
|
||||||
|
}
|
||||||
|
svr.router.Replace(method, path, svr.wrapHandle(cb, middleware...))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (svr *Server) Group(prefix string, routes []Route, middleware ...Middleware) {
|
||||||
|
for _, route := range routes {
|
||||||
|
svr.Handle(route.Method, path.Join(prefix, route.Path), route.Handle, middleware...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (svr *Server) Embed(prefix string, root string, embedFs embed.FS) {
|
||||||
|
routePath := prefix
|
||||||
|
if !strings.HasSuffix(routePath, "/*filepath") {
|
||||||
|
if strings.HasSuffix(routePath, "/") {
|
||||||
|
routePath += "/"
|
||||||
|
} else {
|
||||||
|
routePath += "/*filepath"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
httpFs := http.FS(embedFs)
|
||||||
|
svr.Handle(MethodGet, routePath, func(ctx *Context) (err error) {
|
||||||
|
filename := strings.TrimPrefix(ctx.Request().URL.Path, prefix)
|
||||||
|
if filename == "" || filename == "/" {
|
||||||
|
filename = root + "/"
|
||||||
|
if !strings.HasSuffix(filename, "/") {
|
||||||
|
filename = filename + "/"
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if !strings.HasPrefix(filename, root) {
|
||||||
|
filename = path.Clean(path.Join(root, filename))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !strings.HasPrefix(filename, "/") {
|
||||||
|
filename = "/" + filename
|
||||||
|
}
|
||||||
|
ctx.Request().URL.Path = filename
|
||||||
|
http.FileServer(httpFs).ServeHTTP(ctx.Response(), ctx.Request())
|
||||||
|
return
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (svr *Server) Static(path string, root http.FileSystem) {
|
||||||
|
if !strings.HasSuffix(path, "/*filepath") {
|
||||||
|
if strings.HasSuffix(path, "/") {
|
||||||
|
path += "/"
|
||||||
|
} else {
|
||||||
|
path += "/*filepath"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
svr.router.ServeFiles(path, root)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (svr *Server) handleOption(res http.ResponseWriter, req *http.Request) {
|
||||||
|
res.Header().Add("Vary", "Origin")
|
||||||
|
res.Header().Add("Vary", "Access-Control-Request-Method")
|
||||||
|
res.Header().Add("Vary", "Access-Control-Request-Headers")
|
||||||
|
res.Header().Set("Access-Control-Allow-Origin", "*")
|
||||||
|
res.Header().Set("Access-Control-Allow-Credentials", "true")
|
||||||
|
res.Header().Set("Access-Control-Allow-Methods", "GET,HEAD,PUT,PATCH,POST,DELETE")
|
||||||
|
h := req.Header.Get("Access-Control-Request-Headers")
|
||||||
|
if h != "" {
|
||||||
|
res.Header().Set("Access-Control-Allow-Headers", h)
|
||||||
|
}
|
||||||
|
res.WriteHeader(http.StatusNoContent)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (svr *Server) handleRequest(res http.ResponseWriter, req *http.Request) {
|
||||||
|
res.Header().Add("Vary", "Origin")
|
||||||
|
res.Header().Set("Access-Control-Allow-Origin", "*")
|
||||||
|
res.Header().Set("Access-Control-Allow-Credentials", "true")
|
||||||
|
h := req.Header.Get("Access-Control-Request-Headers")
|
||||||
|
if h != "" {
|
||||||
|
res.Header().Set("Access-Control-Allow-Headers", h)
|
||||||
|
}
|
||||||
|
svr.router.ServeHTTP(res, req)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (svr *Server) ServeHTTP(writer http.ResponseWriter, request *http.Request) {
|
||||||
|
switch request.Method {
|
||||||
|
case http.MethodOptions:
|
||||||
|
svr.handleOption(writer, request)
|
||||||
|
default:
|
||||||
|
svr.handleRequest(writer, request)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (svr *Server) Serve(l net.Listener) (err error) {
|
||||||
|
svr.serve = &http.Server{
|
||||||
|
Handler: svr,
|
||||||
|
}
|
||||||
|
svr.router.NotFound = NotFound{}
|
||||||
|
svr.router.MethodNotAllowed = NotAllowed{}
|
||||||
|
return svr.serve.Serve(l)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (svr *Server) Shutdown() (err error) {
|
||||||
|
if svr.serve != nil {
|
||||||
|
err = svr.serve.Shutdown(svr.ctx)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func New(ctx context.Context) *Server {
|
||||||
|
svr := &Server{
|
||||||
|
ctx: ctx,
|
||||||
|
router: router.New(),
|
||||||
|
middleware: make([]Middleware, 0, 10),
|
||||||
|
}
|
||||||
|
return svr
|
||||||
|
}
|
|
@ -0,0 +1,26 @@
|
||||||
|
package http
|
||||||
|
|
||||||
|
import "net/http"
|
||||||
|
|
||||||
|
type responsePayload struct {
|
||||||
|
Code int `json:"code"`
|
||||||
|
Reason string `json:"reason,omitempty"`
|
||||||
|
Data any `json:"data,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type HandleFunc func(ctx *Context) (err error)
|
||||||
|
|
||||||
|
type Middleware func(next HandleFunc) HandleFunc
|
||||||
|
|
||||||
|
type Route struct {
|
||||||
|
Method string
|
||||||
|
Path string
|
||||||
|
Handle HandleFunc
|
||||||
|
}
|
||||||
|
|
||||||
|
func Wrap(f http.HandlerFunc) HandleFunc {
|
||||||
|
return func(ctx *Context) (err error) {
|
||||||
|
f(ctx.Response(), ctx.Request())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,53 @@
|
||||||
|
package entry
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io"
|
||||||
|
"net"
|
||||||
|
"sync/atomic"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Listener struct {
|
||||||
|
addr net.Addr
|
||||||
|
ch chan net.Conn
|
||||||
|
exitFlag int32
|
||||||
|
exitChan chan struct{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *Listener) Receive(conn net.Conn) {
|
||||||
|
select {
|
||||||
|
case l.ch <- conn:
|
||||||
|
case <-l.exitChan:
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *Listener) Accept() (net.Conn, error) {
|
||||||
|
select {
|
||||||
|
case conn, ok := <-l.ch:
|
||||||
|
if ok {
|
||||||
|
return conn, nil
|
||||||
|
} else {
|
||||||
|
return nil, io.ErrClosedPipe
|
||||||
|
}
|
||||||
|
case <-l.exitChan:
|
||||||
|
return nil, io.ErrClosedPipe
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *Listener) Close() error {
|
||||||
|
if atomic.CompareAndSwapInt32(&l.exitFlag, 0, 1) {
|
||||||
|
close(l.exitChan)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *Listener) Addr() net.Addr {
|
||||||
|
return l.addr
|
||||||
|
}
|
||||||
|
|
||||||
|
func newListener(addr net.Addr) *Listener {
|
||||||
|
return &Listener{
|
||||||
|
addr: addr,
|
||||||
|
ch: make(chan net.Conn, 10),
|
||||||
|
exitChan: make(chan struct{}),
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,16 @@
|
||||||
|
package entry
|
||||||
|
|
||||||
|
type State struct {
|
||||||
|
Accepting int32 `json:"accepting"` //是否正在接收连接
|
||||||
|
Processing int32 `json:"processing"` //是否正在处理连接
|
||||||
|
Concurrency int32 `json:"concurrency"`
|
||||||
|
Request struct {
|
||||||
|
Total int64 `json:"total"` //总处理请求
|
||||||
|
Processed int64 `json:"processed"` //处理完成的请求
|
||||||
|
Discarded int64 `json:"discarded"` //丢弃的请求
|
||||||
|
} `json:"request"`
|
||||||
|
Traffic struct {
|
||||||
|
In int64 `json:"in"` //入网流量
|
||||||
|
Out int64 `json:"out"` //出网流量
|
||||||
|
} `json:"traffic"`
|
||||||
|
}
|
|
@ -0,0 +1,12 @@
|
||||||
|
module git.nspix.com/golang/kos
|
||||||
|
|
||||||
|
go 1.20
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/mattn/go-runewidth v0.0.3
|
||||||
|
github.com/patrickmn/go-cache v2.1.0+incompatible
|
||||||
|
github.com/peterh/liner v1.2.2
|
||||||
|
github.com/sourcegraph/conc v0.3.0
|
||||||
|
)
|
||||||
|
|
||||||
|
require golang.org/x/sys v0.0.0-20211117180635-dee7805ff2e1 // indirect
|
|
@ -0,0 +1,14 @@
|
||||||
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
|
github.com/mattn/go-runewidth v0.0.3 h1:a+kO+98RDGEfo6asOGMmpodZq4FNtnGP54yps8BzLR4=
|
||||||
|
github.com/mattn/go-runewidth v0.0.3/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
|
||||||
|
github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc=
|
||||||
|
github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ=
|
||||||
|
github.com/peterh/liner v1.2.2 h1:aJ4AOodmL+JxOZZEL2u9iJf8omNRpqHc/EbrK+3mAXw=
|
||||||
|
github.com/peterh/liner v1.2.2/go.mod h1:xFwJyiKIXJZUKItq5dGHZSTBRAuG/CpeNpWLyiNRNwI=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
|
github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo=
|
||||||
|
github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0=
|
||||||
|
github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
|
||||||
|
golang.org/x/sys v0.0.0-20211117180635-dee7805ff2e1 h1:kwrAHlwJ0DUBZwQ238v+Uod/3eZ8B2K5rYsUHBQvzmI=
|
||||||
|
golang.org/x/sys v0.0.0-20211117180635-dee7805ff2e1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
|
@ -0,0 +1,43 @@
|
||||||
|
package kos
|
||||||
|
|
||||||
|
import (
|
||||||
|
"git.nspix.com/golang/kos/entry/cli"
|
||||||
|
"git.nspix.com/golang/kos/entry/http"
|
||||||
|
"sync"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
once sync.Once
|
||||||
|
std *application
|
||||||
|
)
|
||||||
|
|
||||||
|
func initApplication(cbs ...Option) {
|
||||||
|
once.Do(func() {
|
||||||
|
std = New(cbs...)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func Init(cbs ...Option) *application {
|
||||||
|
initApplication(cbs...)
|
||||||
|
return std
|
||||||
|
}
|
||||||
|
|
||||||
|
func Node() *Info {
|
||||||
|
initApplication()
|
||||||
|
return std.Info()
|
||||||
|
}
|
||||||
|
|
||||||
|
func Http() *http.Server {
|
||||||
|
initApplication()
|
||||||
|
return std.Http()
|
||||||
|
}
|
||||||
|
|
||||||
|
func Command() *cli.Server {
|
||||||
|
initApplication()
|
||||||
|
return std.Command()
|
||||||
|
}
|
||||||
|
|
||||||
|
func Handle(method string, cb HandleFunc) {
|
||||||
|
initApplication()
|
||||||
|
std.Handle(method, cb)
|
||||||
|
}
|
|
@ -0,0 +1,80 @@
|
||||||
|
package kos
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"git.nspix.com/golang/kos/util/env"
|
||||||
|
"git.nspix.com/golang/kos/util/ip"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
"syscall"
|
||||||
|
)
|
||||||
|
|
||||||
|
type (
|
||||||
|
Options struct {
|
||||||
|
Name string
|
||||||
|
Version string
|
||||||
|
Address string
|
||||||
|
Port int
|
||||||
|
EnableDebug bool //开启调试模式
|
||||||
|
DisableHttp bool //禁用HTTP入口
|
||||||
|
DisableCommand bool //禁用命令行入口
|
||||||
|
DisableStateApi bool //禁用系统状态接口
|
||||||
|
Metadata map[string]string //原数据
|
||||||
|
Context context.Context
|
||||||
|
Signals []os.Signal
|
||||||
|
server Server
|
||||||
|
shortName string
|
||||||
|
}
|
||||||
|
|
||||||
|
Option func(o *Options)
|
||||||
|
)
|
||||||
|
|
||||||
|
func (o *Options) ShortName() string {
|
||||||
|
if o.shortName != "" {
|
||||||
|
return o.shortName
|
||||||
|
}
|
||||||
|
if pos := strings.LastIndex(o.Name, "/"); pos != -1 {
|
||||||
|
o.shortName = o.Name[pos+1:]
|
||||||
|
} else {
|
||||||
|
o.shortName = o.Name
|
||||||
|
}
|
||||||
|
return o.shortName
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithName(name string, version string) Option {
|
||||||
|
return func(o *Options) {
|
||||||
|
o.Name = name
|
||||||
|
o.Version = version
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithPort(port int) Option {
|
||||||
|
return func(o *Options) {
|
||||||
|
o.Port = port
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithServer(s Server) Option {
|
||||||
|
return func(o *Options) {
|
||||||
|
o.server = s
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithDebug() Option {
|
||||||
|
return func(o *Options) {
|
||||||
|
o.EnableDebug = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewOptions() *Options {
|
||||||
|
opts := &Options{
|
||||||
|
Name: env.Get(EnvAppName, ""),
|
||||||
|
Version: env.Get(EnvAppVersion, "0.0.1"),
|
||||||
|
Context: context.Background(),
|
||||||
|
Metadata: make(map[string]string),
|
||||||
|
Signals: []os.Signal{syscall.SIGTERM, syscall.SIGINT, syscall.SIGQUIT, syscall.SIGKILL},
|
||||||
|
}
|
||||||
|
opts.Port = int(env.Integer(EnvAppPort, 80))
|
||||||
|
opts.Address = env.Get(EnvAppAddress, ip.Internal())
|
||||||
|
return opts
|
||||||
|
}
|
|
@ -0,0 +1,13 @@
|
||||||
|
package cache
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Cache interface {
|
||||||
|
Set(ctx context.Context, key string, value any)
|
||||||
|
SetEx(ctx context.Context, key string, value any, expire time.Duration)
|
||||||
|
Get(ctx context.Context, key string) (value any, ok bool)
|
||||||
|
Del(ctx context.Context, key string)
|
||||||
|
}
|
|
@ -0,0 +1,38 @@
|
||||||
|
package cache
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
std Cache
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
std = NewMemCache()
|
||||||
|
}
|
||||||
|
|
||||||
|
func SetCache(l Cache) {
|
||||||
|
std = l
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetCache() Cache {
|
||||||
|
return std
|
||||||
|
}
|
||||||
|
|
||||||
|
func Set(ctx context.Context, key string, value any) {
|
||||||
|
std.Set(ctx, key, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
func SetEx(ctx context.Context, key string, value any, expire time.Duration) {
|
||||||
|
std.SetEx(ctx, key, value, expire)
|
||||||
|
}
|
||||||
|
|
||||||
|
func Get(ctx context.Context, key string) (value any, ok bool) {
|
||||||
|
return std.Get(ctx, key)
|
||||||
|
}
|
||||||
|
|
||||||
|
func Del(ctx context.Context, key string) {
|
||||||
|
std.Del(ctx, key)
|
||||||
|
}
|
|
@ -0,0 +1,33 @@
|
||||||
|
package cache
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"github.com/patrickmn/go-cache"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type MemCache struct {
|
||||||
|
engine *cache.Cache
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cache *MemCache) Set(ctx context.Context, key string, value any) {
|
||||||
|
cache.engine.Set(key, value, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cache *MemCache) SetEx(ctx context.Context, key string, value any, expire time.Duration) {
|
||||||
|
cache.engine.Set(key, value, expire)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cache *MemCache) Get(ctx context.Context, key string) (value any, ok bool) {
|
||||||
|
return cache.engine.Get(key)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cache *MemCache) Del(ctx context.Context, key string) {
|
||||||
|
cache.engine.Delete(key)
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewMemCache() *MemCache {
|
||||||
|
return &MemCache{
|
||||||
|
engine: cache.New(time.Hour, time.Minute*90),
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,118 @@
|
||||||
|
package log
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
FG_BLACK int = 30
|
||||||
|
FG_RED = 31
|
||||||
|
FG_GREEN = 32
|
||||||
|
FG_YELLOW = 33
|
||||||
|
FG_BLUE = 34
|
||||||
|
FG_PURPLE = 35
|
||||||
|
FG_CYAN = 36
|
||||||
|
FG_GREY = 37
|
||||||
|
)
|
||||||
|
|
||||||
|
type Console struct {
|
||||||
|
Level int
|
||||||
|
EnableColor int
|
||||||
|
prefix string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (log *Console) SetLevel(lv int) {
|
||||||
|
log.Level = lv
|
||||||
|
}
|
||||||
|
|
||||||
|
func (log *Console) Prefix(s string) {
|
||||||
|
log.prefix = s
|
||||||
|
}
|
||||||
|
|
||||||
|
func (log *Console) Print(i ...interface{}) {
|
||||||
|
log.write(TraceLevel, fmt.Sprint(i...))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (log *Console) Printf(format string, args ...interface{}) {
|
||||||
|
log.write(TraceLevel, fmt.Sprintf(format, args...))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (log *Console) Debug(i ...interface{}) {
|
||||||
|
log.write(DebugLevel, fmt.Sprint(i...))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (log *Console) Debugf(format string, args ...interface{}) {
|
||||||
|
log.write(DebugLevel, fmt.Sprintf(format, args...))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (log *Console) Info(i ...interface{}) {
|
||||||
|
log.write(InfoLevel, fmt.Sprint(i...))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (log *Console) Infof(format string, args ...interface{}) {
|
||||||
|
log.write(InfoLevel, fmt.Sprintf(format, args...))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (log *Console) Warn(i ...interface{}) {
|
||||||
|
log.write(WarnLevel, fmt.Sprint(i...))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (log *Console) Warnf(format string, args ...interface{}) {
|
||||||
|
log.write(WarnLevel, fmt.Sprintf(format, args...))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (log *Console) Error(i ...interface{}) {
|
||||||
|
log.write(ErrorLevel, fmt.Sprint(i...))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (log *Console) Errorf(format string, args ...interface{}) {
|
||||||
|
log.write(ErrorLevel, fmt.Sprintf(format, args...))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (log *Console) Fatal(i ...interface{}) {
|
||||||
|
log.write(FatalLevel, fmt.Sprint(i...))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (log *Console) Fatalf(format string, args ...interface{}) {
|
||||||
|
log.write(FatalLevel, fmt.Sprintf(format, args...))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (log *Console) Panic(i ...interface{}) {
|
||||||
|
log.write(PanicLevel, fmt.Sprint(i...))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (log *Console) Panicf(format string, args ...interface{}) {
|
||||||
|
log.write(PanicLevel, fmt.Sprintf(format, args...))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (log *Console) write(level int, s string) {
|
||||||
|
if log.Level > level {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
lvColor := map[int]int{
|
||||||
|
TraceLevel: FG_GREY,
|
||||||
|
DebugLevel: FG_BLUE,
|
||||||
|
InfoLevel: FG_GREEN,
|
||||||
|
WarnLevel: FG_PURPLE,
|
||||||
|
ErrorLevel: FG_RED,
|
||||||
|
FatalLevel: FG_RED,
|
||||||
|
PanicLevel: FG_RED,
|
||||||
|
}
|
||||||
|
var ls string
|
||||||
|
if log.EnableColor > 0 {
|
||||||
|
ls = fmt.Sprintf("\033[0m\033[%dm[%s]\033[0m", lvColor[level], getLevelText(level))
|
||||||
|
} else {
|
||||||
|
ls = getLevelText(level)
|
||||||
|
}
|
||||||
|
if log.prefix != "" {
|
||||||
|
ls += " [" + log.prefix + "]"
|
||||||
|
}
|
||||||
|
fmt.Println(time.Now().Format("2006-01-02 15:04:05") + " " + ls + " " + s)
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewConsoleLogger() *Console {
|
||||||
|
return &Console{
|
||||||
|
EnableColor: 1,
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,251 @@
|
||||||
|
package log
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"strconv"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type File struct {
|
||||||
|
Filename string `json:"filename"`
|
||||||
|
MaxSize int64 `json:"max_size"`
|
||||||
|
MaxLogFiles int `json:"max_log_files"`
|
||||||
|
Format string `json:"format"`
|
||||||
|
Level int `json:"level"`
|
||||||
|
mutex sync.RWMutex
|
||||||
|
buf []byte
|
||||||
|
fp *os.File
|
||||||
|
prefix string
|
||||||
|
size int64
|
||||||
|
}
|
||||||
|
|
||||||
|
func itoa(buf *[]byte, i int, wid int) {
|
||||||
|
// Assemble decimal in reverse order.
|
||||||
|
var b [20]byte
|
||||||
|
bp := len(b) - 1
|
||||||
|
for i >= 10 || wid > 1 {
|
||||||
|
wid--
|
||||||
|
q := i / 10
|
||||||
|
b[bp] = byte('0' + i - q*10)
|
||||||
|
bp--
|
||||||
|
i = q
|
||||||
|
}
|
||||||
|
// i < 10
|
||||||
|
b[bp] = byte('0' + i)
|
||||||
|
*buf = append(*buf, b[bp:]...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (lg *File) SetLevel(lv int) {
|
||||||
|
lg.Level = lv
|
||||||
|
}
|
||||||
|
|
||||||
|
func (lg *File) Prefix(s string) {
|
||||||
|
lg.prefix = s
|
||||||
|
}
|
||||||
|
|
||||||
|
func (lg *File) Print(i ...interface{}) {
|
||||||
|
lg.write(TraceLevel, fmt.Sprint(i...))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (lg *File) Printf(format string, args ...interface{}) {
|
||||||
|
lg.write(TraceLevel, fmt.Sprintf(format, args...))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (lg *File) Debug(i ...interface{}) {
|
||||||
|
lg.write(DebugLevel, fmt.Sprint(i...))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (lg *File) Debugf(format string, args ...interface{}) {
|
||||||
|
lg.write(DebugLevel, fmt.Sprintf(format, args...))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (lg *File) Info(i ...interface{}) {
|
||||||
|
lg.write(InfoLevel, fmt.Sprint(i...))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (lg *File) Infof(format string, args ...interface{}) {
|
||||||
|
lg.write(InfoLevel, fmt.Sprintf(format, args...))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (lg *File) Warn(i ...interface{}) {
|
||||||
|
lg.write(WarnLevel, fmt.Sprint(i...))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (lg *File) Warnf(format string, args ...interface{}) {
|
||||||
|
lg.write(WarnLevel, fmt.Sprintf(format, args...))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (lg *File) Error(i ...interface{}) {
|
||||||
|
lg.write(ErrorLevel, fmt.Sprint(i...))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (lg *File) Errorf(format string, args ...interface{}) {
|
||||||
|
lg.write(ErrorLevel, fmt.Sprintf(format, args...))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (lg *File) Fatal(i ...interface{}) {
|
||||||
|
lg.write(FatalLevel, fmt.Sprint(i...))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (lg *File) Fatalf(format string, args ...interface{}) {
|
||||||
|
lg.write(FatalLevel, fmt.Sprintf(format, args...))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (lg *File) Panic(i ...interface{}) {
|
||||||
|
lg.write(PanicLevel, fmt.Sprint(i...))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (lg *File) Panicf(format string, args ...interface{}) {
|
||||||
|
lg.write(PanicLevel, fmt.Sprintf(format, args...))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (lg *File) format(buf *[]byte, level int, s string) (err error) {
|
||||||
|
t := time.Now()
|
||||||
|
year, month, day := t.Date()
|
||||||
|
itoa(buf, year, 4)
|
||||||
|
*buf = append(*buf, '-')
|
||||||
|
itoa(buf, int(month), 2)
|
||||||
|
*buf = append(*buf, '-')
|
||||||
|
itoa(buf, day, 2)
|
||||||
|
*buf = append(*buf, ' ')
|
||||||
|
hour, min, sec := t.Clock()
|
||||||
|
itoa(buf, hour, 2)
|
||||||
|
*buf = append(*buf, ':')
|
||||||
|
itoa(buf, min, 2)
|
||||||
|
*buf = append(*buf, ':')
|
||||||
|
itoa(buf, sec, 2)
|
||||||
|
*buf = append(*buf, ' ')
|
||||||
|
*buf = append(*buf, '[')
|
||||||
|
*buf = append(*buf, getLevelText(level)...)
|
||||||
|
*buf = append(*buf, ']')
|
||||||
|
*buf = append(*buf, ' ')
|
||||||
|
*buf = append(*buf, s...)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write 实现标准的写入行数
|
||||||
|
func (lg *File) Write(p []byte) (n int, err error) {
|
||||||
|
lg.mutex.Lock()
|
||||||
|
defer lg.mutex.Unlock()
|
||||||
|
if n, err = lg.fp.Write(p); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
lg.size += int64(n)
|
||||||
|
if lg.MaxSize > 0 && lg.size >= lg.MaxSize {
|
||||||
|
if err = lg.rotate(); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
lg.size = 0
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (lg *File) write(level int, s string) {
|
||||||
|
var (
|
||||||
|
n int
|
||||||
|
err error
|
||||||
|
)
|
||||||
|
if lg.Level > level {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
lg.mutex.Lock()
|
||||||
|
defer lg.mutex.Unlock()
|
||||||
|
lg.buf = lg.buf[:0]
|
||||||
|
if err = lg.format(&lg.buf, level, s); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
lg.buf = append(lg.buf, '\n')
|
||||||
|
if n, err = lg.fp.Write(lg.buf); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
lg.size += int64(n)
|
||||||
|
if lg.MaxSize > 0 && lg.size >= lg.MaxSize {
|
||||||
|
if err = lg.rotate(); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
lg.size = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (lg *File) isExists(filename string) bool {
|
||||||
|
if _, err := os.Stat(filename); err == nil {
|
||||||
|
return true
|
||||||
|
} else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// rotate 实现日志滚动处理
|
||||||
|
func (lg *File) rotate() (err error) {
|
||||||
|
if err = lg.close(); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for i := lg.MaxLogFiles; i >= 0; i-- {
|
||||||
|
filename := lg.Filename
|
||||||
|
if i > 0 {
|
||||||
|
filename += "." + strconv.Itoa(i)
|
||||||
|
}
|
||||||
|
if i == lg.MaxLogFiles {
|
||||||
|
if lg.isExists(filename) {
|
||||||
|
if err = os.Remove(filename); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if lg.isExists(filename) {
|
||||||
|
if err = os.Rename(filename, lg.Filename+"."+strconv.Itoa(i+1)); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
err = lg.open()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (lg *File) Reload() (err error) {
|
||||||
|
lg.mutex.Lock()
|
||||||
|
defer lg.mutex.Unlock()
|
||||||
|
_ = lg.close()
|
||||||
|
err = lg.open()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (lg *File) open() (err error) {
|
||||||
|
if lg.fp, err = os.OpenFile(lg.Filename, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0600); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (lg *File) Open() (err error) {
|
||||||
|
var (
|
||||||
|
info os.FileInfo
|
||||||
|
)
|
||||||
|
if err = lg.open(); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if info, err = os.Stat(lg.Filename); err == nil {
|
||||||
|
lg.size = info.Size()
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (lg *File) close() (err error) {
|
||||||
|
if lg.fp != nil {
|
||||||
|
err = lg.fp.Close()
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (lg *File) Close() (err error) {
|
||||||
|
err = lg.close()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewFileLogger(filename string) *File {
|
||||||
|
lg := &File{Filename: filename, buf: make([]byte, 1024)}
|
||||||
|
return lg
|
||||||
|
}
|
|
@ -0,0 +1,81 @@
|
||||||
|
package log
|
||||||
|
|
||||||
|
var (
|
||||||
|
std Logger
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
std = NewConsoleLogger()
|
||||||
|
}
|
||||||
|
|
||||||
|
func SetLogger(l Logger) {
|
||||||
|
std = l
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetLogger() Logger {
|
||||||
|
return std
|
||||||
|
}
|
||||||
|
|
||||||
|
func Prefix(s string) {
|
||||||
|
std.Prefix(s)
|
||||||
|
}
|
||||||
|
|
||||||
|
func SetLevel(lv int) {
|
||||||
|
std.SetLevel(lv)
|
||||||
|
}
|
||||||
|
|
||||||
|
func Print(args ...interface{}) {
|
||||||
|
std.Print(args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func Printf(format string, args ...interface{}) {
|
||||||
|
std.Printf(format, args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func Debug(args ...interface{}) {
|
||||||
|
std.Debug(args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func Debugf(format string, args ...interface{}) {
|
||||||
|
std.Debugf(format, args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func Info(args ...interface{}) {
|
||||||
|
std.Info(args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func Infof(format string, args ...interface{}) {
|
||||||
|
std.Infof(format, args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func Warn(args ...interface{}) {
|
||||||
|
std.Warn(args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func Warnf(format string, args ...interface{}) {
|
||||||
|
std.Warnf(format, args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func Error(args ...interface{}) {
|
||||||
|
std.Error(args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func Errorf(format string, args ...interface{}) {
|
||||||
|
std.Errorf(format, args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func Fatal(args ...interface{}) {
|
||||||
|
std.Fatal(args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func Fatalf(format string, args ...interface{}) {
|
||||||
|
std.Fatalf(format, args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func Panic(args ...interface{}) {
|
||||||
|
std.Panic(args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func Panicf(format string, args ...interface{}) {
|
||||||
|
std.Panicf(format, args...)
|
||||||
|
}
|
|
@ -0,0 +1,80 @@
|
||||||
|
package log
|
||||||
|
|
||||||
|
import "strings"
|
||||||
|
|
||||||
|
const (
|
||||||
|
TraceLevel int = iota + 1
|
||||||
|
DebugLevel
|
||||||
|
InfoLevel
|
||||||
|
WarnLevel
|
||||||
|
ErrorLevel
|
||||||
|
FatalLevel
|
||||||
|
PanicLevel
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
lvText = map[int]string{
|
||||||
|
TraceLevel: "TRACE",
|
||||||
|
DebugLevel: "DEBUG",
|
||||||
|
InfoLevel: "INFO",
|
||||||
|
WarnLevel: "WARN",
|
||||||
|
ErrorLevel: "ERROR",
|
||||||
|
FatalLevel: "FATAL",
|
||||||
|
PanicLevel: "PANIC",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
type Logger interface {
|
||||||
|
SetLevel(int)
|
||||||
|
Prefix(string)
|
||||||
|
Print(i ...interface{})
|
||||||
|
Printf(format string, args ...interface{})
|
||||||
|
Debug(i ...interface{})
|
||||||
|
Debugf(format string, args ...interface{})
|
||||||
|
Info(i ...interface{})
|
||||||
|
Infof(format string, args ...interface{})
|
||||||
|
Warn(i ...interface{})
|
||||||
|
Warnf(format string, args ...interface{})
|
||||||
|
Error(i ...interface{})
|
||||||
|
Errorf(format string, args ...interface{})
|
||||||
|
Fatal(i ...interface{})
|
||||||
|
Fatalf(format string, args ...interface{})
|
||||||
|
Panic(i ...interface{})
|
||||||
|
Panicf(format string, args ...interface{})
|
||||||
|
}
|
||||||
|
|
||||||
|
func getLevelText(lv int) string {
|
||||||
|
if s, ok := lvText[lv]; ok {
|
||||||
|
return s
|
||||||
|
} else {
|
||||||
|
return "TRACE"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func FormatLevel(lv int) string {
|
||||||
|
return getLevelText(lv)
|
||||||
|
}
|
||||||
|
|
||||||
|
func ParseLevel(s string) (lv int) {
|
||||||
|
lv = DebugLevel
|
||||||
|
s = strings.ToUpper(s)
|
||||||
|
for k, v := range lvText {
|
||||||
|
if s == v {
|
||||||
|
lv = k
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func ParseLevelWithoutDefault(s string) (lv int) {
|
||||||
|
lv = -1
|
||||||
|
s = strings.ToUpper(s)
|
||||||
|
for k, v := range lvText {
|
||||||
|
if s == v {
|
||||||
|
lv = k
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
|
@ -0,0 +1,13 @@
|
||||||
|
package kos
|
||||||
|
|
||||||
|
import "context"
|
||||||
|
|
||||||
|
type Plugin interface {
|
||||||
|
Name() string
|
||||||
|
Mount(ctx context.Context) (err error)
|
||||||
|
BeforeStart() (err error)
|
||||||
|
AfterStart() (err error)
|
||||||
|
BeforeStop() (err error)
|
||||||
|
AfterStop() (err error)
|
||||||
|
Umount() (err error)
|
||||||
|
}
|
|
@ -0,0 +1,347 @@
|
||||||
|
package kos
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"flag"
|
||||||
|
"fmt"
|
||||||
|
"git.nspix.com/golang/kos/entry"
|
||||||
|
"git.nspix.com/golang/kos/entry/cli"
|
||||||
|
"git.nspix.com/golang/kos/entry/http"
|
||||||
|
"git.nspix.com/golang/kos/pkg/log"
|
||||||
|
"git.nspix.com/golang/kos/util/env"
|
||||||
|
"github.com/sourcegraph/conc"
|
||||||
|
"net"
|
||||||
|
"net/http/pprof"
|
||||||
|
"os"
|
||||||
|
"os/signal"
|
||||||
|
"runtime"
|
||||||
|
"strconv"
|
||||||
|
"sync"
|
||||||
|
"sync/atomic"
|
||||||
|
"syscall"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
ErrStopping = errors.New("stopping")
|
||||||
|
|
||||||
|
cliFlag = flag.Bool("cli", false, "Go application interactive mode")
|
||||||
|
)
|
||||||
|
|
||||||
|
type (
|
||||||
|
application struct {
|
||||||
|
ctx context.Context
|
||||||
|
cancelFunc context.CancelCauseFunc
|
||||||
|
opts *Options
|
||||||
|
gateway *entry.Gateway
|
||||||
|
http *http.Server
|
||||||
|
command *cli.Server
|
||||||
|
uptime time.Time
|
||||||
|
info *Info
|
||||||
|
plugins sync.Map
|
||||||
|
waitGroup conc.WaitGroup
|
||||||
|
exitFlag int32
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
func (app *application) Log() log.Logger {
|
||||||
|
return log.GetLogger()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (app *application) Healthy() string {
|
||||||
|
if atomic.LoadInt32(&app.gateway.State().Processing) == 1 && atomic.LoadInt32(&app.gateway.State().Accepting) == 1 {
|
||||||
|
return StateHealthy
|
||||||
|
}
|
||||||
|
if atomic.LoadInt32(&app.gateway.State().Processing) == 1 {
|
||||||
|
return StateNoAccepting
|
||||||
|
}
|
||||||
|
if atomic.LoadInt32(&app.gateway.State().Accepting) == 1 {
|
||||||
|
return StateNoProgress
|
||||||
|
}
|
||||||
|
return StateUnavailable
|
||||||
|
}
|
||||||
|
|
||||||
|
func (app *application) Info() *Info {
|
||||||
|
return app.info
|
||||||
|
}
|
||||||
|
|
||||||
|
func (app *application) Http() *http.Server {
|
||||||
|
return app.http
|
||||||
|
}
|
||||||
|
|
||||||
|
func (app *application) Command() *cli.Server {
|
||||||
|
return app.command
|
||||||
|
}
|
||||||
|
|
||||||
|
func (app *application) Handle(path string, cb HandleFunc) {
|
||||||
|
if app.http != nil {
|
||||||
|
app.http.Handle(http.MethodPost, path, func(ctx *http.Context) (err error) {
|
||||||
|
return cb(ctx)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if app.command != nil {
|
||||||
|
app.command.Handle(path, "", func(ctx *cli.Context) (err error) {
|
||||||
|
return cb(ctx)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (app *application) httpServe() (err error) {
|
||||||
|
var (
|
||||||
|
l net.Listener
|
||||||
|
)
|
||||||
|
app.http = http.New(app.ctx)
|
||||||
|
if l, err = app.gateway.Apply(
|
||||||
|
entry.Feature(http.MethodGet),
|
||||||
|
entry.Feature(http.MethodHead),
|
||||||
|
entry.Feature(http.MethodPost),
|
||||||
|
entry.Feature(http.MethodPut),
|
||||||
|
entry.Feature(http.MethodPatch),
|
||||||
|
entry.Feature(http.MethodDelete),
|
||||||
|
entry.Feature(http.MethodConnect),
|
||||||
|
entry.Feature(http.MethodOptions),
|
||||||
|
entry.Feature(http.MethodTrace),
|
||||||
|
); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if app.opts.EnableDebug {
|
||||||
|
app.http.Handle(http.MethodGet, "/debug/pprof/", http.Wrap(pprof.Index))
|
||||||
|
app.http.Handle(http.MethodGet, "/debug/pprof/goroutine", http.Wrap(pprof.Index))
|
||||||
|
app.http.Handle(http.MethodGet, "/debug/pprof/heap", http.Wrap(pprof.Index))
|
||||||
|
app.http.Handle(http.MethodGet, "/debug/pprof/mutex", http.Wrap(pprof.Index))
|
||||||
|
app.http.Handle(http.MethodGet, "/debug/pprof/threadcreate", http.Wrap(pprof.Index))
|
||||||
|
app.http.Handle(http.MethodGet, "/debug/pprof/cmdline", http.Wrap(pprof.Cmdline))
|
||||||
|
app.http.Handle(http.MethodGet, "/debug/pprof/profile", http.Wrap(pprof.Profile))
|
||||||
|
app.http.Handle(http.MethodGet, "/debug/pprof/symbol", http.Wrap(pprof.Symbol))
|
||||||
|
app.http.Handle(http.MethodGet, "/debug/pprof/trace", http.Wrap(pprof.Trace))
|
||||||
|
}
|
||||||
|
timer := time.NewTimer(time.Millisecond * 200)
|
||||||
|
defer timer.Stop()
|
||||||
|
errChan := make(chan error, 1)
|
||||||
|
app.waitGroup.Go(func() {
|
||||||
|
select {
|
||||||
|
case errChan <- app.http.Serve(l):
|
||||||
|
log.Infof("http server closed")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
select {
|
||||||
|
case err = <-errChan:
|
||||||
|
case <-timer.C:
|
||||||
|
log.Infof("http server started")
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (app *application) commandServe() (err error) {
|
||||||
|
var (
|
||||||
|
l net.Listener
|
||||||
|
)
|
||||||
|
app.command = cli.New(app.ctx)
|
||||||
|
if l, err = app.gateway.Apply(
|
||||||
|
cli.Feature,
|
||||||
|
); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
timer := time.NewTimer(time.Millisecond * 200)
|
||||||
|
defer timer.Stop()
|
||||||
|
errChan := make(chan error, 1)
|
||||||
|
app.waitGroup.Go(func() {
|
||||||
|
select {
|
||||||
|
case errChan <- app.command.Serve(l):
|
||||||
|
log.Infof("command server closed")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
select {
|
||||||
|
case err = <-errChan:
|
||||||
|
case <-timer.C:
|
||||||
|
log.Infof("command server started")
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (app *application) gotoInteractive() (err error) {
|
||||||
|
var (
|
||||||
|
client *cli.Client
|
||||||
|
)
|
||||||
|
client = cli.NewClient(
|
||||||
|
app.ctx,
|
||||||
|
net.JoinHostPort(app.opts.Address, strconv.Itoa(app.opts.Port)),
|
||||||
|
)
|
||||||
|
return client.Shell()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (app *application) buildInfo() *Info {
|
||||||
|
info := &Info{
|
||||||
|
ID: app.opts.Name,
|
||||||
|
Name: app.opts.Name,
|
||||||
|
Version: app.opts.Version,
|
||||||
|
Status: StateHealthy,
|
||||||
|
Address: app.opts.Address,
|
||||||
|
Port: app.opts.Port,
|
||||||
|
Metadata: app.opts.Metadata,
|
||||||
|
}
|
||||||
|
if info.Metadata == nil {
|
||||||
|
info.Metadata = make(map[string]string)
|
||||||
|
}
|
||||||
|
info.Metadata["os"] = runtime.GOOS
|
||||||
|
info.Metadata["numOfCPU"] = strconv.Itoa(runtime.NumCPU())
|
||||||
|
info.Metadata["goVersion"] = runtime.Version()
|
||||||
|
info.Metadata["shortName"] = app.opts.ShortName()
|
||||||
|
info.Metadata["upTime"] = app.uptime.Format(time.DateTime)
|
||||||
|
return info
|
||||||
|
}
|
||||||
|
|
||||||
|
func (app *application) preStart() (err error) {
|
||||||
|
var (
|
||||||
|
addr string
|
||||||
|
)
|
||||||
|
app.ctx, app.cancelFunc = context.WithCancelCause(app.opts.Context)
|
||||||
|
if *cliFlag && !app.opts.DisableCommand {
|
||||||
|
if err = app.gotoInteractive(); err != nil {
|
||||||
|
fmt.Println(err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
os.Exit(0)
|
||||||
|
}
|
||||||
|
app.info = app.buildInfo()
|
||||||
|
app.Log().Infof("server starting")
|
||||||
|
env.Set(EnvAppName, app.opts.ShortName())
|
||||||
|
env.Set(EnvAppVersion, app.opts.Version)
|
||||||
|
addr = net.JoinHostPort(app.opts.Address, strconv.Itoa(app.opts.Port))
|
||||||
|
app.Log().Infof("server listen on: %s", addr)
|
||||||
|
app.gateway = entry.New(addr)
|
||||||
|
if err = app.gateway.Start(app.ctx); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if !app.opts.DisableHttp {
|
||||||
|
if err = app.httpServe(); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !app.opts.DisableCommand {
|
||||||
|
if err = app.commandServe(); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
app.plugins.Range(func(key, value any) bool {
|
||||||
|
if plugin, ok := value.(Plugin); ok {
|
||||||
|
if err = plugin.BeforeStart(); err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
if app.opts.server != nil {
|
||||||
|
if err = app.opts.server.Start(app.ctx); err != nil {
|
||||||
|
app.Log().Warnf("server start error: %s", err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !app.opts.DisableStateApi {
|
||||||
|
app.Handle("/-/run/state", func(ctx Context) (err error) {
|
||||||
|
return ctx.Success(State{
|
||||||
|
ID: app.opts.Name,
|
||||||
|
Name: app.opts.Name,
|
||||||
|
Version: app.opts.Version,
|
||||||
|
Uptime: time.Now().Sub(app.uptime).String(),
|
||||||
|
Gateway: app.gateway.State(),
|
||||||
|
})
|
||||||
|
})
|
||||||
|
app.Handle("/-/healthy", func(ctx Context) (err error) {
|
||||||
|
return ctx.Success(app.Healthy())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
app.plugins.Range(func(key, value any) bool {
|
||||||
|
if plugin, ok := value.(Plugin); ok {
|
||||||
|
if err = plugin.AfterStart(); err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
app.Log().Infof("server started")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (app *application) preStop() (err error) {
|
||||||
|
if !atomic.CompareAndSwapInt32(&app.exitFlag, 0, 1) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
app.Log().Infof("server stopping")
|
||||||
|
app.cancelFunc(ErrStopping)
|
||||||
|
app.plugins.Range(func(key, value any) bool {
|
||||||
|
if plugin, ok := value.(Plugin); ok {
|
||||||
|
if err = plugin.BeforeStop(); err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
if app.http != nil {
|
||||||
|
if err = app.http.Shutdown(); err != nil {
|
||||||
|
app.Log().Warnf("server http shutdown error: %s", err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if app.command != nil {
|
||||||
|
if err = app.command.Shutdown(); err != nil {
|
||||||
|
app.Log().Warnf("server command shutdown error: %s", err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err = app.gateway.Stop(); err != nil {
|
||||||
|
app.Log().Warnf("server gateway shutdown error: %s", err.Error())
|
||||||
|
}
|
||||||
|
app.plugins.Range(func(key, value any) bool {
|
||||||
|
if plugin, ok := value.(Plugin); ok {
|
||||||
|
if err = plugin.AfterStop(); err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
app.waitGroup.Wait()
|
||||||
|
app.Log().Infof("server stopped")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (app *application) Use(plugin Plugin) (err error) {
|
||||||
|
var (
|
||||||
|
ok bool
|
||||||
|
)
|
||||||
|
if _, ok = app.plugins.Load(plugin.Name()); ok {
|
||||||
|
return fmt.Errorf("plugin %s already registered", plugin.Name())
|
||||||
|
}
|
||||||
|
if err = plugin.Mount(app.ctx); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
app.plugins.Store(plugin.Name(), plugin)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (app *application) Run() (err error) {
|
||||||
|
if err = app.preStart(); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ch := make(chan os.Signal, 1)
|
||||||
|
if app.opts.Signals == nil {
|
||||||
|
app.opts.Signals = []os.Signal{syscall.SIGTERM, syscall.SIGINT, syscall.SIGQUIT, syscall.SIGKILL}
|
||||||
|
}
|
||||||
|
signal.Notify(ch, app.opts.Signals...)
|
||||||
|
select {
|
||||||
|
case <-ch:
|
||||||
|
case <-app.ctx.Done():
|
||||||
|
}
|
||||||
|
return app.preStop()
|
||||||
|
}
|
||||||
|
|
||||||
|
func New(cbs ...Option) *application {
|
||||||
|
opts := NewOptions()
|
||||||
|
for _, cb := range cbs {
|
||||||
|
cb(opts)
|
||||||
|
}
|
||||||
|
app := &application{
|
||||||
|
opts: opts,
|
||||||
|
uptime: time.Now(),
|
||||||
|
}
|
||||||
|
return app
|
||||||
|
}
|
|
@ -0,0 +1,48 @@
|
||||||
|
package kos
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"git.nspix.com/golang/kos/entry"
|
||||||
|
"git.nspix.com/golang/kos/entry/cli"
|
||||||
|
"git.nspix.com/golang/kos/entry/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
type (
|
||||||
|
// Server custom attach server
|
||||||
|
Server interface {
|
||||||
|
Start(ctx context.Context) (err error)
|
||||||
|
Stop() (err error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// HandleFunc request handle func
|
||||||
|
HandleFunc func(ctx Context) (err error)
|
||||||
|
|
||||||
|
// Application app interface
|
||||||
|
Application interface {
|
||||||
|
Info() *Info
|
||||||
|
Http() *http.Server
|
||||||
|
Command() *cli.Server
|
||||||
|
Handle(method string, cb HandleFunc)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Info application information
|
||||||
|
Info struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Version string `json:"version"`
|
||||||
|
VcsVersion string `json:"vcsVersion"`
|
||||||
|
Status string `json:"status"`
|
||||||
|
Address string `json:"address"`
|
||||||
|
Port int `json:"port"`
|
||||||
|
Metadata map[string]string `json:"metadata"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// State application runtime state
|
||||||
|
State struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Version string `json:"version"`
|
||||||
|
Uptime string `json:"uptime"`
|
||||||
|
Gateway *entry.State `json:"gateway"`
|
||||||
|
}
|
||||||
|
)
|
|
@ -0,0 +1,68 @@
|
||||||
|
package crypto
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"crypto/aes"
|
||||||
|
"crypto/cipher"
|
||||||
|
"fmt"
|
||||||
|
)
|
||||||
|
|
||||||
|
func PKCS7Padding(ciphertext []byte, blockSize int) []byte {
|
||||||
|
m := blockSize - len(ciphertext)%blockSize
|
||||||
|
n := bytes.Repeat([]byte{byte(m)}, m)
|
||||||
|
return append(ciphertext, n...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func PKCS7UnPadding(origData []byte) []byte {
|
||||||
|
m := len(origData)
|
||||||
|
n := int(origData[m-1])
|
||||||
|
if m > n {
|
||||||
|
return origData[:(m - n)]
|
||||||
|
} else {
|
||||||
|
return origData
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Encrypt(buf, key []byte) ([]byte, error) {
|
||||||
|
var (
|
||||||
|
err error
|
||||||
|
blockSize int
|
||||||
|
block cipher.Block
|
||||||
|
)
|
||||||
|
if block, err = aes.NewCipher(key); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer func() {
|
||||||
|
if v := recover(); v != nil {
|
||||||
|
err = fmt.Errorf("decrypt error %v", v)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
blockSize = block.BlockSize()
|
||||||
|
buf = PKCS7Padding(buf, blockSize)
|
||||||
|
blockMode := cipher.NewCBCEncrypter(block, key[:blockSize])
|
||||||
|
tmp := make([]byte, len(buf))
|
||||||
|
blockMode.CryptBlocks(tmp, buf)
|
||||||
|
return tmp, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func Decrypt(buf, key []byte) ([]byte, error) {
|
||||||
|
var (
|
||||||
|
err error
|
||||||
|
blockSize int
|
||||||
|
block cipher.Block
|
||||||
|
)
|
||||||
|
if block, err = aes.NewCipher(key); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer func() {
|
||||||
|
if v := recover(); v != nil {
|
||||||
|
err = fmt.Errorf("decrypt error %v", v)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
blockSize = block.BlockSize()
|
||||||
|
blockMode := cipher.NewCBCDecrypter(block, key[:blockSize])
|
||||||
|
origData := make([]byte, len(buf))
|
||||||
|
blockMode.CryptBlocks(origData, buf)
|
||||||
|
origData = PKCS7UnPadding(origData)
|
||||||
|
return origData, nil
|
||||||
|
}
|
|
@ -0,0 +1,41 @@
|
||||||
|
package env
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Get(name string, val string) string {
|
||||||
|
value := strings.TrimSpace(os.Getenv(name))
|
||||||
|
if value == "" {
|
||||||
|
return val
|
||||||
|
} else {
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Integer(name string, val int64) int64 {
|
||||||
|
value := Get(name, "")
|
||||||
|
if n, err := strconv.ParseInt(value, 10, 64); err == nil {
|
||||||
|
return n
|
||||||
|
} else {
|
||||||
|
return val
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Float(name string, val float64) float64 {
|
||||||
|
value := Get(name, "")
|
||||||
|
if n, err := strconv.ParseFloat(value, 64); err == nil {
|
||||||
|
return n
|
||||||
|
} else {
|
||||||
|
return val
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Set(name string, val string) {
|
||||||
|
value := os.Getenv(name)
|
||||||
|
if value == "" {
|
||||||
|
os.Setenv(name, val)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,10 @@
|
||||||
|
package fetch
|
||||||
|
|
||||||
|
const (
|
||||||
|
UserAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/104.0.5112.81 Safari/537.36 Edg/104.0.1293.54"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
JSON = "application/json"
|
||||||
|
XML = "application/xml"
|
||||||
|
)
|
|
@ -0,0 +1,177 @@
|
||||||
|
package fetch
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"crypto/tls"
|
||||||
|
"encoding/json"
|
||||||
|
"encoding/xml"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
httpClient = http.Client{
|
||||||
|
Timeout: time.Second * 15,
|
||||||
|
Transport: &http.Transport{
|
||||||
|
Proxy: http.ProxyFromEnvironment,
|
||||||
|
TLSClientConfig: &tls.Config{
|
||||||
|
InsecureSkipVerify: true,
|
||||||
|
},
|
||||||
|
DialContext: (&net.Dialer{
|
||||||
|
Timeout: 30 * time.Second,
|
||||||
|
KeepAlive: 30 * time.Second,
|
||||||
|
}).DialContext,
|
||||||
|
ForceAttemptHTTP2: false,
|
||||||
|
MaxIdleConns: 10,
|
||||||
|
IdleConnTimeout: 30 * time.Second,
|
||||||
|
TLSHandshakeTimeout: 10 * time.Second,
|
||||||
|
ExpectContinueTimeout: 1 * time.Second,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
func Get(ctx context.Context, urlString string, cbs ...Option) (res *http.Response, err error) {
|
||||||
|
var (
|
||||||
|
uri *url.URL
|
||||||
|
req *http.Request
|
||||||
|
)
|
||||||
|
opts := newOptions()
|
||||||
|
for _, cb := range cbs {
|
||||||
|
cb(opts)
|
||||||
|
}
|
||||||
|
if uri, err = url.Parse(urlString); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if opts.Params != nil {
|
||||||
|
qs := uri.Query()
|
||||||
|
for k, v := range opts.Params {
|
||||||
|
qs.Set(k, v)
|
||||||
|
}
|
||||||
|
uri.RawQuery = qs.Encode()
|
||||||
|
}
|
||||||
|
if req, err = http.NewRequest(http.MethodGet, uri.String(), nil); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if opts.Header != nil {
|
||||||
|
for k, v := range opts.Header {
|
||||||
|
req.Header.Set(k, v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Do(ctx, req)
|
||||||
|
}
|
||||||
|
|
||||||
|
func Post(ctx context.Context, urlString string, cbs ...Option) (res *http.Response, err error) {
|
||||||
|
var (
|
||||||
|
buf []byte
|
||||||
|
uri *url.URL
|
||||||
|
req *http.Request
|
||||||
|
contentType string
|
||||||
|
reader io.Reader
|
||||||
|
)
|
||||||
|
opts := newOptions()
|
||||||
|
for _, cb := range cbs {
|
||||||
|
cb(opts)
|
||||||
|
}
|
||||||
|
if uri, err = url.Parse(urlString); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if opts.Params != nil {
|
||||||
|
qs := uri.Query()
|
||||||
|
for k, v := range opts.Params {
|
||||||
|
qs.Set(k, v)
|
||||||
|
}
|
||||||
|
uri.RawQuery = qs.Encode()
|
||||||
|
}
|
||||||
|
if opts.Data != nil {
|
||||||
|
switch v := opts.Data.(type) {
|
||||||
|
case string:
|
||||||
|
reader = strings.NewReader(v)
|
||||||
|
contentType = "x-www-form-urlencoded"
|
||||||
|
case []byte:
|
||||||
|
reader = bytes.NewReader(v)
|
||||||
|
contentType = "x-www-form-urlencoded"
|
||||||
|
default:
|
||||||
|
if buf, err = json.Marshal(v); err == nil {
|
||||||
|
reader = bytes.NewReader(buf)
|
||||||
|
contentType = "application/json"
|
||||||
|
} else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if req, err = http.NewRequest(http.MethodPost, uri.String(), reader); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if opts.Header != nil {
|
||||||
|
for k, v := range opts.Header {
|
||||||
|
req.Header.Set(k, v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
req.Header.Set("Content-Type", contentType)
|
||||||
|
return Do(ctx, req)
|
||||||
|
}
|
||||||
|
|
||||||
|
func Do(ctx context.Context, req *http.Request) (res *http.Response, err error) {
|
||||||
|
return httpClient.Do(req.WithContext(ctx))
|
||||||
|
}
|
||||||
|
|
||||||
|
func Request(ctx context.Context, urlString string, response any, cbs ...Option) (err error) {
|
||||||
|
var (
|
||||||
|
buf []byte
|
||||||
|
uri *url.URL
|
||||||
|
res *http.Response
|
||||||
|
req *http.Request
|
||||||
|
contentType string
|
||||||
|
)
|
||||||
|
opts := newOptions()
|
||||||
|
for _, cb := range cbs {
|
||||||
|
cb(opts)
|
||||||
|
}
|
||||||
|
if uri, err = url.Parse(urlString); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if opts.Params != nil {
|
||||||
|
qs := uri.Query()
|
||||||
|
for k, v := range opts.Params {
|
||||||
|
qs.Set(k, v)
|
||||||
|
}
|
||||||
|
uri.RawQuery = qs.Encode()
|
||||||
|
}
|
||||||
|
if req, err = http.NewRequest(http.MethodGet, uri.String(), nil); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if opts.Header != nil {
|
||||||
|
for k, v := range opts.Header {
|
||||||
|
req.Header.Set(k, v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if res, err = Do(ctx, req); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer func() {
|
||||||
|
_ = res.Body.Close()
|
||||||
|
}()
|
||||||
|
if res.StatusCode != http.StatusOK {
|
||||||
|
if buf, err = io.ReadAll(res.Body); err == nil && len(buf) > 0 {
|
||||||
|
err = fmt.Errorf("remote server response %s(%d): %s", res.Status, res.StatusCode, string(buf))
|
||||||
|
} else {
|
||||||
|
err = fmt.Errorf("remote server response %d: %s", res.StatusCode, res.Status)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
contentType = strings.ToLower(res.Header.Get("Content-Type"))
|
||||||
|
if strings.Contains(contentType, JSON) {
|
||||||
|
err = json.NewDecoder(res.Body).Decode(response)
|
||||||
|
} else if strings.Contains(contentType, XML) {
|
||||||
|
err = xml.NewDecoder(res.Body).Decode(response)
|
||||||
|
} else {
|
||||||
|
err = fmt.Errorf("unsupported content type: %s", contentType)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
|
@ -0,0 +1,50 @@
|
||||||
|
package fetch
|
||||||
|
|
||||||
|
import "net/http"
|
||||||
|
|
||||||
|
type (
|
||||||
|
Options struct {
|
||||||
|
Url string
|
||||||
|
Method string
|
||||||
|
Header map[string]string
|
||||||
|
Params map[string]string
|
||||||
|
Data any
|
||||||
|
}
|
||||||
|
Option func(o *Options)
|
||||||
|
)
|
||||||
|
|
||||||
|
func WithUrl(s string) Option {
|
||||||
|
return func(o *Options) {
|
||||||
|
o.Url = s
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithMethod(s string) Option {
|
||||||
|
return func(o *Options) {
|
||||||
|
o.Method = s
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithHeader(h map[string]string) Option {
|
||||||
|
return func(o *Options) {
|
||||||
|
o.Header = h
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithParams(h map[string]string) Option {
|
||||||
|
return func(o *Options) {
|
||||||
|
o.Params = h
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithData(v any) Option {
|
||||||
|
return func(o *Options) {
|
||||||
|
o.Data = v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func newOptions() *Options {
|
||||||
|
return &Options{
|
||||||
|
Method: http.MethodGet,
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,32 @@
|
||||||
|
package fs
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"os"
|
||||||
|
)
|
||||||
|
|
||||||
|
// IsDir Tells whether the filename is a directory
|
||||||
|
func IsDir(filename string) (bool, error) {
|
||||||
|
fd, err := os.Stat(filename)
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
fm := fd.Mode()
|
||||||
|
return fm.IsDir(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// DirectoryOrCreate checking directory, is not exists will create
|
||||||
|
func DirectoryOrCreate(dirname string) error {
|
||||||
|
if fi, err := os.Stat(dirname); err != nil {
|
||||||
|
if errors.Is(err, os.ErrNotExist) {
|
||||||
|
return os.MkdirAll(dirname, 0755)
|
||||||
|
} else {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if fi.IsDir() {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return errors.New("file not directory")
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1 @@
|
||||||
|
package fs
|
|
@ -0,0 +1,44 @@
|
||||||
|
package ip
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
func External() (res []string) {
|
||||||
|
var (
|
||||||
|
err error
|
||||||
|
addrs []net.Addr
|
||||||
|
inters []net.Interface
|
||||||
|
)
|
||||||
|
if inters, err = net.Interfaces(); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for _, inter := range inters {
|
||||||
|
if !strings.HasPrefix(inter.Name, "lo") {
|
||||||
|
if addrs, err = inter.Addrs(); err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
for _, addr := range addrs {
|
||||||
|
if ipNet, ok := addr.(*net.IPNet); ok {
|
||||||
|
if ipNet.IP.IsLoopback() || ipNet.IP.IsLinkLocalMulticast() || ipNet.IP.IsLinkLocalUnicast() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if ip4 := ipNet.IP.To4(); ip4 != nil {
|
||||||
|
switch true {
|
||||||
|
case ip4[0] == 10:
|
||||||
|
continue
|
||||||
|
case ip4[0] == 172 && ip4[1] >= 16 && ip4[1] <= 31:
|
||||||
|
continue
|
||||||
|
case ip4[0] == 192 && ip4[1] == 168:
|
||||||
|
continue
|
||||||
|
default:
|
||||||
|
res = append(res, ipNet.IP.String())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
|
@ -0,0 +1,36 @@
|
||||||
|
package ip
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Internal get internal ip.
|
||||||
|
func Internal() string {
|
||||||
|
var (
|
||||||
|
err error
|
||||||
|
addrs []net.Addr
|
||||||
|
inters []net.Interface
|
||||||
|
)
|
||||||
|
if inters, err = net.Interfaces(); err != nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
for _, inter := range inters {
|
||||||
|
if !isUp(inter.Flags) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if !strings.HasPrefix(inter.Name, "lo") {
|
||||||
|
if addrs, err = inter.Addrs(); err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
for _, addr := range addrs {
|
||||||
|
if ipNet, ok := addr.(*net.IPNet); ok && !ipNet.IP.IsLoopback() {
|
||||||
|
if ipNet.IP.To4() != nil {
|
||||||
|
return ipNet.IP.String()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
|
@ -0,0 +1,28 @@
|
||||||
|
package ip
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/binary"
|
||||||
|
"net"
|
||||||
|
)
|
||||||
|
|
||||||
|
// isUp Interface is up
|
||||||
|
func isUp(v net.Flags) bool {
|
||||||
|
return v&net.FlagUp == net.FlagUp
|
||||||
|
}
|
||||||
|
|
||||||
|
// ToLong Converts a string containing an (IPv4) Internet Protocol dotted address into a long integer
|
||||||
|
func ToLong(ipAddress string) uint32 {
|
||||||
|
ip := net.ParseIP(ipAddress)
|
||||||
|
if ip == nil {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
return binary.BigEndian.Uint32(ip.To4())
|
||||||
|
}
|
||||||
|
|
||||||
|
// FromLong Converts a long integer address into a string in (IPv4) Internet standard dotted format
|
||||||
|
func FromLong(properAddress uint32) string {
|
||||||
|
ipByte := make([]byte, 4)
|
||||||
|
binary.BigEndian.PutUint32(ipByte, properAddress)
|
||||||
|
ip := net.IP(ipByte)
|
||||||
|
return ip.String()
|
||||||
|
}
|
|
@ -0,0 +1,22 @@
|
||||||
|
package pool
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"sync"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
bufferPool sync.Pool
|
||||||
|
)
|
||||||
|
|
||||||
|
func GetBuffer() *bytes.Buffer {
|
||||||
|
if v := bufferPool.Get(); v != nil {
|
||||||
|
return v.(*bytes.Buffer)
|
||||||
|
}
|
||||||
|
return bytes.NewBuffer([]byte{})
|
||||||
|
}
|
||||||
|
|
||||||
|
func PutBuffer(b *bytes.Buffer) {
|
||||||
|
b.Reset()
|
||||||
|
bufferPool.Put(b)
|
||||||
|
}
|
|
@ -0,0 +1,50 @@
|
||||||
|
package pool
|
||||||
|
|
||||||
|
import "sync"
|
||||||
|
|
||||||
|
var (
|
||||||
|
bufPool5k sync.Pool
|
||||||
|
bufPool2k sync.Pool
|
||||||
|
bufPool1k sync.Pool
|
||||||
|
bufPool sync.Pool
|
||||||
|
)
|
||||||
|
|
||||||
|
func GetBytes(size int) []byte {
|
||||||
|
if size <= 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
var x interface{}
|
||||||
|
if size >= 5*1024 {
|
||||||
|
x = bufPool5k.Get()
|
||||||
|
} else if size >= 2*1024 {
|
||||||
|
x = bufPool2k.Get()
|
||||||
|
} else if size >= 1*1024 {
|
||||||
|
x = bufPool1k.Get()
|
||||||
|
} else {
|
||||||
|
x = bufPool.Get()
|
||||||
|
}
|
||||||
|
if x == nil {
|
||||||
|
return make([]byte, size)
|
||||||
|
}
|
||||||
|
buf := x.([]byte)
|
||||||
|
if cap(buf) < size {
|
||||||
|
return make([]byte, size)
|
||||||
|
}
|
||||||
|
return buf[:size]
|
||||||
|
}
|
||||||
|
|
||||||
|
func PutBytes(buf []byte) {
|
||||||
|
size := cap(buf)
|
||||||
|
if size <= 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if size >= 5*1024 {
|
||||||
|
bufPool5k.Put(buf)
|
||||||
|
} else if size >= 2*1024 {
|
||||||
|
bufPool2k.Put(buf)
|
||||||
|
} else if size >= 1*1024 {
|
||||||
|
bufPool1k.Put(buf)
|
||||||
|
} else {
|
||||||
|
bufPool.Put(buf)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,9 @@
|
||||||
|
package random
|
||||||
|
|
||||||
|
import (
|
||||||
|
"math/rand"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Int(min, max int64) int64 {
|
||||||
|
return min + rand.Int63n(max-min)
|
||||||
|
}
|
File diff suppressed because one or more lines are too long
|
@ -0,0 +1,28 @@
|
||||||
|
package random
|
||||||
|
|
||||||
|
import (
|
||||||
|
"math/rand"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
Uppercase = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
|
||||||
|
Lowercase = "abcdefghijklmnopqrstuvwxyz"
|
||||||
|
Alphabetic = Uppercase + Lowercase
|
||||||
|
Numeric = "0123456789"
|
||||||
|
Alphanumeric = Alphabetic + Numeric
|
||||||
|
Symbols = "`" + `~!@#$%^&*()-_+={}[]|\;:"<>,./?`
|
||||||
|
Hex = Numeric + "abcdef"
|
||||||
|
)
|
||||||
|
|
||||||
|
func String(length uint8, charsets ...string) string {
|
||||||
|
charset := strings.Join(charsets, "")
|
||||||
|
if charset == "" {
|
||||||
|
charset = Alphanumeric
|
||||||
|
}
|
||||||
|
b := make([]byte, length)
|
||||||
|
for i := range b {
|
||||||
|
b[i] = charset[rand.Int63()%int64(len(charset))]
|
||||||
|
}
|
||||||
|
return string(b)
|
||||||
|
}
|
|
@ -0,0 +1,161 @@
|
||||||
|
package reflect
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"reflect"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
func findField(v reflect.Value, field string) reflect.Value {
|
||||||
|
var (
|
||||||
|
pos int
|
||||||
|
tagValue string
|
||||||
|
refValue reflect.Value
|
||||||
|
refType reflect.Type
|
||||||
|
fieldType reflect.StructField
|
||||||
|
allowTags = []string{"json", "yaml", "xml"}
|
||||||
|
)
|
||||||
|
refValue = v.FieldByName(field)
|
||||||
|
if !refValue.IsValid() {
|
||||||
|
refType = v.Type()
|
||||||
|
for i := 0; i < refType.NumField(); i++ {
|
||||||
|
fieldType = refType.Field(i)
|
||||||
|
for _, tagName := range allowTags {
|
||||||
|
tagValue = fieldType.Tag.Get(tagName)
|
||||||
|
if tagValue == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if pos = strings.Index(tagValue, ","); pos != -1 {
|
||||||
|
tagValue = tagValue[:pos]
|
||||||
|
}
|
||||||
|
if tagValue == field {
|
||||||
|
return v.Field(i)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return refValue
|
||||||
|
}
|
||||||
|
|
||||||
|
func safeAssignment(variable reflect.Value, value interface{}) (err error) {
|
||||||
|
var (
|
||||||
|
n int64
|
||||||
|
un uint64
|
||||||
|
fn float64
|
||||||
|
kind reflect.Kind
|
||||||
|
)
|
||||||
|
rv := reflect.ValueOf(value)
|
||||||
|
kind = variable.Kind()
|
||||||
|
if kind != reflect.Slice && kind != reflect.Array && kind != reflect.Map && kind == rv.Kind() {
|
||||||
|
variable.Set(rv)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
switch kind {
|
||||||
|
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
||||||
|
switch rv.Kind() {
|
||||||
|
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
||||||
|
variable.SetInt(rv.Int())
|
||||||
|
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
|
||||||
|
variable.SetInt(int64(rv.Uint()))
|
||||||
|
case reflect.Float32, reflect.Float64:
|
||||||
|
variable.SetInt(int64(rv.Float()))
|
||||||
|
case reflect.String:
|
||||||
|
if n, err = strconv.ParseInt(rv.String(), 10, 64); err == nil {
|
||||||
|
variable.SetInt(n)
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
err = fmt.Errorf("unsupported kind %s", rv.Kind())
|
||||||
|
}
|
||||||
|
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
|
||||||
|
switch rv.Kind() {
|
||||||
|
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
||||||
|
variable.SetUint(uint64(rv.Int()))
|
||||||
|
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
|
||||||
|
variable.SetUint(rv.Uint())
|
||||||
|
case reflect.Float32, reflect.Float64:
|
||||||
|
variable.SetUint(uint64(rv.Float()))
|
||||||
|
case reflect.String:
|
||||||
|
if un, err = strconv.ParseUint(rv.String(), 10, 64); err == nil {
|
||||||
|
variable.SetUint(un)
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
err = fmt.Errorf("unsupported kind %s", rv.Kind())
|
||||||
|
}
|
||||||
|
case reflect.Float32, reflect.Float64:
|
||||||
|
switch rv.Kind() {
|
||||||
|
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
||||||
|
variable.SetFloat(float64(rv.Int()))
|
||||||
|
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
|
||||||
|
variable.SetFloat(float64(rv.Uint()))
|
||||||
|
case reflect.Float32, reflect.Float64:
|
||||||
|
variable.SetFloat(rv.Float())
|
||||||
|
case reflect.String:
|
||||||
|
if fn, err = strconv.ParseFloat(rv.String(), 64); err == nil {
|
||||||
|
variable.SetFloat(fn)
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
err = fmt.Errorf("unsupported kind %s", rv.Kind())
|
||||||
|
}
|
||||||
|
case reflect.String:
|
||||||
|
switch rv.Kind() {
|
||||||
|
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
||||||
|
variable.SetString(strconv.FormatInt(rv.Int(), 10))
|
||||||
|
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
|
||||||
|
variable.SetString(strconv.FormatUint(rv.Uint(), 10))
|
||||||
|
case reflect.Float32, reflect.Float64:
|
||||||
|
variable.SetString(strconv.FormatFloat(rv.Float(), 'f', -1, 64))
|
||||||
|
case reflect.String:
|
||||||
|
variable.SetString(rv.String())
|
||||||
|
default:
|
||||||
|
variable.SetString(fmt.Sprint(value))
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
err = fmt.Errorf("unsupported kind %s", kind)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func Set(hacky interface{}, field string, value interface{}) (err error) {
|
||||||
|
var (
|
||||||
|
n int
|
||||||
|
refField reflect.Value
|
||||||
|
)
|
||||||
|
refVal := reflect.ValueOf(hacky)
|
||||||
|
if refVal.Kind() == reflect.Ptr {
|
||||||
|
refVal = reflect.Indirect(refVal)
|
||||||
|
}
|
||||||
|
if refVal.Kind() != reflect.Struct {
|
||||||
|
return fmt.Errorf("%s kind is %v", refVal.Type().String(), refField.Kind())
|
||||||
|
}
|
||||||
|
refField = findField(refVal, field)
|
||||||
|
if !refField.IsValid() {
|
||||||
|
return fmt.Errorf("%s field `%s` not found", refVal.Type(), field)
|
||||||
|
}
|
||||||
|
rv := reflect.ValueOf(value)
|
||||||
|
fieldKind := refField.Kind()
|
||||||
|
if fieldKind != reflect.Slice && fieldKind != reflect.Array && fieldKind != reflect.Map && fieldKind == rv.Kind() {
|
||||||
|
refField.Set(rv)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
switch fieldKind {
|
||||||
|
case reflect.Array, reflect.Slice:
|
||||||
|
innerType := refField.Type().Elem()
|
||||||
|
if rv.Kind() == reflect.Array || rv.Kind() == reflect.Slice {
|
||||||
|
sliceVar := reflect.MakeSlice(refField.Type(), rv.Len(), rv.Len())
|
||||||
|
n = 0
|
||||||
|
for i := 0; i < rv.Len(); i++ {
|
||||||
|
srcVal := rv.Index(i)
|
||||||
|
dstVal := reflect.New(innerType).Elem()
|
||||||
|
if err = safeAssignment(dstVal, srcVal); err == nil {
|
||||||
|
sliceVar.Index(n).Set(dstVal)
|
||||||
|
n++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
refField.Set(sliceVar.Slice(0, n))
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
err = safeAssignment(refField, value)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
|
@ -0,0 +1,48 @@
|
||||||
|
package sys
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"runtime"
|
||||||
|
)
|
||||||
|
|
||||||
|
// HomeDir return user home directory
|
||||||
|
func HomeDir() string {
|
||||||
|
if runtime.GOOS == "windows" {
|
||||||
|
return os.Getenv("HOMEDRIVE") + os.Getenv("HOMEPATH")
|
||||||
|
}
|
||||||
|
if h := os.Getenv("HOME"); h != "" {
|
||||||
|
return h
|
||||||
|
}
|
||||||
|
return "/"
|
||||||
|
}
|
||||||
|
|
||||||
|
// HiddenFile get hidden file prefix
|
||||||
|
func HiddenFile(name string) string {
|
||||||
|
switch runtime.GOOS {
|
||||||
|
case "windows":
|
||||||
|
return "~" + name
|
||||||
|
default:
|
||||||
|
return "." + name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// CacheDir return user cache directory
|
||||||
|
func CacheDir() string {
|
||||||
|
switch runtime.GOOS {
|
||||||
|
case "darwin":
|
||||||
|
return filepath.Join(HomeDir(), "Library", "Caches")
|
||||||
|
case "windows":
|
||||||
|
for _, ev := range []string{"APPDATA", "CSIDL_APPDATA", "TEMP", "TMP"} {
|
||||||
|
if v := os.Getenv(ev); v != "" {
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Worst case:
|
||||||
|
return HomeDir()
|
||||||
|
}
|
||||||
|
if xdg := os.Getenv("XDG_CACHE_HOME"); xdg != "" {
|
||||||
|
return xdg
|
||||||
|
}
|
||||||
|
return filepath.Join(HomeDir(), ".cache")
|
||||||
|
}
|
Loading…
Reference in New Issue