Kaynağa Gözat

init project

fancl 1 yıl önce
işleme
db3e80dcf7
59 değiştirilmiş dosya ile 5596 ekleme ve 0 silme
  1. 47 0
      .gitignore
  2. 3 0
      README.md
  3. 34 0
      cmd/main.go
  4. 3 0
      cmd/web/css/index.css
  5. 14 0
      cmd/web/index.html
  6. 15 0
      constant.go
  7. 8 0
      context.go
  8. 257 0
      entry/cli/client.go
  9. 12 0
      entry/cli/constant.go
  10. 110 0
      entry/cli/context.go
  11. 154 0
      entry/cli/frame.go
  12. 177 0
      entry/cli/router.go
  13. 250 0
      entry/cli/serialize.go
  14. 201 0
      entry/cli/server.go
  15. 46 0
      entry/cli/types.go
  16. 72 0
      entry/conn.go
  17. 191 0
      entry/gateway.go
  18. 271 0
      entry/http/bind.go
  19. 107 0
      entry/http/context.go
  20. 30 0
      entry/http/handle.go
  21. 15 0
      entry/http/method.go
  22. 145 0
      entry/http/router/path.go
  23. 489 0
      entry/http/router/router.go
  24. 683 0
      entry/http/router/tree.go
  25. 183 0
      entry/http/server.go
  26. 26 0
      entry/http/types.go
  27. 53 0
      entry/listener.go
  28. 16 0
      entry/state.go
  29. 12 0
      go.mod
  30. 14 0
      go.sum
  31. 43 0
      instance.go
  32. 80 0
      options.go
  33. 13 0
      pkg/cache/cache.go
  34. 38 0
      pkg/cache/instance.go
  35. 33 0
      pkg/cache/memcache.go
  36. 118 0
      pkg/log/console.go
  37. 251 0
      pkg/log/file.go
  38. 81 0
      pkg/log/instance.go
  39. 80 0
      pkg/log/logger.go
  40. 13 0
      plugin.go
  41. 347 0
      service.go
  42. 48 0
      types.go
  43. 68 0
      util/crypto/aes/aes.go
  44. 41 0
      util/env/env.go
  45. 10 0
      util/fetch/constant.go
  46. 177 0
      util/fetch/fetch.go
  47. 50 0
      util/fetch/options.go
  48. 32 0
      util/fs/dir.go
  49. 1 0
      util/fs/file.go
  50. 44 0
      util/ip/external.go
  51. 36 0
      util/ip/internal.go
  52. 28 0
      util/ip/ip.go
  53. 22 0
      util/pool/buffer.go
  54. 50 0
      util/pool/bytes.go
  55. 9 0
      util/random/int.go
  56. 8 0
      util/random/ip.go
  57. 28 0
      util/random/string.go
  58. 161 0
      util/reflect/reflect.go
  59. 48 0
      util/sys/homedir.go

+ 47 - 0
.gitignore

@@ -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]

+ 3 - 0
README.md

@@ -0,0 +1,3 @@
+## Kos
+
+项目为基础脚手架

+ 34 - 0
cmd/main.go

@@ -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()
+}

+ 3 - 0
cmd/web/css/index.css

@@ -0,0 +1,3 @@
+h1{
+    font-size: 20px;
+}

+ 14 - 0
cmd/web/index.html

@@ -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>

+ 15 - 0
constant.go

@@ -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"
+)

+ 8 - 0
context.go

@@ -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)
+}

+ 257 - 0
entry/cli/client.go

@@ -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),
+	}
+}

+ 12 - 0
entry/cli/constant.go

@@ -0,0 +1,12 @@
+package cli
+
+var (
+	Feature = []byte{67, 76, 73}
+	OK      = []byte("OK")
+	Bye     = "Bye Bye"
+)
+
+const (
+	errNotFound      = 4004
+	errExecuteFailed = 4005
+)

+ 110 - 0
entry/cli/context.go

@@ -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,
+	}
+}

