init project

This commit is contained in:
fancl 2023-04-23 17:57:36 +08:00
commit db3e80dcf7
59 changed files with 5609 additions and 0 deletions

47
.gitignore vendored 100644
View File

@ -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
README.md 100644
View File

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

34
cmd/main.go 100644
View File

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

View File

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

14
cmd/web/index.html 100644
View File

@ -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
constant.go 100644
View File

@ -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
context.go 100644
View File

@ -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
entry/cli/client.go 100644
View File

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

View File

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

View File

@ -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
entry/cli/frame.go 100644
View File

@ -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
entry/cli/router.go 100644
View File

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

View File

@ -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
entry/cli/server.go 100644
View File

@ -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
entry/cli/types.go 100644
View File

@ -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
entry/conn.go 100644
View File

@ -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
entry/gateway.go 100644
View File

@ -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
entry/http/bind.go 100644
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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
entry/listener.go 100644
View File

@ -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
entry/state.go 100644
View File

@ -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
go.mod 100644
View File

@ -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
go.sum 100644
View File

@ -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
instance.go 100644
View File

@ -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
options.go 100644
View File

@ -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
pkg/cache/cache.go vendored 100644
View File

@ -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
pkg/cache/instance.go vendored 100644
View File

@ -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
pkg/cache/memcache.go vendored 100644
View File

@ -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
pkg/log/console.go 100644
View File

@ -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
pkg/log/file.go 100644
View File

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

View File

@ -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
pkg/log/logger.go 100644
View File

@ -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
plugin.go 100644
View File

@ -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
service.go 100644
View File

@ -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
types.go 100644
View File

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

View File

@ -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
util/env/env.go vendored 100644
View File

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

View File

@ -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
util/fetch/fetch.go 100644
View File

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

View File

@ -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
util/fs/dir.go 100644
View File

@ -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
util/fs/file.go 100644
View File

@ -0,0 +1 @@
package fs

View File

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

View File

@ -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
util/ip/ip.go 100644
View File

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

View File

@ -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
util/pool/bytes.go 100644
View File

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

View File

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

21
util/random/ip.go 100644

File diff suppressed because one or more lines are too long

View File

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

View File

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

View File

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