+ 154 - 0
entry/cli/frame.go

@@ -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(),
+	}
+}

+ 177 - 0
entry/cli/router.go

@@ -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),
+	}
+}

+ 250 - 0
entry/cli/serialize.go

@@ -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
+}

+ 201 - 0
entry/cli/server.go

@@ -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),
+	}
+}

+ 46 - 0
entry/cli/types.go

@@ -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)
+}

+ 72 - 0
entry/conn.go

@@ -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
+}

+ 191 - 0
entry/gateway.go

@@ -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
+}

+ 271 - 0
entry/http/bind.go

@@ -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
+}

+ 107 - 0
entry/http/context.go

@@ -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
+}

+ 30 - 0
entry/http/handle.go

@@ -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),
+	})
+}

+ 15 - 0
entry/http/method.go

@@ -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
+)

+ 145 - 0
entry/http/router/path.go

@@ -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
+}

+ 489 - 0
entry/http/router/router.go

@@ -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)
+	}
+}

+ 683 - 0
entry/http/router/tree.go

@@ -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
+}

+ 183 - 0
entry/http/server.go

@@ -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
+}

+ 26 - 0
entry/http/types.go

@@ -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
+	}
+}

+ 53 - 0
entry/listener.go

@@ -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{}),
+	}
+}

+ 16 - 0
entry/state.go

@@ -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"`
+}

+ 12 - 0
go.mod

@@ -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

+ 14 - 0
go.sum

@@ -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=

+ 43 - 0
instance.go

@@ -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)
+}

+ 80 - 0
options.go

@@ -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
+}

+ 13 - 0
pkg/cache/cache.go

@@ -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)
+}

+ 38 - 0
pkg/cache/instance.go

@@ -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)
+}

+ 33 - 0
pkg/cache/memcache.go

@@ -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),
+	}
+}

+ 118 - 0
pkg/log/console.go

@@ -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,
+	}
+}

+ 251 - 0
pkg/log/file.go

@@ -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
+}

+ 81 - 0
pkg/log/instance.go

@@ -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...)
+}

+ 80 - 0
pkg/log/logger.go

@@ -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
+}

+ 13 - 0
plugin.go

@@ -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)
+}

+ 347 - 0
service.go

@@ -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
+}

+ 48 - 0
types.go

@@ -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"`
+	}
+)

+ 68 - 0
util/crypto/aes/aes.go

@@ -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
+}

+ 41 - 0
util/env/env.go

@@ -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)
+	}
+}

+ 10 - 0
util/fetch/constant.go

@@ -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"
+)

+ 177 - 0
util/fetch/fetch.go

@@ -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
+}

+ 50 - 0
util/fetch/options.go

@@ -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,
+	}
+}

+ 32 - 0
util/fs/dir.go

@@ -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")
+	}
+}

+ 1 - 0
util/fs/file.go

@@ -0,0 +1 @@
+package fs

+ 44 - 0
util/ip/external.go

@@ -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
+}

+ 36 - 0
util/ip/internal.go

@@ -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 ""
+}

+ 28 - 0
util/ip/ip.go

@@ -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()
+}

+ 22 - 0
util/pool/buffer.go

@@ -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)
+}

+ 50 - 0
util/pool/bytes.go

@@ -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)
+	}
+}

+ 9 - 0
util/random/int.go

@@ -0,0 +1,9 @@
+package random
+
+import (
+	"math/rand"
+)
+
+func Int(min, max int64) int64 {
+	return min + rand.Int63n(max-min)
+}

Dosya farkı çok büyük olduğundan ihmal edildi
+ 8 - 0
util/random/ip.go


+ 28 - 0
util/random/string.go

@@ -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)
+}

+ 161 - 0
util/reflect/reflect.go

@@ -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
+}

+ 48 - 0
util/sys/homedir.go

@@ -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")
+}

Bu fark içinde çok fazla dosya değişikliği olduğu için bazı dosyalar gösterilmiyor