From db3e80dcf7a091cb544c2bce02bac5d67fb94812 Mon Sep 17 00:00:00 2001 From: fancl Date: Sun, 23 Apr 2023 17:57:36 +0800 Subject: [PATCH] init project --- .gitignore | 47 +++ README.md | 3 + cmd/main.go | 34 ++ cmd/web/css/index.css | 3 + cmd/web/index.html | 14 + constant.go | 15 + context.go | 8 + entry/cli/client.go | 257 ++++++++++++++ entry/cli/constant.go | 12 + entry/cli/context.go | 110 ++++++ entry/cli/frame.go | 154 ++++++++ entry/cli/router.go | 177 ++++++++++ entry/cli/serialize.go | 250 +++++++++++++ entry/cli/server.go | 201 +++++++++++ entry/cli/types.go | 46 +++ entry/conn.go | 72 ++++ entry/gateway.go | 191 ++++++++++ entry/http/bind.go | 271 ++++++++++++++ entry/http/context.go | 107 ++++++ entry/http/handle.go | 30 ++ entry/http/method.go | 15 + entry/http/router/path.go | 145 ++++++++ entry/http/router/router.go | 489 ++++++++++++++++++++++++++ entry/http/router/tree.go | 683 ++++++++++++++++++++++++++++++++++++ entry/http/server.go | 183 ++++++++++ entry/http/types.go | 26 ++ entry/listener.go | 53 +++ entry/state.go | 16 + go.mod | 12 + go.sum | 14 + instance.go | 43 +++ options.go | 80 +++++ pkg/cache/cache.go | 13 + pkg/cache/instance.go | 38 ++ pkg/cache/memcache.go | 33 ++ pkg/log/console.go | 118 +++++++ pkg/log/file.go | 251 +++++++++++++ pkg/log/instance.go | 81 +++++ pkg/log/logger.go | 80 +++++ plugin.go | 13 + service.go | 347 ++++++++++++++++++ types.go | 48 +++ util/crypto/aes/aes.go | 68 ++++ util/env/env.go | 41 +++ util/fetch/constant.go | 10 + util/fetch/fetch.go | 177 ++++++++++ util/fetch/options.go | 50 +++ util/fs/dir.go | 32 ++ util/fs/file.go | 1 + util/ip/external.go | 44 +++ util/ip/internal.go | 36 ++ util/ip/ip.go | 28 ++ util/pool/buffer.go | 22 ++ util/pool/bytes.go | 50 +++ util/random/int.go | 9 + util/random/ip.go | 21 ++ util/random/string.go | 28 ++ util/reflect/reflect.go | 161 +++++++++ util/sys/homedir.go | 48 +++ 59 files changed, 5609 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 cmd/main.go create mode 100644 cmd/web/css/index.css create mode 100644 cmd/web/index.html create mode 100644 constant.go create mode 100644 context.go create mode 100644 entry/cli/client.go create mode 100644 entry/cli/constant.go create mode 100644 entry/cli/context.go create mode 100644 entry/cli/frame.go create mode 100644 entry/cli/router.go create mode 100644 entry/cli/serialize.go create mode 100644 entry/cli/server.go create mode 100644 entry/cli/types.go create mode 100644 entry/conn.go create mode 100644 entry/gateway.go create mode 100644 entry/http/bind.go create mode 100644 entry/http/context.go create mode 100644 entry/http/handle.go create mode 100644 entry/http/method.go create mode 100644 entry/http/router/path.go create mode 100644 entry/http/router/router.go create mode 100644 entry/http/router/tree.go create mode 100644 entry/http/server.go create mode 100644 entry/http/types.go create mode 100644 entry/listener.go create mode 100644 entry/state.go create mode 100644 go.mod create mode 100644 go.sum create mode 100644 instance.go create mode 100644 options.go create mode 100644 pkg/cache/cache.go create mode 100644 pkg/cache/instance.go create mode 100644 pkg/cache/memcache.go create mode 100644 pkg/log/console.go create mode 100644 pkg/log/file.go create mode 100644 pkg/log/instance.go create mode 100644 pkg/log/logger.go create mode 100644 plugin.go create mode 100644 service.go create mode 100644 types.go create mode 100644 util/crypto/aes/aes.go create mode 100644 util/env/env.go create mode 100644 util/fetch/constant.go create mode 100644 util/fetch/fetch.go create mode 100644 util/fetch/options.go create mode 100644 util/fs/dir.go create mode 100644 util/fs/file.go create mode 100644 util/ip/external.go create mode 100644 util/ip/internal.go create mode 100644 util/ip/ip.go create mode 100644 util/pool/buffer.go create mode 100644 util/pool/bytes.go create mode 100644 util/random/int.go create mode 100644 util/random/ip.go create mode 100644 util/random/string.go create mode 100644 util/reflect/reflect.go create mode 100644 util/sys/homedir.go diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..f4c53a2 --- /dev/null +++ b/.gitignore @@ -0,0 +1,47 @@ +bin/ + +.svn/ +.godeps +./build +.cover/ +dist +_site +_posts +*.dat +*.db +.DS_Store +.vscode +vendor + + +# Go.gitignore + +# Compiled Object files, Static and Dynamic libs (Shared Objects) +*.o +*.a +*.so + +# Folders +_obj +_test +storage +.idea + +# Architecture specific extensions/prefixes +*.[568vq] +[568vq].out + +*.cgo1.go +*.cgo2.c +_cgo_defun.c +_cgo_gotypes.go +_cgo_export.* + +_testmain.go + +*.exe + +profile + +# vim stuff +*.sw[op] \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..0f77880 --- /dev/null +++ b/README.md @@ -0,0 +1,3 @@ +## Kos + +项目为基础脚手架 \ No newline at end of file diff --git a/cmd/main.go b/cmd/main.go new file mode 100644 index 0000000..6f4a353 --- /dev/null +++ b/cmd/main.go @@ -0,0 +1,34 @@ +package main + +import ( + "context" + "embed" + "flag" + "git.nspix.com/golang/kos" + "git.nspix.com/golang/kos/pkg/log" +) + +//go:embed web +var webDir embed.FS + +type subServer struct { +} + +func (s *subServer) Start(ctx context.Context) (err error) { + kos.Http().Embed("/ui/web", "web", webDir) + return +} + +func (s *subServer) Stop() (err error) { + log.Debugf("stopxxx") + return +} + +func main() { + flag.Parse() + svr := kos.Init( + kos.WithName("git.nspix.com/golang/test", "0.0.1"), + kos.WithServer(&subServer{}), + ) + svr.Run() +} diff --git a/cmd/web/css/index.css b/cmd/web/css/index.css new file mode 100644 index 0000000..f242d93 --- /dev/null +++ b/cmd/web/css/index.css @@ -0,0 +1,3 @@ +h1{ + font-size: 20px; +} \ No newline at end of file diff --git a/cmd/web/index.html b/cmd/web/index.html new file mode 100644 index 0000000..d1518e7 --- /dev/null +++ b/cmd/web/index.html @@ -0,0 +1,14 @@ + + + + + + + Document + + + +

Hello

+ + \ No newline at end of file diff --git a/constant.go b/constant.go new file mode 100644 index 0000000..97742ca --- /dev/null +++ b/constant.go @@ -0,0 +1,15 @@ +package kos + +const ( + EnvAppName = "VOX_NAME" + EnvAppVersion = "VOX_VERSION" + EnvAppPort = "VOX_PORT" + EnvAppAddress = "VOX_ADDRESS" +) + +const ( + StateHealthy = "Healthy" + StateNoAccepting = "NoAccepting" + StateNoProgress = "NoProgress" + StateUnavailable = "Unavailable" +) diff --git a/context.go b/context.go new file mode 100644 index 0000000..e0d09cf --- /dev/null +++ b/context.go @@ -0,0 +1,8 @@ +package kos + +type Context interface { + Bind(v any) (err error) + Param(s string) string + Success(v any) (err error) + Error(code int, reason string) (err error) +} diff --git a/entry/cli/client.go b/entry/cli/client.go new file mode 100644 index 0000000..ad41936 --- /dev/null +++ b/entry/cli/client.go @@ -0,0 +1,257 @@ +package cli + +import ( + "context" + "encoding/json" + "fmt" + "github.com/peterh/liner" + "io" + "math" + "net" + "os" + "path/filepath" + "strings" + "sync" + "sync/atomic" + "time" +) + +type Client struct { + name string + ctx context.Context + address string + sequence uint16 + conn net.Conn + liner *liner.State + mutex sync.Mutex + exitChan chan struct{} + readyChan chan struct{} + commandChan chan *Frame + completerChan chan *Frame + Timeout time.Duration + exitFlag int32 +} + +func (client *Client) getSequence() uint16 { + client.mutex.Lock() + defer client.mutex.Unlock() + if client.sequence >= math.MaxUint16 { + client.sequence = 0 + } + client.sequence++ + n := client.sequence + return n +} + +func (client *Client) dialContext(ctx context.Context, address string) (conn net.Conn, err error) { + var ( + pos int + network string + dialer net.Dialer + ) + if pos = strings.Index(address, "://"); pos > -1 { + network = address[:pos] + address = address[pos+3:] + } else { + network = "tcp" + } + if conn, err = dialer.DialContext(ctx, network, address); err != nil { + return + } + return +} + +func (client *Client) renderBanner(info *Info) { + client.name = info.Name + fmt.Printf("Welcome to the %s(%s) monitor\n", info.Name, info.Version) + fmt.Printf("Your connection id is %d\n", info.ID) + fmt.Printf("Last login: %s from %s\n", info.ServerTime.Format(time.RFC822), info.RemoteAddr) + fmt.Printf("Type 'help' for help. Type 'exit' for quit. Type 'cls' to clear input statement.\n") +} + +func (client *Client) ioLoop(r io.Reader) { + defer func() { + _ = client.Close() + }() + for { + frame, err := readFrame(r) + if err != nil { + return + } + switch frame.Type { + case PacketTypeHandshake: + info := &Info{} + if err = json.Unmarshal(frame.Data, info); err == nil { + client.renderBanner(info) + } + select { + case client.readyChan <- struct{}{}: + case <-client.exitChan: + return + } + case PacketTypeCompleter: + select { + case client.completerChan <- frame: + case <-client.exitChan: + return + } + case PacketTypeCommand: + select { + case client.commandChan <- frame: + case <-client.exitChan: + return + } + } + } +} + +func (client *Client) waitResponse(seq uint16, timeout time.Duration) { + timer := time.NewTimer(timeout) + defer timer.Stop() + for { + select { + case <-timer.C: + fmt.Println("timeout waiting for response") + return + case <-client.exitChan: + return + case res, ok := <-client.commandChan: + if !ok { + break + } + if res.Seq == seq { + if res.Error != "" { + fmt.Print(res.Error) + } else { + fmt.Print(string(res.Data)) + } + if res.Flag == FlagComplete { + fmt.Println("") + return + } + } + } + } +} + +func (client *Client) completer(str string) (ss []string) { + var ( + err error + seq uint16 + ) + ss = make([]string, 0) + seq = client.getSequence() + if err = writeFrame(client.conn, newFrame(PacketTypeCompleter, FlagComplete, seq, []byte(str))); err != nil { + return + } + select { + case <-time.After(time.Second * 5): + case frame, ok := <-client.completerChan: + if ok { + err = json.Unmarshal(frame.Data, &ss) + } + } + return +} + +func (client *Client) Execute(s string) (err error) { + var ( + seq uint16 + ) + if client.conn, err = client.dialContext(client.ctx, client.address); err != nil { + return err + } + defer func() { + _ = client.Close() + }() + go client.ioLoop(client.conn) + seq = client.getSequence() + if err = writeFrame(client.conn, newFrame(PacketTypeCommand, FlagComplete, seq, []byte(s))); err != nil { + return err + } + client.waitResponse(seq, time.Second*30) + return +} + +func (client *Client) Shell() (err error) { + var ( + seq uint16 + line string + ) + client.liner.SetCtrlCAborts(true) + if client.conn, err = client.dialContext(client.ctx, client.address); err != nil { + return err + } + defer func() { + _ = client.Close() + }() + if err = writeFrame(client.conn, newFrame(PacketTypeHandshake, FlagComplete, client.getSequence(), nil)); err != nil { + return + } + go client.ioLoop(client.conn) + select { + case <-client.readyChan: + case <-client.ctx.Done(): + return + } + client.liner.SetCompleter(client.completer) + for { + if line, err = client.liner.Prompt(client.name + "> "); err != nil { + break + } + if atomic.LoadInt32(&client.exitFlag) == 1 { + fmt.Println(Bye) + break + } + line = strings.TrimSpace(line) + if line == "" { + continue + } + if strings.ToLower(line) == "exit" || strings.ToLower(line) == "quit" { + fmt.Println(Bye) + return + } + if strings.ToLower(line) == "clear" || strings.ToLower(line) == "cls" { + fmt.Print("\033[2J") + continue + } + seq = client.getSequence() + if err = writeFrame(client.conn, newFrame(PacketTypeCommand, FlagComplete, seq, []byte(line))); err != nil { + break + } + client.liner.AppendHistory(line) + client.waitResponse(seq, client.Timeout) + } + return +} + +func (client *Client) Close() (err error) { + if !atomic.CompareAndSwapInt32(&client.exitFlag, 0, 1) { + return + } + close(client.exitChan) + if client.conn != nil { + err = client.conn.Close() + } + if client.liner != nil { + err = client.liner.Close() + } + return +} + +func NewClient(ctx context.Context, addr string) *Client { + if ctx == nil { + ctx = context.Background() + } + return &Client{ + ctx: ctx, + address: addr, + name: filepath.Base(os.Args[0]), + Timeout: time.Second * 30, + liner: liner.NewLiner(), + readyChan: make(chan struct{}, 1), + exitChan: make(chan struct{}), + commandChan: make(chan *Frame, 5), + completerChan: make(chan *Frame, 5), + } +} diff --git a/entry/cli/constant.go b/entry/cli/constant.go new file mode 100644 index 0000000..ecfeee4 --- /dev/null +++ b/entry/cli/constant.go @@ -0,0 +1,12 @@ +package cli + +var ( + Feature = []byte{67, 76, 73} + OK = []byte("OK") + Bye = "Bye Bye" +) + +const ( + errNotFound = 4004 + errExecuteFailed = 4005 +) diff --git a/entry/cli/context.go b/entry/cli/context.go new file mode 100644 index 0000000..d4ec067 --- /dev/null +++ b/entry/cli/context.go @@ -0,0 +1,110 @@ +package cli + +import ( + "fmt" + "io" + "math" +) + +type Context struct { + Id int64 + seq uint16 + wc io.WriteCloser + params map[string]string + args []string +} + +func (ctx *Context) reset(id int64, wc io.WriteCloser) { + ctx.Id = id + ctx.wc = wc + ctx.seq = 0 + ctx.args = make([]string, 0) + ctx.params = make(map[string]string) +} + +func (ctx *Context) setArgs(args []string) { + ctx.args = args +} + +func (ctx *Context) setParam(ps map[string]string) { + ctx.params = ps +} + +func (ctx *Context) Bind(v any) (err error) { + return +} + +func (ctx *Context) Argument(index int) string { + if index >= len(ctx.args) || index < 0 { + return "" + } + return ctx.args[index] +} + +func (ctx *Context) Param(s string) string { + if v, ok := ctx.params[s]; ok { + return v + } + return "" +} + +func (ctx *Context) Success(v any) (err error) { + return ctx.send(responsePayload{Type: PacketTypeCommand, Data: v}) +} + +func (ctx *Context) Error(code int, reason string) (err error) { + return ctx.send(responsePayload{Type: PacketTypeCommand, Code: code, Reason: reason}) +} + +func (ctx *Context) Close() (err error) { + return ctx.wc.Close() +} + +func (ctx *Context) send(res responsePayload) (err error) { + var ( + ok bool + buf []byte + marshal encoder + ) + if res.Code > 0 { + err = writeFrame(ctx.wc, &Frame{ + Feature: Feature, + Type: res.Type, + Seq: ctx.seq, + Flag: FlagComplete, + Error: fmt.Sprintf("ERROR(%d): %s", res.Code, res.Reason), + }) + return + } + if res.Data == nil { + buf = OK + goto __END + } + if marshal, ok = res.Data.(encoder); ok { + buf, err = marshal.Marshal() + goto __END + } + buf, err = serialize(res.Data) +__END: + if err != nil { + return + } + offset := 0 + chunkSize := math.MaxInt16 - 1 + n := len(buf) / chunkSize + for i := 0; i < n; i++ { + if err = writeFrame(ctx.wc, newFrame(res.Type, FlagPortion, ctx.seq, buf[offset:chunkSize+offset])); err != nil { + return + } + offset += chunkSize + } + err = writeFrame(ctx.wc, newFrame(res.Type, FlagComplete, ctx.seq, buf[offset:])) + return +} + +func newContext(id int64, wc io.WriteCloser) *Context { + return &Context{ + Id: id, + wc: wc, + } +} diff --git a/entry/cli/frame.go b/entry/cli/frame.go new file mode 100644 index 0000000..8a1ac5f --- /dev/null +++ b/entry/cli/frame.go @@ -0,0 +1,154 @@ +package cli + +import ( + "bytes" + "encoding/binary" + "io" + "math" + "time" +) + +const ( + PacketTypeCompleter byte = 0x01 + PacketTypeCommand = 0x02 + PacketTypeHandshake = 0x03 +) + +const ( + FlagPortion = 0x00 + FlagComplete = 0x01 +) + +type ( + Frame struct { + Feature []byte + Type byte `json:"type"` + Flag byte `json:"flag"` + Seq uint16 `json:"seq"` + Data []byte `json:"data"` + Error string `json:"error"` + Timestamp int64 `json:"timestamp"` + } +) + +func readFrame(r io.Reader) (frame *Frame, err error) { + var ( + n int + dataLength uint16 + errorLength uint16 + errBuf []byte + ) + frame = &Frame{Feature: make([]byte, 3)} + if _, err = io.ReadFull(r, frame.Feature); err != nil { + return + } + if !bytes.Equal(frame.Feature, Feature) { + err = io.ErrUnexpectedEOF + return + } + if err = binary.Read(r, binary.LittleEndian, &frame.Type); err != nil { + return + } + if err = binary.Read(r, binary.LittleEndian, &frame.Flag); err != nil { + return + } + if err = binary.Read(r, binary.LittleEndian, &frame.Seq); err != nil { + return + } + if err = binary.Read(r, binary.LittleEndian, &frame.Timestamp); err != nil { + return + } + if err = binary.Read(r, binary.LittleEndian, &dataLength); err != nil { + return + } + if err = binary.Read(r, binary.LittleEndian, &errorLength); err != nil { + return + } + if dataLength > 0 { + frame.Data = make([]byte, dataLength) + if n, err = io.ReadFull(r, frame.Data); err == nil { + if n < int(dataLength) { + err = io.ErrShortBuffer + } + } + } + if errorLength > 0 { + errBuf = make([]byte, errorLength) + if n, err = io.ReadFull(r, errBuf); err == nil { + if n < int(dataLength) { + err = io.ErrShortBuffer + } else { + frame.Error = string(errBuf) + } + } + } + return +} + +func writeFrame(w io.Writer, frame *Frame) (err error) { + var ( + n int + dl int + dataLength uint16 + errorLength uint16 + errBuf []byte + ) + if _, err = w.Write(Feature); err != nil { + return + } + if frame.Data != nil { + dl = len(frame.Data) + if dl > math.MaxUint16 { + return io.ErrNoProgress + } + dataLength = uint16(dl) + } + if frame.Error != "" { + errBuf = []byte(frame.Error) + errorLength = uint16(len(errBuf)) + } + if err = binary.Write(w, binary.LittleEndian, frame.Type); err != nil { + return + } + if err = binary.Write(w, binary.LittleEndian, frame.Flag); err != nil { + return + } + if err = binary.Write(w, binary.LittleEndian, frame.Seq); err != nil { + return + } + if err = binary.Write(w, binary.LittleEndian, frame.Timestamp); err != nil { + return + } + if err = binary.Write(w, binary.LittleEndian, dataLength); err != nil { + return + } + if err = binary.Write(w, binary.LittleEndian, errorLength); err != nil { + return + } + if dataLength > 0 { + if n, err = w.Write(frame.Data); err == nil { + if n < int(dataLength) { + err = io.ErrShortWrite + } + } + } + if errorLength > 0 { + if n, err = w.Write(errBuf); err == nil { + if n < int(errorLength) { + err = io.ErrShortWrite + } + } + } + return +} + +func newFrame(t, f byte, seq uint16, data []byte) *Frame { + return &Frame{ + Feature: Feature, + Type: t, + Flag: f, + Seq: seq, + Data: data, + Timestamp: time.Now().Unix(), + } +} diff --git a/entry/cli/router.go b/entry/cli/router.go new file mode 100644 index 0000000..e0c4fb4 --- /dev/null +++ b/entry/cli/router.go @@ -0,0 +1,177 @@ +package cli + +import ( + "errors" + "fmt" + "github.com/mattn/go-runewidth" + "strconv" + "strings" +) + +var ( + ErrNotFound = errors.New("not found") +) + +type Router struct { + name string + path []string + children []*Router + command Command + params []string +} + +func (r *Router) getChildren(name string) *Router { + for _, child := range r.children { + if child.name == name { + return child + } + } + return nil +} + +func (r *Router) Completer(tokens ...string) []string { + ss := make([]string, 0, 10) + if len(tokens) == 0 { + for _, child := range r.children { + ss = append(ss, strings.Join(child.path, " ")) + } + return ss + } + children := r.getChildren(tokens[0]) + if children == nil { + token := tokens[0] + for _, child := range r.children { + if strings.HasPrefix(child.name, token) { + ss = append(ss, strings.Join(child.path, " ")) + } + } + return ss + } + return children.Completer(tokens[1:]...) +} + +func (r *Router) Usage() string { + if len(r.path) <= 0 { + return "" + } + var ( + sb strings.Builder + ) + sb.WriteString("Usage: ") + sb.WriteString(strings.Join(r.path, " ")) + if len(r.params) > 0 { + for _, s := range r.params { + sb.WriteString(" {" + s + "}") + } + } + return sb.String() +} + +func (r *Router) Handle(path string, command Command) { + var ( + pos int + name string + ) + if strings.HasSuffix(path, "/") { + path = strings.TrimSuffix(path, "/") + } + if strings.HasPrefix(path, "/") { + path = strings.TrimPrefix(path, "/") + } + if path == "" { + r.command = command + return + } + if path[0] == ':' { + ss := strings.Split(path, "/") + for _, s := range ss { + r.params = append(r.params, strings.TrimPrefix(s, ":")) + } + r.command = command + return + } + if pos = strings.IndexByte(path, '/'); pos > -1 { + name = path[:pos] + path = path[pos:] + } else { + name = path + path = "" + } + children := r.getChildren(name) + if children == nil { + children = newRouter(name) + if len(r.path) == 0 { + children.path = append(children.path, name) + } else { + children.path = append(children.path, r.path...) + children.path = append(children.path, name) + } + r.children = append(r.children, children) + } + if children.command.Handle != nil { + panic("a handle is already registered for path /" + strings.Join(children.path, "/")) + } + children.Handle(path, command) +} + +func (r *Router) Lookup(tokens []string) (router *Router, args []string, err error) { + if len(tokens) > 0 { + children := r.getChildren(tokens[0]) + if children != nil { + return children.Lookup(tokens[1:]) + } + } + if r.command.Handle == nil { + err = ErrNotFound + return + } + router = r + args = tokens + return +} + +func (r *Router) String() string { + var ( + sb strings.Builder + width int + maxWidth int + walkFunc func(router *Router) []commander + ) + walkFunc = func(router *Router) []commander { + vs := make([]commander, 0, 5) + if router.command.Handle != nil { + vs = append(vs, commander{ + Name: router.name, + Path: strings.Join(router.path, " "), + Description: router.command.Description, + }) + } else { + if len(router.children) > 0 { + for _, child := range router.children { + vs = append(vs, walkFunc(child)...) + } + } + } + return vs + } + vs := walkFunc(r) + for _, v := range vs { + width = runewidth.StringWidth(v.Path) + if width > maxWidth { + maxWidth = width + } + } + for _, v := range vs { + sb.WriteString(fmt.Sprintf("%-"+strconv.Itoa(maxWidth+4)+"s %s\n", v.Path, v.Description)) + } + return sb.String() +} + +func newRouter(name string) *Router { + return &Router{ + name: name, + path: make([]string, 0, 4), + params: make([]string, 0, 4), + children: make([]*Router, 0, 10), + } +} diff --git a/entry/cli/serialize.go b/entry/cli/serialize.go new file mode 100644 index 0000000..d6636fe --- /dev/null +++ b/entry/cli/serialize.go @@ -0,0 +1,250 @@ +package cli + +import ( + "bytes" + "encoding/json" + "fmt" + "git.nspix.com/golang/kos/util/pool" + "github.com/mattn/go-runewidth" + "reflect" + "strconv" + "strings" + "time" +) + +func isNormalKind(kind reflect.Kind) bool { + normalKinds := []reflect.Kind{ + reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, reflect.Int, + reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uint, + reflect.Float32, reflect.Float64, + reflect.String, + } + for _, k := range normalKinds { + if k == kind { + return true + } + } + return false +} + +func serializeMap(val map[any]any) ([]byte, error) { + var ( + canFormat bool + width int + maxWidth int + ) + canFormat = true + for k, v := range val { + if !isNormalKind(reflect.Indirect(reflect.ValueOf(k)).Kind()) || !isNormalKind(reflect.Indirect(reflect.ValueOf(v)).Kind()) { + canFormat = false + break + } + } + if !canFormat { + return json.MarshalIndent(val, "", "\t") + } + ms := make(map[string]string) + for k, v := range val { + sk := fmt.Sprint(k) + ms[sk] = fmt.Sprint(v) + width = runewidth.StringWidth(sk) + if width > maxWidth { + maxWidth = width + } + } + buffer := pool.GetBuffer() + defer pool.PutBuffer(buffer) + for k, v := range ms { + buffer.WriteString(fmt.Sprintf("%-"+strconv.Itoa(maxWidth+4)+"s %s\n", k, v)) + } + return buffer.Bytes(), nil +} + +func printBorder(w *bytes.Buffer, ws []int) { + for _, l := range ws { + w.WriteString("+") + w.WriteString(strings.Repeat("-", l+2)) + } + w.WriteString("+\n") +} + +func toString(v any) string { + switch t := v.(type) { + case float32, float64: + return fmt.Sprintf("%.2f", t) + case time.Time: + return t.Format("2006-01-02 15:04:05") + default: + return fmt.Sprint(v) + } +} + +func printArray(vals [][]any) (buf []byte) { + var ( + cell string + str string + widths []int + maxLength int + width int + rows [][]string + ) + rows = make([][]string, 0, len(vals)) + for _, value := range vals { + if len(value) > maxLength { + maxLength = len(value) + } + } + widths = make([]int, maxLength) + for _, vs := range vals { + rl := len(vs) + row := make([]string, rl) + for i, val := range vs { + str = toString(val) + if rl > 1 { + width = runewidth.StringWidth(str) + if width > widths[i] { + widths[i] = width + } + } + row[i] = str + } + rows = append(rows, row) + } + buffer := pool.GetBuffer() + defer pool.PutBuffer(buffer) + printBorder(buffer, widths) + for index, row := range rows { + size := len(row) + for i, w := range widths { + cell = "" + buffer.WriteString("|") + if size > i { + cell = row[i] + } + buffer.WriteString(" ") + buffer.WriteString(cell) + cl := runewidth.StringWidth(cell) + if w > cl { + buffer.WriteString(strings.Repeat(" ", w-cl)) + } + buffer.WriteString(" ") + } + buffer.WriteString("|\n") + if index == 0 { + printBorder(buffer, widths) + } + } + printBorder(buffer, widths) + return buffer.Bytes() +} + +func serializeArray(val []any) (buf []byte, err error) { + var ( + ok bool + vs [][]any + normalFormat bool + isArrayElement bool + isStructElement bool + columnName string + ) + normalFormat = true + for _, row := range val { + kind := reflect.Indirect(reflect.ValueOf(row)).Kind() + if !isNormalKind(kind) { + normalFormat = false + } + if kind == reflect.Array || kind == reflect.Slice { + isArrayElement = true + } + if kind == reflect.Struct { + isStructElement = true + } + } + if normalFormat { + goto __END + } + if isArrayElement { + vs = make([][]any, 0, len(val)) + for _, v := range val { + rv := reflect.Indirect(reflect.ValueOf(v)) + if rv.Kind() == reflect.Array || rv.Kind() == reflect.Slice { + row := make([]any, 0, rv.Len()) + for i := 0; i < rv.Len(); i++ { + if isNormalKind(rv.Index(i).Elem().Kind()) || rv.Index(i).Interface() == nil { + row = append(row, rv.Index(i).Interface()) + } else { + goto __END + } + } + vs = append(vs, row) + } else { + goto __END + } + } + } + if isStructElement { + vs = make([][]any, 0, len(val)) + for i, v := range val { + rv := reflect.Indirect(reflect.ValueOf(v)) + if rv.Kind() == reflect.Struct { + if i == 0 { + row := make([]any, 0, rv.Type().NumField()) + for j := 0; j < rv.Type().NumField(); j++ { + st := rv.Type().Field(j).Tag + if columnName, ok = st.Lookup("name"); !ok { + columnName = strings.ToUpper(rv.Type().Field(j).Name) + } + row = append(row, columnName) + } + vs = append(vs, row) + } + row := make([]any, 0, rv.Type().NumField()) + for j := 0; j < rv.Type().NumField(); j++ { + row = append(row, rv.Field(j).Interface()) + } + vs = append(vs, row) + } else { + goto __END + } + } + } + buf = printArray(vs) + return +__END: + return json.MarshalIndent(val, "", "\t") +} + +func serialize(val any) (buf []byte, err error) { + var ( + refVal reflect.Value + ) + refVal = reflect.Indirect(reflect.ValueOf(val)) + switch refVal.Kind() { + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: + buf = []byte(strconv.FormatInt(refVal.Int(), 10)) + case reflect.Float32, reflect.Float64: + buf = []byte(strconv.FormatFloat(refVal.Float(), 'f', -1, 64)) + case reflect.String: + buf = []byte(refVal.String()) + case reflect.Slice, reflect.Array: + if refVal.Type().Elem().Kind() == reflect.Uint8 { + buf = refVal.Bytes() + } else { + as := make([]any, 0, refVal.Len()) + for i := 0; i < refVal.Len(); i++ { + as = append(as, refVal.Index(i).Interface()) + } + buf, err = serializeArray(as) + } + case reflect.Map: + ms := make(map[any]any) + keys := refVal.MapKeys() + for _, key := range keys { + ms[key.Interface()] = refVal.MapIndex(key).Interface() + } + buf, err = serializeMap(ms) + default: + buf, err = json.MarshalIndent(refVal.Interface(), "", "\t") + } + return +} diff --git a/entry/cli/server.go b/entry/cli/server.go new file mode 100644 index 0000000..7f16c9b --- /dev/null +++ b/entry/cli/server.go @@ -0,0 +1,201 @@ +package cli + +import ( + "context" + "errors" + "fmt" + "git.nspix.com/golang/kos/util/env" + "github.com/sourcegraph/conc" + "net" + "path" + "runtime" + "strings" + "sync" + "sync/atomic" + "time" +) + +var ( + ctxPool sync.Pool +) + +type Server struct { + ctx context.Context + sequence int64 + ctxMap sync.Map + waitGroup conc.WaitGroup + middleware []Middleware + router *Router + l net.Listener +} + +func (svr *Server) applyContext() *Context { + if v := ctxPool.Get(); v != nil { + if ctx, ok := v.(*Context); ok { + return ctx + } + } + return &Context{} +} + +func (svr *Server) releaseContext(ctx *Context) { + ctxPool.Put(ctx) +} + +func (svr *Server) handle(ctx *Context, frame *Frame) { + var ( + err error + params map[string]string + tokens []string + args []string + r *Router + ) + cmd := string(frame.Data) + tokens = strings.Fields(cmd) + if r, args, err = svr.router.Lookup(tokens); err != nil { + if errors.Is(err, ErrNotFound) { + err = ctx.Error(errNotFound, fmt.Sprintf("Command %s not found", cmd)) + } else { + err = ctx.Error(errExecuteFailed, err.Error()) + } + } else { + if len(r.params) > len(args) { + err = ctx.Error(errExecuteFailed, r.Usage()) + return + } + if len(r.params) > 0 { + params = make(map[string]string) + for i, s := range r.params { + params[s] = args[i] + } + } + ctx.setArgs(args) + ctx.setParam(params) + err = r.command.Handle(ctx) + } +} + +func (svr *Server) process(conn net.Conn) { + var ( + err error + ctx *Context + frame *Frame + ) + ctx = svr.applyContext() + ctx.reset(atomic.AddInt64(&svr.sequence, 1), conn) + svr.ctxMap.Store(ctx.Id, ctx) + defer func() { + _ = conn.Close() + svr.ctxMap.Delete(ctx.Id) + svr.releaseContext(ctx) + }() + for { + if frame, err = readFrame(conn); err != nil { + break + } + //reset frame + ctx.seq = frame.Seq + switch frame.Type { + case PacketTypeHandshake: + if err = ctx.send(responsePayload{ + Type: PacketTypeHandshake, + Data: &Info{ + ID: ctx.Id, + Name: env.Get("VOX_NAME", ""), + Version: env.Get("VOX_VERSION", ""), + OS: runtime.GOOS, + ServerTime: time.Now(), + RemoteAddr: conn.RemoteAddr().String(), + }, + }); err != nil { + break + } + case PacketTypeCompleter: + if err = ctx.send(responsePayload{ + Type: PacketTypeCompleter, + Data: svr.router.Completer(strings.Fields(string(frame.Data))...), + }); err != nil { + break + } + case PacketTypeCommand: + svr.handle(ctx, frame) + default: + break + } + } +} + +func (svr *Server) serve() { + for { + conn, err := svr.l.Accept() + if err != nil { + break + } + svr.waitGroup.Go(func() { + svr.process(conn) + }) + } +} + +func (svr *Server) wrapHandle(pathname, desc string, cb HandleFunc, middleware ...Middleware) Command { + h := func(ctx *Context) (err error) { + for i := len(svr.middleware) - 1; i >= 0; i-- { + cb = svr.middleware[i](cb) + } + for i := len(middleware) - 1; i >= 0; i-- { + cb = middleware[i](cb) + } + return cb(ctx) + } + if desc == "" { + desc = strings.Join(strings.Split(strings.TrimPrefix(pathname, "/"), "/"), " ") + } + return Command{ + Path: pathname, + Handle: h, + Description: desc, + } +} + +func (svr *Server) Use(middleware ...Middleware) { + svr.middleware = append(svr.middleware, middleware...) +} + +func (svr *Server) Group(prefix string, commands []Command, middleware ...Middleware) { + for _, cmd := range commands { + svr.Handle(path.Join(prefix, cmd.Path), cmd.Description, cmd.Handle, middleware...) + } +} + +func (svr *Server) Handle(pathname string, desc string, cb HandleFunc, middleware ...Middleware) { + svr.router.Handle(pathname, svr.wrapHandle(pathname, desc, cb, middleware...)) +} + +func (svr *Server) Serve(l net.Listener) (err error) { + svr.l = l + svr.Handle("/help", "Display help information", func(ctx *Context) (err error) { + return ctx.Success(svr.router.String()) + }) + svr.serve() + return +} + +func (svr *Server) Shutdown() (err error) { + err = svr.l.Close() + svr.ctxMap.Range(func(key, value any) bool { + if ctx, ok := value.(*Context); ok { + err = ctx.Close() + } + return true + }) + svr.waitGroup.Wait() + return +} + +func New(ctx context.Context) *Server { + return &Server{ + ctx: ctx, + router: newRouter(""), + middleware: make([]Middleware, 0, 10), + } +} diff --git a/entry/cli/types.go b/entry/cli/types.go new file mode 100644 index 0000000..6115d6d --- /dev/null +++ b/entry/cli/types.go @@ -0,0 +1,46 @@ +package cli + +import "time" + +type Param struct { + Key string + Value string +} + +type Params []Param + +type HandleFunc func(ctx *Context) (err error) + +type Middleware func(next HandleFunc) HandleFunc + +type responsePayload struct { + Type uint8 `json:"-"` + Code int `json:"code"` + Reason string `json:"reason,omitempty"` + Data any `json:"data,omitempty"` +} + +type Info struct { + ID int64 `json:"id"` + OS string `json:"os"` + Name string `json:"name"` + Version string `json:"version"` + ServerTime time.Time `json:"server_time"` + RemoteAddr string `json:"remote_addr"` +} + +type Command struct { + Path string + Handle HandleFunc + Description string +} + +type commander struct { + Name string + Path string + Description string +} + +type encoder interface { + Marshal() ([]byte, error) +} diff --git a/entry/conn.go b/entry/conn.go new file mode 100644 index 0000000..ad2f747 --- /dev/null +++ b/entry/conn.go @@ -0,0 +1,72 @@ +package entry + +import ( + "net" + "sync/atomic" + "time" +) + +type Conn struct { + buf []byte + conn net.Conn + exitFlag int32 + state *State +} + +func (c *Conn) Read(b []byte) (n int, err error) { + var m int + if len(c.buf) > 0 { + if len(b) >= len(c.buf) { + m = copy(b[:], c.buf[:]) + c.buf = c.buf[m:] + } + } + n, err = c.conn.Read(b[m:]) + n += m + atomic.AddInt64(&c.state.Traffic.In, int64(n)) + return +} + +func (c *Conn) Write(b []byte) (n int, err error) { + n, err = c.conn.Write(b) + atomic.AddInt64(&c.state.Traffic.Out, int64(n)) + return +} + +func (c *Conn) Close() error { + if atomic.CompareAndSwapInt32(&c.exitFlag, 0, 1) { + atomic.AddInt32(&c.state.Concurrency, -1) + atomic.AddInt64(&c.state.Request.Processed, 1) + return c.conn.Close() + } + return nil +} + +func (c *Conn) LocalAddr() net.Addr { + return c.conn.LocalAddr() +} + +func (c *Conn) RemoteAddr() net.Addr { + return c.conn.RemoteAddr() +} + +func (c *Conn) SetDeadline(t time.Time) error { + return c.conn.SetDeadline(t) +} + +func (c *Conn) SetReadDeadline(t time.Time) error { + return c.conn.SetReadDeadline(t) +} + +func (c *Conn) SetWriteDeadline(t time.Time) error { + return c.conn.SetWriteDeadline(t) +} + +func wrapConn(c net.Conn, state *State, buf []byte) net.Conn { + conn := &Conn{conn: c, buf: buf, state: state} + if buf != nil { + conn.buf = make([]byte, len(buf)) + copy(conn.buf, buf) + } + return conn +} diff --git a/entry/gateway.go b/entry/gateway.go new file mode 100644 index 0000000..d8ef4c9 --- /dev/null +++ b/entry/gateway.go @@ -0,0 +1,191 @@ +package entry + +import ( + "bytes" + "context" + "errors" + "github.com/sourcegraph/conc" + "io" + "net" + "sync/atomic" + "time" +) + +const ( + minFeatureLength = 3 +) + +var ( + ErrShortFeature = errors.New("short feature") + ErrInvalidListener = errors.New("invalid listener") +) + +type ( + Feature []byte + + listenerEntity struct { + feature Feature + listener *Listener + } + + Gateway struct { + ctx context.Context + cancelFunc context.CancelCauseFunc + l net.Listener + ch chan net.Conn + address string + state *State + waitGroup conc.WaitGroup + listeners []*listenerEntity + exitFlag int32 + } +) + +func (gw *Gateway) handle(conn net.Conn) { + var ( + n int + err error + successed int32 + feature = make([]byte, minFeatureLength) + ) + atomic.AddInt32(&gw.state.Concurrency, 1) + defer func() { + if atomic.LoadInt32(&successed) != 1 { + atomic.AddInt32(&gw.state.Concurrency, -1) + atomic.AddInt64(&gw.state.Request.Discarded, 1) + _ = conn.Close() + } + }() + //set deadline + if err = conn.SetReadDeadline(time.Now().Add(time.Second * 30)); err != nil { + return + } + //read feature + if n, err = io.ReadFull(conn, feature); err != nil { + return + } + //reset deadline + if err = conn.SetReadDeadline(time.Time{}); err != nil { + return + } + for _, l := range gw.listeners { + if bytes.Compare(feature[:n], l.feature[:n]) == 0 { + atomic.StoreInt32(&successed, 1) + l.listener.Receive(wrapConn(conn, gw.state, feature[:n])) + return + } + } +} + +func (gw *Gateway) accept() { + atomic.StoreInt32(&gw.state.Accepting, 1) + defer func() { + atomic.StoreInt32(&gw.state.Accepting, 0) + }() + for { + if conn, err := gw.l.Accept(); err != nil { + break + } else { + select { + case gw.ch <- conn: + atomic.AddInt64(&gw.state.Request.Total, 1) + case <-gw.ctx.Done(): + return + } + } + } +} + +func (gw *Gateway) worker() { + atomic.StoreInt32(&gw.state.Processing, 1) + defer func() { + atomic.StoreInt32(&gw.state.Processing, 0) + }() + for { + select { + case <-gw.ctx.Done(): + return + case conn, ok := <-gw.ch: + if ok { + gw.handle(conn) + } + } + } +} + +func (gw *Gateway) Bind(feature Feature, listener net.Listener) (err error) { + var ( + ok bool + ls *Listener + ) + if len(feature) < minFeatureLength { + return ErrShortFeature + } + if ls, ok = listener.(*Listener); !ok { + return ErrInvalidListener + } + for _, l := range gw.listeners { + if bytes.Compare(l.feature, feature) == 0 { + l.listener = ls + return + } + } + gw.listeners = append(gw.listeners, &listenerEntity{ + feature: feature, + listener: ls, + }) + return +} + +func (gw *Gateway) Apply(feature ...Feature) (listener net.Listener, err error) { + listener = newListener(gw.l.Addr()) + for _, code := range feature { + if len(code) < minFeatureLength { + continue + } + err = gw.Bind(code, listener) + } + return listener, nil +} + +func (gw *Gateway) Release(feature Feature) { + for i, l := range gw.listeners { + if bytes.Compare(l.feature, feature) == 0 { + gw.listeners = append(gw.listeners[:i], gw.listeners[i+1:]...) + } + } +} + +func (gw *Gateway) State() *State { + return gw.state +} + +func (gw *Gateway) Start(ctx context.Context) (err error) { + gw.ctx, gw.cancelFunc = context.WithCancelCause(ctx) + if gw.l, err = net.Listen("tcp", gw.address); err != nil { + return + } + gw.waitGroup.Go(gw.worker) + gw.waitGroup.Go(gw.accept) + return +} + +func (gw *Gateway) Stop() (err error) { + if !atomic.CompareAndSwapInt32(&gw.exitFlag, 0, 1) { + return + } + gw.cancelFunc(io.ErrClosedPipe) + err = gw.l.Close() + gw.waitGroup.Wait() + close(gw.ch) + return +} + +func New(address string) *Gateway { + gw := &Gateway{ + address: address, + state: &State{}, + ch: make(chan net.Conn, 10), + } + return gw +} diff --git a/entry/http/bind.go b/entry/http/bind.go new file mode 100644 index 0000000..7699fa0 --- /dev/null +++ b/entry/http/bind.go @@ -0,0 +1,271 @@ +package http + +import ( + "encoding/json" + "encoding/xml" + "errors" + "fmt" + "net/http" + "reflect" + "strconv" + "strings" +) + +type ( + // Binder is the interface that wraps the Bind method. + Binder interface { + Bind(i interface{}, req *http.Request) error + } + + // DefaultBinder is the default implementation of the Binder interface. + DefaultBinder struct{} + + // BindUnmarshaler is the interface used to wrap the UnmarshalParam method. + BindUnmarshaler interface { + // UnmarshalParam decodes and assigns a value from an form or query param. + UnmarshalParam(param string) error + } +) + +// Bind implements the `Binder#Bind` function. +func (b *DefaultBinder) Bind(i any, req *http.Request) (err error) { + if req.ContentLength == 0 { + if req.Method == http.MethodGet || req.Method == http.MethodDelete { + if err = b.bindData(i, req.URL.Query(), "query"); err != nil { + return err + } + return + } + return errors.New("request body can't be empty") + } + ctype := req.Header.Get("Content-Type") + switch { + case strings.HasPrefix(ctype, "application/json"): + if err = json.NewDecoder(req.Body).Decode(i); err != nil { + if ute, ok := err.(*json.UnmarshalTypeError); ok { + return fmt.Errorf("unmarshal type error: expected=%v, got=%v, field=%v, offset=%v", ute.Type, ute.Value, ute.Field, ute.Offset) + } else if se, ok := err.(*json.SyntaxError); ok { + return fmt.Errorf("syntax error: offset=%v, error=%v", se.Offset, se.Error()) + } else { + return err + } + } + case strings.HasPrefix(ctype, "application/xml"), strings.HasPrefix(ctype, "text/xml"): + if err = xml.NewDecoder(req.Body).Decode(i); err != nil { + if ute, ok := err.(*xml.UnsupportedTypeError); ok { + return fmt.Errorf("unsupported type error: type=%v, error=%v", ute.Type, ute.Error()) + } else if se, ok := err.(*xml.SyntaxError); ok { + return fmt.Errorf("syntax error: line=%v, error=%v", se.Line, se.Error()) + } else { + return err + } + } + case strings.HasPrefix(ctype, "application/x-www-form-urlencoded"), strings.HasPrefix(ctype, "multipart/form-data"): + if strings.HasPrefix(req.Header.Get("Content-Type"), "multipart/form-data") { + if err := req.ParseMultipartForm(32 << 20); err != nil { + return err + } + } else { + if err := req.ParseForm(); err != nil { + return err + } + } + if err = b.bindData(i, req.Form, "form"); err != nil { + return err + } + default: + return errors.New("unsupported formatter") + } + return +} + +func (b *DefaultBinder) bindData(ptr interface{}, data map[string][]string, tag string) error { + typ := reflect.TypeOf(ptr).Elem() + val := reflect.ValueOf(ptr).Elem() + if typ.Kind() != reflect.Struct { + return errors.New("binding element must be a struct") + } + for i := 0; i < typ.NumField(); i++ { + typeField := typ.Field(i) + structField := val.Field(i) + if !structField.CanSet() { + continue + } + structFieldKind := structField.Kind() + inputFieldName := typeField.Tag.Get(tag) + if inputFieldName == "" { + inputFieldName = typeField.Name + // If tag is nil, we inspect if the field is a struct. + if _, ok := bindUnmarshaler(structField); !ok && structFieldKind == reflect.Struct { + if err := b.bindData(structField.Addr().Interface(), data, tag); err != nil { + return err + } + continue + } + } + inputValue, exists := data[inputFieldName] + if !exists { + // Go json.Unmarshal supports case insensitive binding. However the + // url params are bound case sensitive which is inconsistent. To + // fix this we must check all of the map values in a + // case-insensitive search. + inputFieldName = strings.ToLower(inputFieldName) + for k, v := range data { + if strings.ToLower(k) == inputFieldName { + inputValue = v + exists = true + break + } + } + } + if !exists { + continue + } + // Call this first, in case we're dealing with an alias to an array type + if ok, err := unmarshalField(typeField.Type.Kind(), inputValue[0], structField); ok { + if err != nil { + return err + } + continue + } + numElems := len(inputValue) + if structFieldKind == reflect.Slice && numElems > 0 { + sliceOf := structField.Type().Elem().Kind() + slice := reflect.MakeSlice(structField.Type(), numElems, numElems) + for j := 0; j < numElems; j++ { + if err := setWithProperType(sliceOf, inputValue[j], slice.Index(j)); err != nil { + return err + } + } + val.Field(i).Set(slice) + } else if err := setWithProperType(typeField.Type.Kind(), inputValue[0], structField); err != nil { + return err + + } + } + return nil +} + +func setWithProperType(valueKind reflect.Kind, val string, structField reflect.Value) error { + // But also call it here, in case we're dealing with an array of BindUnmarshalers + if ok, err := unmarshalField(valueKind, val, structField); ok { + return err + } + switch valueKind { + case reflect.Ptr: + return setWithProperType(structField.Elem().Kind(), val, structField.Elem()) + case reflect.Int: + return setIntField(val, 0, structField) + case reflect.Int8: + return setIntField(val, 8, structField) + case reflect.Int16: + return setIntField(val, 16, structField) + case reflect.Int32: + return setIntField(val, 32, structField) + case reflect.Int64: + return setIntField(val, 64, structField) + case reflect.Uint: + return setUintField(val, 0, structField) + case reflect.Uint8: + return setUintField(val, 8, structField) + case reflect.Uint16: + return setUintField(val, 16, structField) + case reflect.Uint32: + return setUintField(val, 32, structField) + case reflect.Uint64: + return setUintField(val, 64, structField) + case reflect.Bool: + return setBoolField(val, structField) + case reflect.Float32: + return setFloatField(val, 32, structField) + case reflect.Float64: + return setFloatField(val, 64, structField) + case reflect.String: + structField.SetString(val) + default: + return errors.New("unknown type") + } + return nil +} + +func unmarshalField(valueKind reflect.Kind, val string, field reflect.Value) (bool, error) { + switch valueKind { + case reflect.Ptr: + return unmarshalFieldPtr(val, field) + default: + return unmarshalFieldNonPtr(val, field) + } +} + +// bindUnmarshaler attempts to unmarshal a reflect.Value into a BindUnmarshaler +func bindUnmarshaler(field reflect.Value) (BindUnmarshaler, bool) { + ptr := reflect.New(field.Type()) + if ptr.CanInterface() { + iface := ptr.Interface() + if unmarshaler, ok := iface.(BindUnmarshaler); ok { + return unmarshaler, ok + } + } + return nil, false +} + +func unmarshalFieldNonPtr(value string, field reflect.Value) (bool, error) { + if unmarshaler, ok := bindUnmarshaler(field); ok { + err := unmarshaler.UnmarshalParam(value) + field.Set(reflect.ValueOf(unmarshaler).Elem()) + return true, err + } + return false, nil +} + +func unmarshalFieldPtr(value string, field reflect.Value) (bool, error) { + if field.IsNil() { + // Initialize the pointer to a nil value + field.Set(reflect.New(field.Type().Elem())) + } + return unmarshalFieldNonPtr(value, field.Elem()) +} + +func setIntField(value string, bitSize int, field reflect.Value) error { + if value == "" { + value = "0" + } + intVal, err := strconv.ParseInt(value, 10, bitSize) + if err == nil { + field.SetInt(intVal) + } + return err +} + +func setUintField(value string, bitSize int, field reflect.Value) error { + if value == "" { + value = "0" + } + uintVal, err := strconv.ParseUint(value, 10, bitSize) + if err == nil { + field.SetUint(uintVal) + } + return err +} + +func setBoolField(value string, field reflect.Value) error { + if value == "" { + value = "false" + } + boolVal, err := strconv.ParseBool(value) + if err == nil { + field.SetBool(boolVal) + } + return err +} + +func setFloatField(value string, bitSize int, field reflect.Value) error { + if value == "" { + value = "0.0" + } + floatVal, err := strconv.ParseFloat(value, bitSize) + if err == nil { + field.SetFloat(floatVal) + } + return err +} diff --git a/entry/http/context.go b/entry/http/context.go new file mode 100644 index 0000000..55b06fb --- /dev/null +++ b/entry/http/context.go @@ -0,0 +1,107 @@ +package http + +import ( + "context" + "encoding/json" + "net/http" + "os" + "path" + "strings" +) + +var ( + defaultBinder = &DefaultBinder{} +) + +type Context struct { + ctx context.Context + req *http.Request + res http.ResponseWriter + params map[string]string + statusCode int +} + +func (ctx *Context) reset(req *http.Request, res http.ResponseWriter, ps map[string]string) { + ctx.statusCode = http.StatusOK + ctx.req, ctx.res, ctx.params = req, res, ps +} + +func (ctx *Context) Request() *http.Request { + return ctx.req +} + +func (ctx *Context) Response() http.ResponseWriter { + return ctx.res +} + +func (ctx *Context) Context() context.Context { + if ctx.Request().Context() != nil { + return ctx.Request().Context() + } + return ctx.ctx +} + +func (ctx *Context) Bind(v any) (err error) { + return defaultBinder.Bind(v, ctx.Request()) +} + +func (ctx *Context) Query(k string) string { + return ctx.Request().FormValue(k) +} + +func (ctx *Context) Param(k string) string { + var ( + ok bool + v string + ) + if v, ok = ctx.params[k]; ok { + return v + } + return ctx.Request().FormValue(k) +} + +func (ctx *Context) send(res responsePayload) (err error) { + ctx.Response().Header().Set("Content-Type", "application/json") + encoder := json.NewEncoder(ctx.Response()) + if strings.HasPrefix(ctx.Request().Header.Get("User-Agent"), "curl") { + encoder.SetIndent("", "\t") + } + return encoder.Encode(res) +} + +func (ctx *Context) Success(v any) (err error) { + return ctx.send(responsePayload{Data: v}) +} + +func (ctx *Context) Status(code int) { + ctx.statusCode = code +} + +func (ctx *Context) Error(code int, reason string) (err error) { + return ctx.send(responsePayload{Code: code, Reason: reason}) +} + +func (ctx *Context) Redirect(url string, code int) { + if code != http.StatusFound && code != http.StatusMovedPermanently { + code = http.StatusMovedPermanently + } + http.Redirect(ctx.Response(), ctx.Request(), url, code) +} + +func (ctx *Context) SetCookie(cookie *http.Cookie) { + http.SetCookie(ctx.Response(), cookie) +} + +func (ctx *Context) SendFile(filename string) (err error) { + var ( + fi os.FileInfo + fp *os.File + ) + if fi, err = os.Stat(filename); err == nil { + if fp, err = os.Open(filename); err == nil { + http.ServeContent(ctx.Response(), ctx.Request(), path.Base(filename), fi.ModTime(), fp) + err = fp.Close() + } + } + return +} diff --git a/entry/http/handle.go b/entry/http/handle.go new file mode 100644 index 0000000..b22aef0 --- /dev/null +++ b/entry/http/handle.go @@ -0,0 +1,30 @@ +package http + +import ( + "encoding/json" + "fmt" + "net/http" +) + +type ( + NotFound struct { + } + NotAllowed struct { + } +) + +func (n NotFound) ServeHTTP(writer http.ResponseWriter, request *http.Request) { + writer.WriteHeader(http.StatusNotFound) + json.NewEncoder(writer).Encode(responsePayload{ + Code: http.StatusNotFound, + Reason: fmt.Sprintf("requested URL %s was not found on this server", request.URL.Path), + }) +} + +func (n NotAllowed) ServeHTTP(writer http.ResponseWriter, request *http.Request) { + writer.WriteHeader(http.StatusMethodNotAllowed) + json.NewEncoder(writer).Encode(responsePayload{ + Code: http.StatusMethodNotAllowed, + Reason: fmt.Sprintf("%s URL %s was not allow on this server", request.Method, request.URL.Path), + }) +} diff --git a/entry/http/method.go b/entry/http/method.go new file mode 100644 index 0000000..3111430 --- /dev/null +++ b/entry/http/method.go @@ -0,0 +1,15 @@ +package http + +import "net/http" + +const ( + MethodGet = http.MethodGet + MethodHead = http.MethodHead + MethodPost = http.MethodPost + MethodPut = http.MethodPut + MethodPatch = http.MethodPatch // RFC 5789 + MethodDelete = http.MethodDelete + MethodConnect = http.MethodConnect + MethodOptions = http.MethodOptions + MethodTrace = http.MethodTrace +) diff --git a/entry/http/router/path.go b/entry/http/router/path.go new file mode 100644 index 0000000..90bcb7c --- /dev/null +++ b/entry/http/router/path.go @@ -0,0 +1,145 @@ +package router + +// CleanPath is the URL version of path.Clean, it returns a canonical URL path +// for p, eliminating . and .. elements. +// +// The following rules are applied iteratively until no further processing can +// be done: +// 1. Replace multiple slashes with a single slash. +// 2. Eliminate each . path name element (the current directory). +// 3. Eliminate each inner .. path name element (the parent directory) +// along with the non-.. element that precedes it. +// 4. Eliminate .. elements that begin a rooted path: +// that is, replace "/.." by "/" at the beginning of a path. +// +// If the result of this process is an empty string, "/" is returned +func CleanPath(p string) string { + const stackBufSize = 128 + + // Turn empty string into "/" + if p == "" { + return "/" + } + + // Reasonably sized buffer on stack to avoid allocations in the common case. + // If a larger buffer is required, it gets allocated dynamically. + buf := make([]byte, 0, stackBufSize) + + n := len(p) + + // Invariants: + // reading from path; r is index of next byte to process. + // writing to buf; w is index of next byte to write. + + // path must start with '/' + r := 1 + w := 1 + + if p[0] != '/' { + r = 0 + + if n+1 > stackBufSize { + buf = make([]byte, n+1) + } else { + buf = buf[:n+1] + } + buf[0] = '/' + } + + trailing := n > 1 && p[n-1] == '/' + + // A bit more clunky without a 'lazybuf' like the path package, but the loop + // gets completely inlined (bufApp calls). + // So in contrast to the path package this loop has no expensive function + // calls (except make, if needed). + + for r < n { + switch { + case p[r] == '/': + // empty path element, trailing slash is added after the end + r++ + + case p[r] == '.' && r+1 == n: + trailing = true + r++ + + case p[r] == '.' && p[r+1] == '/': + // . element + r += 2 + + case p[r] == '.' && p[r+1] == '.' && (r+2 == n || p[r+2] == '/'): + // .. element: remove to last / + r += 3 + + if w > 1 { + // can backtrack + w-- + + if len(buf) == 0 { + for w > 1 && p[w] != '/' { + w-- + } + } else { + for w > 1 && buf[w] != '/' { + w-- + } + } + } + + default: + // Real path element. + // Add slash if needed + if w > 1 { + bufApp(&buf, p, w, '/') + w++ + } + + // Copy element + for r < n && p[r] != '/' { + bufApp(&buf, p, w, p[r]) + w++ + r++ + } + } + } + + // Re-append trailing slash + if trailing && w > 1 { + bufApp(&buf, p, w, '/') + w++ + } + + // If the original string was not modified (or only shortened at the end), + // return the respective substring of the original string. + // Otherwise return a new string from the buffer. + if len(buf) == 0 { + return p[:w] + } + return string(buf[:w]) +} + +// Internal helper to lazily create a buffer if necessary. +// Calls to this function get inlined. +func bufApp(buf *[]byte, s string, w int, c byte) { + b := *buf + if len(b) == 0 { + // No modification of the original string so far. + // If the next character is the same as in the original string, we do + // not yet have to allocate a buffer. + if s[w] == c { + return + } + + // Otherwise use either the stack buffer, if it is large enough, or + // allocate a new buffer on the heap, and copy all previous characters. + if l := len(s); l > cap(b) { + *buf = make([]byte, len(s)) + } else { + *buf = (*buf)[:l] + } + b = *buf + + copy(b, s[:w]) + } + b[w] = c +} diff --git a/entry/http/router/router.go b/entry/http/router/router.go new file mode 100644 index 0000000..ebd662e --- /dev/null +++ b/entry/http/router/router.go @@ -0,0 +1,489 @@ +package router + +import ( + "context" + "net/http" + "strings" + "sync" +) + +// Handle is a function that can be registered to a route to handle HTTP +// requests. Like http.HandlerFunc, but has a third parameter for the values of +// wildcards (path variables). +type Handle func(http.ResponseWriter, *http.Request, Params) + +// Param is a single URL parameter, consisting of a key and a value. +type Param struct { + Key string + Value string +} + +// Params is a Param-slice, as returned by the router. +// The slice is ordered, the first URL parameter is also the first slice value. +// It is therefore safe to read values by the index. +type Params []Param + +// ByName returns the value of the first Param which key matches the given name. +// If no matching Param is found, an empty string is returned. +func (ps Params) ByName(name string) string { + for _, p := range ps { + if p.Key == name { + return p.Value + } + } + return "" +} + +type paramsKey struct{} + +// ParamsKey is the request context key under which URL params are stored. +var ParamsKey = paramsKey{} + +// ParamsFromContext pulls the URL parameters from a request context, +// or returns nil if none are present. +func ParamsFromContext(ctx context.Context) Params { + p, _ := ctx.Value(ParamsKey).(Params) + return p +} + +// MatchedRoutePathParam is the Param name under which the path of the matched +// route is stored, if Router.SaveMatchedRoutePath is set. +var MatchedRoutePathParam = "$matchedRoutePath" + +// MatchedRoutePath retrieves the path of the matched route. +// Router.SaveMatchedRoutePath must have been enabled when the respective +// handler was added, otherwise this function always returns an empty string. +func (ps Params) MatchedRoutePath() string { + return ps.ByName(MatchedRoutePathParam) +} + +// Router is a http.Handler which can be used to dispatch requests to different +// handler functions via configurable routes +type Router struct { + trees map[string]*node + + paramsPool sync.Pool + maxParams uint16 + + // If enabled, adds the matched route path onto the http.Request context + // before invoking the handler. + // The matched route path is only added to handlers of routes that were + // registered when this option was enabled. + SaveMatchedRoutePath bool + + // Enables automatic redirection if the current route can't be matched but a + // handler for the path with (without) the trailing slash exists. + // For example if /foo/ is requested but a route only exists for /foo, the + // client is redirected to /foo with http status code 301 for GET requests + // and 308 for all other request methods. + RedirectTrailingSlash bool + + // If enabled, the router tries to fix the current request path, if no + // handle is registered for it. + // First superfluous path elements like ../ or // are removed. + // Afterwards the router does a case-insensitive lookup of the cleaned path. + // If a handle can be found for this route, the router makes a redirection + // to the corrected path with status code 301 for GET requests and 308 for + // all other request methods. + // For example /FOO and /..//Foo could be redirected to /foo. + // RedirectTrailingSlash is independent of this option. + RedirectFixedPath bool + + // If enabled, the router checks if another method is allowed for the + // current route, if the current request can not be routed. + // If this is the case, the request is answered with 'Method Not Allowed' + // and HTTP status code 405. + // If no other Method is allowed, the request is delegated to the NotFound + // handler. + HandleMethodNotAllowed bool + + // If enabled, the router automatically replies to OPTIONS requests. + // Custom OPTIONS handlers take priority over automatic replies. + HandleOPTIONS bool + + // An optional http.Handler that is called on automatic OPTIONS requests. + // The handler is only called if HandleOPTIONS is true and no OPTIONS + // handler for the specific path was set. + // The "Allowed" header is set before calling the handler. + GlobalOPTIONS http.Handler + + // Cached value of global (*) allowed methods + globalAllowed string + + // Configurable http.Handler which is called when no matching route is + // found. If it is not set, http.NotFound is used. + NotFound http.Handler + + // Configurable http.Handler which is called when a request + // cannot be routed and HandleMethodNotAllowed is true. + // If it is not set, http.Error with http.StatusMethodNotAllowed is used. + // The "Allow" header with allowed request methods is set before the handler + // is called. + MethodNotAllowed http.Handler + + // Function to handle panics recovered from http handlers. + // It should be used to generate a error page and return the http error code + // 500 (Internal Server Error). + // The handler can be used to keep your server from crashing because of + // unrecovered panics. + PanicHandler func(http.ResponseWriter, *http.Request, interface{}) +} + +// Make sure the Router conforms with the http.Handler interface +var _ http.Handler = New() + +// New returns a new initialized Router. +// Path auto-correction, including trailing slashes, is enabled by default. +func New() *Router { + return &Router{ + RedirectTrailingSlash: true, + RedirectFixedPath: true, + HandleMethodNotAllowed: true, + HandleOPTIONS: true, + } +} + +func (r *Router) getParams() *Params { + ps, _ := r.paramsPool.Get().(*Params) + *ps = (*ps)[0:0] // reset slice + return ps +} + +func (r *Router) putParams(ps *Params) { + if ps != nil { + r.paramsPool.Put(ps) + } +} + +func (r *Router) saveMatchedRoutePath(path string, handle Handle) Handle { + return func(w http.ResponseWriter, req *http.Request, ps Params) { + if ps == nil { + psp := r.getParams() + ps = (*psp)[0:1] + ps[0] = Param{Key: MatchedRoutePathParam, Value: path} + handle(w, req, ps) + r.putParams(psp) + } else { + ps = append(ps, Param{Key: MatchedRoutePathParam, Value: path}) + handle(w, req, ps) + } + } +} + +// GET is a shortcut for router.Handle(http.MethodGet, path, handle) +func (r *Router) GET(path string, handle Handle) { + r.Handle(http.MethodGet, path, handle) +} + +// HEAD is a shortcut for router.Handle(http.MethodHead, path, handle) +func (r *Router) HEAD(path string, handle Handle) { + r.Handle(http.MethodHead, path, handle) +} + +// OPTIONS is a shortcut for router.Handle(http.MethodOptions, path, handle) +func (r *Router) OPTIONS(path string, handle Handle) { + r.Handle(http.MethodOptions, path, handle) +} + +// POST is a shortcut for router.Handle(http.MethodPost, path, handle) +func (r *Router) POST(path string, handle Handle) { + r.Handle(http.MethodPost, path, handle) +} + +// PUT is a shortcut for router.Handle(http.MethodPut, path, handle) +func (r *Router) PUT(path string, handle Handle) { + r.Handle(http.MethodPut, path, handle) +} + +// PATCH is a shortcut for router.Handle(http.MethodPatch, path, handle) +func (r *Router) PATCH(path string, handle Handle) { + r.Handle(http.MethodPatch, path, handle) +} + +// DELETE is a shortcut for router.Handle(http.MethodDelete, path, handle) +func (r *Router) DELETE(path string, handle Handle) { + r.Handle(http.MethodDelete, path, handle) +} + +// Replace registers or replace request handle with the given path and method. +// +// For GET, POST, PUT, PATCH and DELETE requests the respective shortcut +// functions can be used. +// +// This function is intended for bulk loading and to allow the usage of less +// frequently used, non-standardized or custom methods (e.g. for internal +// communication with a proxy). +func (r *Router) Replace(method, path string, handle Handle) { + r.addRoute(method, path, true, handle) +} + +// Handle registers a new request handle with the given path and method. +// +// For GET, POST, PUT, PATCH and DELETE requests the respective shortcut +// functions can be used. +// +// This function is intended for bulk loading and to allow the usage of less +// frequently used, non-standardized or custom methods (e.g. for internal +// communication with a proxy). +func (r *Router) Handle(method, path string, handle Handle) { + r.addRoute(method, path, false, handle) +} + +// addRoute registers a new request handle with the given path and method. +// +// For GET, POST, PUT, PATCH and DELETE requests the respective shortcut +// functions can be used. +// +// This function is intended for bulk loading and to allow the usage of less +// frequently used, non-standardized or custom methods (e.g. for internal +// communication with a proxy). +func (r *Router) addRoute(method, path string, replace bool, handle Handle) { + varsCount := uint16(0) + + if method == "" { + panic("method must not be empty") + } + if len(path) < 1 || path[0] != '/' { + panic("path must begin with '/' in path '" + path + "'") + } + if handle == nil { + panic("handle must not be nil") + } + + if r.SaveMatchedRoutePath { + varsCount++ + handle = r.saveMatchedRoutePath(path, handle) + } + + if r.trees == nil { + r.trees = make(map[string]*node) + } + + root := r.trees[method] + if root == nil { + root = new(node) + r.trees[method] = root + + r.globalAllowed = r.allowed("*", "") + } + + root.addRoute(path, handle, replace) + + // Update maxParams + if paramsCount := countParams(path); paramsCount+varsCount > r.maxParams { + r.maxParams = paramsCount + varsCount + } + + // Lazy-init paramsPool alloc func + if r.paramsPool.New == nil && r.maxParams > 0 { + r.paramsPool.New = func() interface{} { + ps := make(Params, 0, r.maxParams) + return &ps + } + } +} + +// Handler is an adapter which allows the usage of an http.Handler as a +// request handle. +// The Params are available in the request context under ParamsKey. +func (r *Router) Handler(method, path string, handler http.Handler) { + r.Handle(method, path, + func(w http.ResponseWriter, req *http.Request, p Params) { + if len(p) > 0 { + ctx := req.Context() + ctx = context.WithValue(ctx, ParamsKey, p) + req = req.WithContext(ctx) + } + handler.ServeHTTP(w, req) + }, + ) +} + +// HandlerFunc is an adapter which allows the usage of an http.HandlerFunc as a +// request handle. +func (r *Router) HandlerFunc(method, path string, handler http.HandlerFunc) { + r.Handler(method, path, handler) +} + +// ServeFiles serves files from the given file system root. +// The path must end with "/*filepath", files are then served from the local +// path /defined/root/dir/*filepath. +// For example if root is "/etc" and *filepath is "passwd", the local file +// "/etc/passwd" would be served. +// Internally a http.FileServer is used, therefore http.NotFound is used instead +// of the Router's NotFound handler. +// To use the operating system's file system implementation, +// use http.Dir: +// +// router.ServeFiles("/src/*filepath", http.Dir("/var/www")) +func (r *Router) ServeFiles(path string, root http.FileSystem) { + if len(path) < 10 || path[len(path)-10:] != "/*filepath" { + panic("path must end with /*filepath in path '" + path + "'") + } + + fileServer := http.FileServer(root) + + r.GET(path, func(w http.ResponseWriter, req *http.Request, ps Params) { + req.URL.Path = ps.ByName("filepath") + fileServer.ServeHTTP(w, req) + }) +} + +func (r *Router) recv(w http.ResponseWriter, req *http.Request) { + if rcv := recover(); rcv != nil { + r.PanicHandler(w, req, rcv) + } +} + +// Lookup allows the manual lookup of a method + path combo. +// This is e.g. useful to build a framework around this router. +// If the path was found, it returns the handle function and the path parameter +// values. Otherwise the third return value indicates whether a redirection to +// the same path with an extra / without the trailing slash should be performed. +func (r *Router) Lookup(method, path string) (Handle, Params, bool) { + if root := r.trees[method]; root != nil { + handle, ps, tsr := root.getValue(path, r.getParams) + if handle == nil { + r.putParams(ps) + return nil, nil, tsr + } + if ps == nil { + return handle, nil, tsr + } + return handle, *ps, tsr + } + return nil, nil, false +} + +func (r *Router) allowed(path, reqMethod string) (allow string) { + allowed := make([]string, 0, 9) + + if path == "*" { // server-wide + // empty method is used for internal calls to refresh the cache + if reqMethod == "" { + for method := range r.trees { + if method == http.MethodOptions { + continue + } + // Add request method to list of allowed methods + allowed = append(allowed, method) + } + } else { + return r.globalAllowed + } + } else { // specific path + for method := range r.trees { + // Skip the requested method - we already tried this one + if method == reqMethod || method == http.MethodOptions { + continue + } + + handle, _, _ := r.trees[method].getValue(path, nil) + if handle != nil { + // Add request method to list of allowed methods + allowed = append(allowed, method) + } + } + } + + if len(allowed) > 0 { + // Add request method to list of allowed methods + allowed = append(allowed, http.MethodOptions) + + // Sort allowed methods. + // sort.Strings(allowed) unfortunately causes unnecessary allocations + // due to allowed being moved to the heap and interface conversion + for i, l := 1, len(allowed); i < l; i++ { + for j := i; j > 0 && allowed[j] < allowed[j-1]; j-- { + allowed[j], allowed[j-1] = allowed[j-1], allowed[j] + } + } + + // return as comma separated list + return strings.Join(allowed, ", ") + } + + return allow +} + +// ServeHTTP makes the router implement the http.Handler interface. +func (r *Router) ServeHTTP(w http.ResponseWriter, req *http.Request) { + if r.PanicHandler != nil { + defer r.recv(w, req) + } + + path := req.URL.Path + + if root := r.trees[req.Method]; root != nil { + if handle, ps, tsr := root.getValue(path, r.getParams); handle != nil { + if ps != nil { + handle(w, req, *ps) + r.putParams(ps) + } else { + handle(w, req, nil) + } + return + } else if req.Method != http.MethodConnect && path != "/" { + // Moved Permanently, request with GET method + code := http.StatusMovedPermanently + if req.Method != http.MethodGet { + // Permanent Redirect, request with same method + code = http.StatusPermanentRedirect + } + + if tsr && r.RedirectTrailingSlash { + if len(path) > 1 && path[len(path)-1] == '/' { + req.URL.Path = path[:len(path)-1] + } else { + req.URL.Path = path + "/" + } + http.Redirect(w, req, req.URL.String(), code) + return + } + + // Try to fix the request path + if r.RedirectFixedPath { + fixedPath, found := root.findCaseInsensitivePath( + CleanPath(path), + r.RedirectTrailingSlash, + ) + if found { + req.URL.Path = fixedPath + http.Redirect(w, req, req.URL.String(), code) + return + } + } + } + } + + if req.Method == http.MethodOptions && r.HandleOPTIONS { + // Handle OPTIONS requests + if allow := r.allowed(path, http.MethodOptions); allow != "" { + w.Header().Set("Allow", allow) + if r.GlobalOPTIONS != nil { + r.GlobalOPTIONS.ServeHTTP(w, req) + } + return + } + } else if r.HandleMethodNotAllowed { // Handle 405 + if allow := r.allowed(path, req.Method); allow != "" { + w.Header().Set("Allow", allow) + if r.MethodNotAllowed != nil { + r.MethodNotAllowed.ServeHTTP(w, req) + } else { + http.Error(w, + http.StatusText(http.StatusMethodNotAllowed), + http.StatusMethodNotAllowed, + ) + } + return + } + } + + // Handle 404 + if r.NotFound != nil { + r.NotFound.ServeHTTP(w, req) + } else { + http.NotFound(w, req) + } +} diff --git a/entry/http/router/tree.go b/entry/http/router/tree.go new file mode 100644 index 0000000..8edce9f --- /dev/null +++ b/entry/http/router/tree.go @@ -0,0 +1,683 @@ +package router + +import ( + "strings" + "unicode" + "unicode/utf8" +) + +func min(a, b int) int { + if a <= b { + return a + } + return b +} + +func longestCommonPrefix(a, b string) int { + i := 0 + max := min(len(a), len(b)) + for i < max && a[i] == b[i] { + i++ + } + return i +} + +// Search for a wildcard segment and check the name for invalid characters. +// Returns -1 as index, if no wildcard was found. +func findWildcard(path string) (wilcard string, i int, valid bool) { + // Find start + for start, c := range []byte(path) { + // A wildcard starts with ':' (param) or '*' (catch-all) + if c != ':' && c != '*' { + continue + } + + // Find end and check for invalid characters + valid = true + for end, c := range []byte(path[start+1:]) { + switch c { + case '/': + return path[start : start+1+end], start, valid + case ':', '*': + valid = false + } + } + return path[start:], start, valid + } + return "", -1, false +} + +func countParams(path string) uint16 { + var n uint + for i := range []byte(path) { + switch path[i] { + case ':', '*': + n++ + } + } + return uint16(n) +} + +type nodeType uint8 + +const ( + static nodeType = iota // default + root + param + catchAll +) + +type node struct { + path string + indices string + wildChild bool + nType nodeType + priority uint32 + children []*node + handle Handle +} + +// Increments priority of the given child and reorders if necessary +func (n *node) incrementChildPrio(pos int) int { + cs := n.children + cs[pos].priority++ + prio := cs[pos].priority + + // Adjust position (move to front) + newPos := pos + for ; newPos > 0 && cs[newPos-1].priority < prio; newPos-- { + // Swap node positions + cs[newPos-1], cs[newPos] = cs[newPos], cs[newPos-1] + } + + // Build new index char string + if newPos != pos { + n.indices = n.indices[:newPos] + // Unchanged prefix, might be empty + n.indices[pos:pos+1] + // The index char we move + n.indices[newPos:pos] + n.indices[pos+1:] // Rest without char at 'pos' + } + + return newPos +} + +// addRoute adds a node with the given handle to the path. +// Not concurrency-safe! +func (n *node) addRoute(path string, handle Handle, replace bool) { + fullPath := path + n.priority++ + + // Empty tree + if n.path == "" && n.indices == "" { + n.insertChild(path, fullPath, handle) + n.nType = root + return + } + +walk: + for { + // Find the longest common prefix. + // This also implies that the common prefix contains no ':' or '*' + // since the existing key can't contain those chars. + i := longestCommonPrefix(path, n.path) + + // Split edge + if i < len(n.path) { + child := node{ + path: n.path[i:], + wildChild: n.wildChild, + nType: static, + indices: n.indices, + children: n.children, + handle: n.handle, + priority: n.priority - 1, + } + + n.children = []*node{&child} + // []byte for proper unicode char conversion, see #65 + n.indices = string([]byte{n.path[i]}) + n.path = path[:i] + n.handle = nil + n.wildChild = false + } + + // Make new node a child of this node + if i < len(path) { + path = path[i:] + + if n.wildChild { + n = n.children[0] + n.priority++ + + // Check if the wildcard matches + if len(path) >= len(n.path) && n.path == path[:len(n.path)] && + // Adding a child to a catchAll is not possible + n.nType != catchAll && + // Check for longer wildcard, e.g. :name and :names + (len(n.path) >= len(path) || path[len(n.path)] == '/') { + continue walk + } else { + // Wildcard conflict + pathSeg := path + if n.nType != catchAll { + pathSeg = strings.SplitN(pathSeg, "/", 2)[0] + } + prefix := fullPath[:strings.Index(fullPath, pathSeg)] + n.path + panic("'" + pathSeg + + "' in new path '" + fullPath + + "' conflicts with existing wildcard '" + n.path + + "' in existing prefix '" + prefix + + "'") + } + } + + idxc := path[0] + + // '/' after param + if n.nType == param && idxc == '/' && len(n.children) == 1 { + n = n.children[0] + n.priority++ + continue walk + } + + // Check if a child with the next path byte exists + for i, c := range []byte(n.indices) { + if c == idxc { + i = n.incrementChildPrio(i) + n = n.children[i] + continue walk + } + } + + // Otherwise insert it + if idxc != ':' && idxc != '*' { + // []byte for proper unicode char conversion, see #65 + n.indices += string([]byte{idxc}) + child := &node{} + n.children = append(n.children, child) + n.incrementChildPrio(len(n.indices) - 1) + n = child + } + n.insertChild(path, fullPath, handle) + return + } + + // Otherwise add handle to current node + if !replace { + if n.handle != nil { + panic("a handle is already registered for path '" + fullPath + "'") + } + } + n.handle = handle + return + } +} + +func (n *node) insertChild(path, fullPath string, handle Handle) { + for { + // Find prefix until first wildcard + wildcard, i, valid := findWildcard(path) + if i < 0 { // No wilcard found + break + } + + // The wildcard name must not contain ':' and '*' + if !valid { + panic("only one wildcard per path segment is allowed, has: '" + + wildcard + "' in path '" + fullPath + "'") + } + + // Check if the wildcard has a name + if len(wildcard) < 2 { + panic("wildcards must be named with a non-empty name in path '" + fullPath + "'") + } + + // Check if this node has existing children which would be + // unreachable if we insert the wildcard here + if len(n.children) > 0 { + panic("wildcard segment '" + wildcard + + "' conflicts with existing children in path '" + fullPath + "'") + } + + // param + if wildcard[0] == ':' { + if i > 0 { + // Insert prefix before the current wildcard + n.path = path[:i] + path = path[i:] + } + + n.wildChild = true + child := &node{ + nType: param, + path: wildcard, + } + n.children = []*node{child} + n = child + n.priority++ + + // If the path doesn't end with the wildcard, then there + // will be another non-wildcard subpath starting with '/' + if len(wildcard) < len(path) { + path = path[len(wildcard):] + child := &node{ + priority: 1, + } + n.children = []*node{child} + n = child + continue + } + + // Otherwise we're done. Insert the handle in the new leaf + n.handle = handle + return + } + + // catchAll + if i+len(wildcard) != len(path) { + panic("catch-all routes are only allowed at the end of the path in path '" + fullPath + "'") + } + + if len(n.path) > 0 && n.path[len(n.path)-1] == '/' { + panic("catch-all conflicts with existing handle for the path segment root in path '" + fullPath + "'") + } + + // Currently fixed width 1 for '/' + i-- + if path[i] != '/' { + panic("no / before catch-all in path '" + fullPath + "'") + } + + n.path = path[:i] + + // First node: catchAll node with empty path + child := &node{ + wildChild: true, + nType: catchAll, + } + n.children = []*node{child} + n.indices = string('/') + n = child + n.priority++ + + // Second node: node holding the variable + child = &node{ + path: path[i:], + nType: catchAll, + handle: handle, + priority: 1, + } + n.children = []*node{child} + + return + } + + // If no wildcard was found, simply insert the path and handle + n.path = path + n.handle = handle +} + +// Returns the handle registered with the given path (key). The values of +// wildcards are saved to a map. +// If no handle can be found, a TSR (trailing slash redirect) recommendation is +// made if a handle exists with an extra (without the) trailing slash for the +// given path. +func (n *node) getValue(path string, params func() *Params) (handle Handle, ps *Params, tsr bool) { +walk: // Outer loop for walking the tree + for { + prefix := n.path + if len(path) > len(prefix) { + if path[:len(prefix)] == prefix { + path = path[len(prefix):] + + // If this node does not have a wildcard (param or catchAll) + // child, we can just look up the next child node and continue + // to walk down the tree + if !n.wildChild { + idxc := path[0] + for i, c := range []byte(n.indices) { + if c == idxc { + n = n.children[i] + continue walk + } + } + + // Nothing found. + // We can recommend to redirect to the same URL without a + // trailing slash if a leaf exists for that path. + tsr = (path == "/" && n.handle != nil) + return + } + + // Handle wildcard child + n = n.children[0] + switch n.nType { + case param: + // Find param end (either '/' or path end) + end := 0 + for end < len(path) && path[end] != '/' { + end++ + } + + // Save param value + if params != nil { + if ps == nil { + ps = params() + } + // Expand slice within preallocated capacity + i := len(*ps) + *ps = (*ps)[:i+1] + (*ps)[i] = Param{ + Key: n.path[1:], + Value: path[:end], + } + } + + // We need to go deeper! + if end < len(path) { + if len(n.children) > 0 { + path = path[end:] + n = n.children[0] + continue walk + } + + // ... but we can't + tsr = (len(path) == end+1) + return + } + + if handle = n.handle; handle != nil { + return + } else if len(n.children) == 1 { + // No handle found. Check if a handle for this path + a + // trailing slash exists for TSR recommendation + n = n.children[0] + tsr = (n.path == "/" && n.handle != nil) || (n.path == "" && n.indices == "/") + } + + return + + case catchAll: + // Save param value + if params != nil { + if ps == nil { + ps = params() + } + // Expand slice within preallocated capacity + i := len(*ps) + *ps = (*ps)[:i+1] + (*ps)[i] = Param{ + Key: n.path[2:], + Value: path, + } + } + + handle = n.handle + return + + default: + panic("invalid node type") + } + } + } else if path == prefix { + // We should have reached the node containing the handle. + // Check if this node has a handle registered. + if handle = n.handle; handle != nil { + return + } + + // If there is no handle for this route, but this route has a + // wildcard child, there must be a handle for this path with an + // additional trailing slash + if path == "/" && n.wildChild && n.nType != root { + tsr = true + return + } + + if path == "/" && n.nType == static { + tsr = true + return + } + + // No handle found. Check if a handle for this path + a + // trailing slash exists for trailing slash recommendation + for i, c := range []byte(n.indices) { + if c == '/' { + n = n.children[i] + tsr = (len(n.path) == 1 && n.handle != nil) || + (n.nType == catchAll && n.children[0].handle != nil) + return + } + } + return + } + + // Nothing found. We can recommend to redirect to the same URL with an + // extra trailing slash if a leaf exists for that path + tsr = (path == "/") || + (len(prefix) == len(path)+1 && prefix[len(path)] == '/' && + path == prefix[:len(prefix)-1] && n.handle != nil) + return + } +} + +// Makes a case-insensitive lookup of the given path and tries to find a handler. +// It can optionally also fix trailing slashes. +// It returns the case-corrected path and a bool indicating whether the lookup +// was successful. +func (n *node) findCaseInsensitivePath(path string, fixTrailingSlash bool) (fixedPath string, found bool) { + const stackBufSize = 128 + + // Use a static sized buffer on the stack in the common case. + // If the path is too long, allocate a buffer on the heap instead. + buf := make([]byte, 0, stackBufSize) + if l := len(path) + 1; l > stackBufSize { + buf = make([]byte, 0, l) + } + + ciPath := n.findCaseInsensitivePathRec( + path, + buf, // Preallocate enough memory for new path + [4]byte{}, // Empty rune buffer + fixTrailingSlash, + ) + + return string(ciPath), ciPath != nil +} + +// Shift bytes in array by n bytes left +func shiftNRuneBytes(rb [4]byte, n int) [4]byte { + switch n { + case 0: + return rb + case 1: + return [4]byte{rb[1], rb[2], rb[3], 0} + case 2: + return [4]byte{rb[2], rb[3]} + case 3: + return [4]byte{rb[3]} + default: + return [4]byte{} + } +} + +// Recursive case-insensitive lookup function used by n.findCaseInsensitivePath +func (n *node) findCaseInsensitivePathRec(path string, ciPath []byte, rb [4]byte, fixTrailingSlash bool) []byte { + npLen := len(n.path) + +walk: // Outer loop for walking the tree + for len(path) >= npLen && (npLen == 0 || strings.EqualFold(path[1:npLen], n.path[1:])) { + // Add common prefix to result + oldPath := path + path = path[npLen:] + ciPath = append(ciPath, n.path...) + + if len(path) > 0 { + // If this node does not have a wildcard (param or catchAll) child, + // we can just look up the next child node and continue to walk down + // the tree + if !n.wildChild { + // Skip rune bytes already processed + rb = shiftNRuneBytes(rb, npLen) + + if rb[0] != 0 { + // Old rune not finished + idxc := rb[0] + for i, c := range []byte(n.indices) { + if c == idxc { + // continue with child node + n = n.children[i] + npLen = len(n.path) + continue walk + } + } + } else { + // Process a new rune + var rv rune + + // Find rune start. + // Runes are up to 4 byte long, + // -4 would definitely be another rune. + var off int + for max := min(npLen, 3); off < max; off++ { + if i := npLen - off; utf8.RuneStart(oldPath[i]) { + // read rune from cached path + rv, _ = utf8.DecodeRuneInString(oldPath[i:]) + break + } + } + + // Calculate lowercase bytes of current rune + lo := unicode.ToLower(rv) + utf8.EncodeRune(rb[:], lo) + + // Skip already processed bytes + rb = shiftNRuneBytes(rb, off) + + idxc := rb[0] + for i, c := range []byte(n.indices) { + // Lowercase matches + if c == idxc { + // must use a recursive approach since both the + // uppercase byte and the lowercase byte might exist + // as an index + if out := n.children[i].findCaseInsensitivePathRec( + path, ciPath, rb, fixTrailingSlash, + ); out != nil { + return out + } + break + } + } + + // If we found no match, the same for the uppercase rune, + // if it differs + if up := unicode.ToUpper(rv); up != lo { + utf8.EncodeRune(rb[:], up) + rb = shiftNRuneBytes(rb, off) + + idxc := rb[0] + for i, c := range []byte(n.indices) { + // Uppercase matches + if c == idxc { + // Continue with child node + n = n.children[i] + npLen = len(n.path) + continue walk + } + } + } + } + + // Nothing found. We can recommend to redirect to the same URL + // without a trailing slash if a leaf exists for that path + if fixTrailingSlash && path == "/" && n.handle != nil { + return ciPath + } + return nil + } + + n = n.children[0] + switch n.nType { + case param: + // Find param end (either '/' or path end) + end := 0 + for end < len(path) && path[end] != '/' { + end++ + } + + // Add param value to case insensitive path + ciPath = append(ciPath, path[:end]...) + + // We need to go deeper! + if end < len(path) { + if len(n.children) > 0 { + // Continue with child node + n = n.children[0] + npLen = len(n.path) + path = path[end:] + continue + } + + // ... but we can't + if fixTrailingSlash && len(path) == end+1 { + return ciPath + } + return nil + } + + if n.handle != nil { + return ciPath + } else if fixTrailingSlash && len(n.children) == 1 { + // No handle found. Check if a handle for this path + a + // trailing slash exists + n = n.children[0] + if n.path == "/" && n.handle != nil { + return append(ciPath, '/') + } + } + return nil + + case catchAll: + return append(ciPath, path...) + + default: + panic("invalid node type") + } + } else { + // We should have reached the node containing the handle. + // Check if this node has a handle registered. + if n.handle != nil { + return ciPath + } + + // No handle found. + // Try to fix the path by adding a trailing slash + if fixTrailingSlash { + for i, c := range []byte(n.indices) { + if c == '/' { + n = n.children[i] + if (len(n.path) == 1 && n.handle != nil) || + (n.nType == catchAll && n.children[0].handle != nil) { + return append(ciPath, '/') + } + return nil + } + } + } + return nil + } + } + + // Nothing found. + // Try to fix the path by adding / removing a trailing slash + if fixTrailingSlash { + if path == "/" { + return ciPath + } + if len(path)+1 == npLen && n.path[len(path)] == '/' && + strings.EqualFold(path[1:], n.path[1:len(path)]) && n.handle != nil { + return append(ciPath, n.path...) + } + } + return nil +} diff --git a/entry/http/server.go b/entry/http/server.go new file mode 100644 index 0000000..e6ec8c3 --- /dev/null +++ b/entry/http/server.go @@ -0,0 +1,183 @@ +package http + +import ( + "context" + "embed" + "git.nspix.com/golang/kos/entry/http/router" + "net" + "net/http" + "path" + "strings" + "sync" +) + +var ( + ctxPool sync.Pool +) + +type Server struct { + ctx context.Context + serve *http.Server + router *router.Router + middleware []Middleware +} + +func (svr *Server) applyContext() *Context { + if v := ctxPool.Get(); v != nil { + if ctx, ok := v.(*Context); ok { + return ctx + } + } + return &Context{} +} + +func (svr *Server) releaseContext(ctx *Context) { + ctxPool.Put(ctx) +} + +func (svr *Server) wrapHandle(cb HandleFunc, middleware ...Middleware) router.Handle { + return func(writer http.ResponseWriter, request *http.Request, params router.Params) { + ctx := svr.applyContext() + defer func() { + svr.releaseContext(ctx) + }() + ps := make(map[string]string) + for _, v := range params { + ps[v.Key] = v.Value + } + ctx.reset(request, writer, ps) + for i := len(svr.middleware) - 1; i >= 0; i-- { + cb = svr.middleware[i](cb) + } + for i := len(middleware) - 1; i >= 0; i-- { + cb = middleware[i](cb) + } + if err := cb(ctx); err != nil { + ctx.Status(http.StatusServiceUnavailable) + } + } +} + +func (svr *Server) Use(middleware ...Middleware) { + svr.middleware = append(svr.middleware, middleware...) +} + +func (svr *Server) Handle(method string, path string, cb HandleFunc, middleware ...Middleware) { + if method == "" { + method = http.MethodPost + } + if path == "" { + path = "/" + } + if !strings.HasPrefix(path, "/") { + path = "/" + path + } + svr.router.Replace(method, path, svr.wrapHandle(cb, middleware...)) +} + +func (svr *Server) Group(prefix string, routes []Route, middleware ...Middleware) { + for _, route := range routes { + svr.Handle(route.Method, path.Join(prefix, route.Path), route.Handle, middleware...) + } +} + +func (svr *Server) Embed(prefix string, root string, embedFs embed.FS) { + routePath := prefix + if !strings.HasSuffix(routePath, "/*filepath") { + if strings.HasSuffix(routePath, "/") { + routePath += "/" + } else { + routePath += "/*filepath" + } + } + httpFs := http.FS(embedFs) + svr.Handle(MethodGet, routePath, func(ctx *Context) (err error) { + filename := strings.TrimPrefix(ctx.Request().URL.Path, prefix) + if filename == "" || filename == "/" { + filename = root + "/" + if !strings.HasSuffix(filename, "/") { + filename = filename + "/" + } + } else { + if !strings.HasPrefix(filename, root) { + filename = path.Clean(path.Join(root, filename)) + } + } + if !strings.HasPrefix(filename, "/") { + filename = "/" + filename + } + ctx.Request().URL.Path = filename + http.FileServer(httpFs).ServeHTTP(ctx.Response(), ctx.Request()) + return + }) +} + +func (svr *Server) Static(path string, root http.FileSystem) { + if !strings.HasSuffix(path, "/*filepath") { + if strings.HasSuffix(path, "/") { + path += "/" + } else { + path += "/*filepath" + } + } + svr.router.ServeFiles(path, root) +} + +func (svr *Server) handleOption(res http.ResponseWriter, req *http.Request) { + res.Header().Add("Vary", "Origin") + res.Header().Add("Vary", "Access-Control-Request-Method") + res.Header().Add("Vary", "Access-Control-Request-Headers") + res.Header().Set("Access-Control-Allow-Origin", "*") + res.Header().Set("Access-Control-Allow-Credentials", "true") + res.Header().Set("Access-Control-Allow-Methods", "GET,HEAD,PUT,PATCH,POST,DELETE") + h := req.Header.Get("Access-Control-Request-Headers") + if h != "" { + res.Header().Set("Access-Control-Allow-Headers", h) + } + res.WriteHeader(http.StatusNoContent) +} + +func (svr *Server) handleRequest(res http.ResponseWriter, req *http.Request) { + res.Header().Add("Vary", "Origin") + res.Header().Set("Access-Control-Allow-Origin", "*") + res.Header().Set("Access-Control-Allow-Credentials", "true") + h := req.Header.Get("Access-Control-Request-Headers") + if h != "" { + res.Header().Set("Access-Control-Allow-Headers", h) + } + svr.router.ServeHTTP(res, req) +} + +func (svr *Server) ServeHTTP(writer http.ResponseWriter, request *http.Request) { + switch request.Method { + case http.MethodOptions: + svr.handleOption(writer, request) + default: + svr.handleRequest(writer, request) + } +} + +func (svr *Server) Serve(l net.Listener) (err error) { + svr.serve = &http.Server{ + Handler: svr, + } + svr.router.NotFound = NotFound{} + svr.router.MethodNotAllowed = NotAllowed{} + return svr.serve.Serve(l) +} + +func (svr *Server) Shutdown() (err error) { + if svr.serve != nil { + err = svr.serve.Shutdown(svr.ctx) + } + return +} + +func New(ctx context.Context) *Server { + svr := &Server{ + ctx: ctx, + router: router.New(), + middleware: make([]Middleware, 0, 10), + } + return svr +} diff --git a/entry/http/types.go b/entry/http/types.go new file mode 100644 index 0000000..7b6ebd6 --- /dev/null +++ b/entry/http/types.go @@ -0,0 +1,26 @@ +package http + +import "net/http" + +type responsePayload struct { + Code int `json:"code"` + Reason string `json:"reason,omitempty"` + Data any `json:"data,omitempty"` +} + +type HandleFunc func(ctx *Context) (err error) + +type Middleware func(next HandleFunc) HandleFunc + +type Route struct { + Method string + Path string + Handle HandleFunc +} + +func Wrap(f http.HandlerFunc) HandleFunc { + return func(ctx *Context) (err error) { + f(ctx.Response(), ctx.Request()) + return + } +} diff --git a/entry/listener.go b/entry/listener.go new file mode 100644 index 0000000..71553dc --- /dev/null +++ b/entry/listener.go @@ -0,0 +1,53 @@ +package entry + +import ( + "io" + "net" + "sync/atomic" +) + +type Listener struct { + addr net.Addr + ch chan net.Conn + exitFlag int32 + exitChan chan struct{} +} + +func (l *Listener) Receive(conn net.Conn) { + select { + case l.ch <- conn: + case <-l.exitChan: + } +} + +func (l *Listener) Accept() (net.Conn, error) { + select { + case conn, ok := <-l.ch: + if ok { + return conn, nil + } else { + return nil, io.ErrClosedPipe + } + case <-l.exitChan: + return nil, io.ErrClosedPipe + } +} + +func (l *Listener) Close() error { + if atomic.CompareAndSwapInt32(&l.exitFlag, 0, 1) { + close(l.exitChan) + } + return nil +} + +func (l *Listener) Addr() net.Addr { + return l.addr +} + +func newListener(addr net.Addr) *Listener { + return &Listener{ + addr: addr, + ch: make(chan net.Conn, 10), + exitChan: make(chan struct{}), + } +} diff --git a/entry/state.go b/entry/state.go new file mode 100644 index 0000000..a1f0abd --- /dev/null +++ b/entry/state.go @@ -0,0 +1,16 @@ +package entry + +type State struct { + Accepting int32 `json:"accepting"` //是否正在接收连接 + Processing int32 `json:"processing"` //是否正在处理连接 + Concurrency int32 `json:"concurrency"` + Request struct { + Total int64 `json:"total"` //总处理请求 + Processed int64 `json:"processed"` //处理完成的请求 + Discarded int64 `json:"discarded"` //丢弃的请求 + } `json:"request"` + Traffic struct { + In int64 `json:"in"` //入网流量 + Out int64 `json:"out"` //出网流量 + } `json:"traffic"` +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..10f7561 --- /dev/null +++ b/go.mod @@ -0,0 +1,12 @@ +module git.nspix.com/golang/kos + +go 1.20 + +require ( + github.com/mattn/go-runewidth v0.0.3 + github.com/patrickmn/go-cache v2.1.0+incompatible + github.com/peterh/liner v1.2.2 + github.com/sourcegraph/conc v0.3.0 +) + +require golang.org/x/sys v0.0.0-20211117180635-dee7805ff2e1 // indirect diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..2bad829 --- /dev/null +++ b/go.sum @@ -0,0 +1,14 @@ +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/mattn/go-runewidth v0.0.3 h1:a+kO+98RDGEfo6asOGMmpodZq4FNtnGP54yps8BzLR4= +github.com/mattn/go-runewidth v0.0.3/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= +github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc= +github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ= +github.com/peterh/liner v1.2.2 h1:aJ4AOodmL+JxOZZEL2u9iJf8omNRpqHc/EbrK+3mAXw= +github.com/peterh/liner v1.2.2/go.mod h1:xFwJyiKIXJZUKItq5dGHZSTBRAuG/CpeNpWLyiNRNwI= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo= +github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0= +github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= +golang.org/x/sys v0.0.0-20211117180635-dee7805ff2e1 h1:kwrAHlwJ0DUBZwQ238v+Uod/3eZ8B2K5rYsUHBQvzmI= +golang.org/x/sys v0.0.0-20211117180635-dee7805ff2e1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= diff --git a/instance.go b/instance.go new file mode 100644 index 0000000..3428979 --- /dev/null +++ b/instance.go @@ -0,0 +1,43 @@ +package kos + +import ( + "git.nspix.com/golang/kos/entry/cli" + "git.nspix.com/golang/kos/entry/http" + "sync" +) + +var ( + once sync.Once + std *application +) + +func initApplication(cbs ...Option) { + once.Do(func() { + std = New(cbs...) + }) +} + +func Init(cbs ...Option) *application { + initApplication(cbs...) + return std +} + +func Node() *Info { + initApplication() + return std.Info() +} + +func Http() *http.Server { + initApplication() + return std.Http() +} + +func Command() *cli.Server { + initApplication() + return std.Command() +} + +func Handle(method string, cb HandleFunc) { + initApplication() + std.Handle(method, cb) +} diff --git a/options.go b/options.go new file mode 100644 index 0000000..1b8236b --- /dev/null +++ b/options.go @@ -0,0 +1,80 @@ +package kos + +import ( + "context" + "git.nspix.com/golang/kos/util/env" + "git.nspix.com/golang/kos/util/ip" + "os" + "strings" + "syscall" +) + +type ( + Options struct { + Name string + Version string + Address string + Port int + EnableDebug bool //开启调试模式 + DisableHttp bool //禁用HTTP入口 + DisableCommand bool //禁用命令行入口 + DisableStateApi bool //禁用系统状态接口 + Metadata map[string]string //原数据 + Context context.Context + Signals []os.Signal + server Server + shortName string + } + + Option func(o *Options) +) + +func (o *Options) ShortName() string { + if o.shortName != "" { + return o.shortName + } + if pos := strings.LastIndex(o.Name, "/"); pos != -1 { + o.shortName = o.Name[pos+1:] + } else { + o.shortName = o.Name + } + return o.shortName +} + +func WithName(name string, version string) Option { + return func(o *Options) { + o.Name = name + o.Version = version + } +} + +func WithPort(port int) Option { + return func(o *Options) { + o.Port = port + } +} + +func WithServer(s Server) Option { + return func(o *Options) { + o.server = s + } +} + +func WithDebug() Option { + return func(o *Options) { + o.EnableDebug = true + } +} + +func NewOptions() *Options { + opts := &Options{ + Name: env.Get(EnvAppName, ""), + Version: env.Get(EnvAppVersion, "0.0.1"), + Context: context.Background(), + Metadata: make(map[string]string), + Signals: []os.Signal{syscall.SIGTERM, syscall.SIGINT, syscall.SIGQUIT, syscall.SIGKILL}, + } + opts.Port = int(env.Integer(EnvAppPort, 80)) + opts.Address = env.Get(EnvAppAddress, ip.Internal()) + return opts +} diff --git a/pkg/cache/cache.go b/pkg/cache/cache.go new file mode 100644 index 0000000..4307407 --- /dev/null +++ b/pkg/cache/cache.go @@ -0,0 +1,13 @@ +package cache + +import ( + "context" + "time" +) + +type Cache interface { + Set(ctx context.Context, key string, value any) + SetEx(ctx context.Context, key string, value any, expire time.Duration) + Get(ctx context.Context, key string) (value any, ok bool) + Del(ctx context.Context, key string) +} diff --git a/pkg/cache/instance.go b/pkg/cache/instance.go new file mode 100644 index 0000000..90c2396 --- /dev/null +++ b/pkg/cache/instance.go @@ -0,0 +1,38 @@ +package cache + +import ( + "context" + "time" +) + +var ( + std Cache +) + +func init() { + std = NewMemCache() +} + +func SetCache(l Cache) { + std = l +} + +func GetCache() Cache { + return std +} + +func Set(ctx context.Context, key string, value any) { + std.Set(ctx, key, value) +} + +func SetEx(ctx context.Context, key string, value any, expire time.Duration) { + std.SetEx(ctx, key, value, expire) +} + +func Get(ctx context.Context, key string) (value any, ok bool) { + return std.Get(ctx, key) +} + +func Del(ctx context.Context, key string) { + std.Del(ctx, key) +} diff --git a/pkg/cache/memcache.go b/pkg/cache/memcache.go new file mode 100644 index 0000000..a22631e --- /dev/null +++ b/pkg/cache/memcache.go @@ -0,0 +1,33 @@ +package cache + +import ( + "context" + "github.com/patrickmn/go-cache" + "time" +) + +type MemCache struct { + engine *cache.Cache +} + +func (cache *MemCache) Set(ctx context.Context, key string, value any) { + cache.engine.Set(key, value, 0) +} + +func (cache *MemCache) SetEx(ctx context.Context, key string, value any, expire time.Duration) { + cache.engine.Set(key, value, expire) +} + +func (cache *MemCache) Get(ctx context.Context, key string) (value any, ok bool) { + return cache.engine.Get(key) +} + +func (cache *MemCache) Del(ctx context.Context, key string) { + cache.engine.Delete(key) +} + +func NewMemCache() *MemCache { + return &MemCache{ + engine: cache.New(time.Hour, time.Minute*90), + } +} diff --git a/pkg/log/console.go b/pkg/log/console.go new file mode 100644 index 0000000..b53d77c --- /dev/null +++ b/pkg/log/console.go @@ -0,0 +1,118 @@ +package log + +import ( + "fmt" + "time" +) + +const ( + FG_BLACK int = 30 + FG_RED = 31 + FG_GREEN = 32 + FG_YELLOW = 33 + FG_BLUE = 34 + FG_PURPLE = 35 + FG_CYAN = 36 + FG_GREY = 37 +) + +type Console struct { + Level int + EnableColor int + prefix string +} + +func (log *Console) SetLevel(lv int) { + log.Level = lv +} + +func (log *Console) Prefix(s string) { + log.prefix = s +} + +func (log *Console) Print(i ...interface{}) { + log.write(TraceLevel, fmt.Sprint(i...)) +} + +func (log *Console) Printf(format string, args ...interface{}) { + log.write(TraceLevel, fmt.Sprintf(format, args...)) +} + +func (log *Console) Debug(i ...interface{}) { + log.write(DebugLevel, fmt.Sprint(i...)) +} + +func (log *Console) Debugf(format string, args ...interface{}) { + log.write(DebugLevel, fmt.Sprintf(format, args...)) +} + +func (log *Console) Info(i ...interface{}) { + log.write(InfoLevel, fmt.Sprint(i...)) +} + +func (log *Console) Infof(format string, args ...interface{}) { + log.write(InfoLevel, fmt.Sprintf(format, args...)) +} + +func (log *Console) Warn(i ...interface{}) { + log.write(WarnLevel, fmt.Sprint(i...)) +} + +func (log *Console) Warnf(format string, args ...interface{}) { + log.write(WarnLevel, fmt.Sprintf(format, args...)) +} + +func (log *Console) Error(i ...interface{}) { + log.write(ErrorLevel, fmt.Sprint(i...)) +} + +func (log *Console) Errorf(format string, args ...interface{}) { + log.write(ErrorLevel, fmt.Sprintf(format, args...)) +} + +func (log *Console) Fatal(i ...interface{}) { + log.write(FatalLevel, fmt.Sprint(i...)) +} + +func (log *Console) Fatalf(format string, args ...interface{}) { + log.write(FatalLevel, fmt.Sprintf(format, args...)) +} + +func (log *Console) Panic(i ...interface{}) { + log.write(PanicLevel, fmt.Sprint(i...)) +} + +func (log *Console) Panicf(format string, args ...interface{}) { + log.write(PanicLevel, fmt.Sprintf(format, args...)) +} + +func (log *Console) write(level int, s string) { + if log.Level > level { + return + } + lvColor := map[int]int{ + TraceLevel: FG_GREY, + DebugLevel: FG_BLUE, + InfoLevel: FG_GREEN, + WarnLevel: FG_PURPLE, + ErrorLevel: FG_RED, + FatalLevel: FG_RED, + PanicLevel: FG_RED, + } + var ls string + if log.EnableColor > 0 { + ls = fmt.Sprintf("\033[0m\033[%dm[%s]\033[0m", lvColor[level], getLevelText(level)) + } else { + ls = getLevelText(level) + } + if log.prefix != "" { + ls += " [" + log.prefix + "]" + } + fmt.Println(time.Now().Format("2006-01-02 15:04:05") + " " + ls + " " + s) +} + +func NewConsoleLogger() *Console { + return &Console{ + EnableColor: 1, + } +} diff --git a/pkg/log/file.go b/pkg/log/file.go new file mode 100644 index 0000000..d8c1887 --- /dev/null +++ b/pkg/log/file.go @@ -0,0 +1,251 @@ +package log + +import ( + "fmt" + "os" + "strconv" + "sync" + "time" +) + +type File struct { + Filename string `json:"filename"` + MaxSize int64 `json:"max_size"` + MaxLogFiles int `json:"max_log_files"` + Format string `json:"format"` + Level int `json:"level"` + mutex sync.RWMutex + buf []byte + fp *os.File + prefix string + size int64 +} + +func itoa(buf *[]byte, i int, wid int) { + // Assemble decimal in reverse order. + var b [20]byte + bp := len(b) - 1 + for i >= 10 || wid > 1 { + wid-- + q := i / 10 + b[bp] = byte('0' + i - q*10) + bp-- + i = q + } + // i < 10 + b[bp] = byte('0' + i) + *buf = append(*buf, b[bp:]...) +} + +func (lg *File) SetLevel(lv int) { + lg.Level = lv +} + +func (lg *File) Prefix(s string) { + lg.prefix = s +} + +func (lg *File) Print(i ...interface{}) { + lg.write(TraceLevel, fmt.Sprint(i...)) +} + +func (lg *File) Printf(format string, args ...interface{}) { + lg.write(TraceLevel, fmt.Sprintf(format, args...)) +} + +func (lg *File) Debug(i ...interface{}) { + lg.write(DebugLevel, fmt.Sprint(i...)) +} + +func (lg *File) Debugf(format string, args ...interface{}) { + lg.write(DebugLevel, fmt.Sprintf(format, args...)) +} + +func (lg *File) Info(i ...interface{}) { + lg.write(InfoLevel, fmt.Sprint(i...)) +} + +func (lg *File) Infof(format string, args ...interface{}) { + lg.write(InfoLevel, fmt.Sprintf(format, args...)) +} + +func (lg *File) Warn(i ...interface{}) { + lg.write(WarnLevel, fmt.Sprint(i...)) +} + +func (lg *File) Warnf(format string, args ...interface{}) { + lg.write(WarnLevel, fmt.Sprintf(format, args...)) +} + +func (lg *File) Error(i ...interface{}) { + lg.write(ErrorLevel, fmt.Sprint(i...)) +} + +func (lg *File) Errorf(format string, args ...interface{}) { + lg.write(ErrorLevel, fmt.Sprintf(format, args...)) +} + +func (lg *File) Fatal(i ...interface{}) { + lg.write(FatalLevel, fmt.Sprint(i...)) +} + +func (lg *File) Fatalf(format string, args ...interface{}) { + lg.write(FatalLevel, fmt.Sprintf(format, args...)) +} + +func (lg *File) Panic(i ...interface{}) { + lg.write(PanicLevel, fmt.Sprint(i...)) +} + +func (lg *File) Panicf(format string, args ...interface{}) { + lg.write(PanicLevel, fmt.Sprintf(format, args...)) +} + +func (lg *File) format(buf *[]byte, level int, s string) (err error) { + t := time.Now() + year, month, day := t.Date() + itoa(buf, year, 4) + *buf = append(*buf, '-') + itoa(buf, int(month), 2) + *buf = append(*buf, '-') + itoa(buf, day, 2) + *buf = append(*buf, ' ') + hour, min, sec := t.Clock() + itoa(buf, hour, 2) + *buf = append(*buf, ':') + itoa(buf, min, 2) + *buf = append(*buf, ':') + itoa(buf, sec, 2) + *buf = append(*buf, ' ') + *buf = append(*buf, '[') + *buf = append(*buf, getLevelText(level)...) + *buf = append(*buf, ']') + *buf = append(*buf, ' ') + *buf = append(*buf, s...) + return +} + +// Write 实现标准的写入行数 +func (lg *File) Write(p []byte) (n int, err error) { + lg.mutex.Lock() + defer lg.mutex.Unlock() + if n, err = lg.fp.Write(p); err != nil { + return + } + lg.size += int64(n) + if lg.MaxSize > 0 && lg.size >= lg.MaxSize { + if err = lg.rotate(); err != nil { + return + } + lg.size = 0 + } + return +} + +func (lg *File) write(level int, s string) { + var ( + n int + err error + ) + if lg.Level > level { + return + } + lg.mutex.Lock() + defer lg.mutex.Unlock() + lg.buf = lg.buf[:0] + if err = lg.format(&lg.buf, level, s); err != nil { + return + } + lg.buf = append(lg.buf, '\n') + if n, err = lg.fp.Write(lg.buf); err != nil { + return + } + lg.size += int64(n) + if lg.MaxSize > 0 && lg.size >= lg.MaxSize { + if err = lg.rotate(); err != nil { + return + } + lg.size = 0 + } +} + +func (lg *File) isExists(filename string) bool { + if _, err := os.Stat(filename); err == nil { + return true + } else { + return false + } +} + +// rotate 实现日志滚动处理 +func (lg *File) rotate() (err error) { + if err = lg.close(); err != nil { + return + } + for i := lg.MaxLogFiles; i >= 0; i-- { + filename := lg.Filename + if i > 0 { + filename += "." + strconv.Itoa(i) + } + if i == lg.MaxLogFiles { + if lg.isExists(filename) { + if err = os.Remove(filename); err != nil { + return + } + } + } else { + if lg.isExists(filename) { + if err = os.Rename(filename, lg.Filename+"."+strconv.Itoa(i+1)); err != nil { + return + } + } + } + } + err = lg.open() + return +} + +func (lg *File) Reload() (err error) { + lg.mutex.Lock() + defer lg.mutex.Unlock() + _ = lg.close() + err = lg.open() + return +} + +func (lg *File) open() (err error) { + if lg.fp, err = os.OpenFile(lg.Filename, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0600); err != nil { + return + } + return +} + +func (lg *File) Open() (err error) { + var ( + info os.FileInfo + ) + if err = lg.open(); err != nil { + return + } + if info, err = os.Stat(lg.Filename); err == nil { + lg.size = info.Size() + } + return +} + +func (lg *File) close() (err error) { + if lg.fp != nil { + err = lg.fp.Close() + } + return +} + +func (lg *File) Close() (err error) { + err = lg.close() + return +} + +func NewFileLogger(filename string) *File { + lg := &File{Filename: filename, buf: make([]byte, 1024)} + return lg +} diff --git a/pkg/log/instance.go b/pkg/log/instance.go new file mode 100644 index 0000000..724ae8e --- /dev/null +++ b/pkg/log/instance.go @@ -0,0 +1,81 @@ +package log + +var ( + std Logger +) + +func init() { + std = NewConsoleLogger() +} + +func SetLogger(l Logger) { + std = l +} + +func GetLogger() Logger { + return std +} + +func Prefix(s string) { + std.Prefix(s) +} + +func SetLevel(lv int) { + std.SetLevel(lv) +} + +func Print(args ...interface{}) { + std.Print(args...) +} + +func Printf(format string, args ...interface{}) { + std.Printf(format, args...) +} + +func Debug(args ...interface{}) { + std.Debug(args...) +} + +func Debugf(format string, args ...interface{}) { + std.Debugf(format, args...) +} + +func Info(args ...interface{}) { + std.Info(args...) +} + +func Infof(format string, args ...interface{}) { + std.Infof(format, args...) +} + +func Warn(args ...interface{}) { + std.Warn(args...) +} + +func Warnf(format string, args ...interface{}) { + std.Warnf(format, args...) +} + +func Error(args ...interface{}) { + std.Error(args...) +} + +func Errorf(format string, args ...interface{}) { + std.Errorf(format, args...) +} + +func Fatal(args ...interface{}) { + std.Fatal(args...) +} + +func Fatalf(format string, args ...interface{}) { + std.Fatalf(format, args...) +} + +func Panic(args ...interface{}) { + std.Panic(args...) +} + +func Panicf(format string, args ...interface{}) { + std.Panicf(format, args...) +} diff --git a/pkg/log/logger.go b/pkg/log/logger.go new file mode 100644 index 0000000..e12d9ae --- /dev/null +++ b/pkg/log/logger.go @@ -0,0 +1,80 @@ +package log + +import "strings" + +const ( + TraceLevel int = iota + 1 + DebugLevel + InfoLevel + WarnLevel + ErrorLevel + FatalLevel + PanicLevel +) + +var ( + lvText = map[int]string{ + TraceLevel: "TRACE", + DebugLevel: "DEBUG", + InfoLevel: "INFO", + WarnLevel: "WARN", + ErrorLevel: "ERROR", + FatalLevel: "FATAL", + PanicLevel: "PANIC", + } +) + +type Logger interface { + SetLevel(int) + Prefix(string) + Print(i ...interface{}) + Printf(format string, args ...interface{}) + Debug(i ...interface{}) + Debugf(format string, args ...interface{}) + Info(i ...interface{}) + Infof(format string, args ...interface{}) + Warn(i ...interface{}) + Warnf(format string, args ...interface{}) + Error(i ...interface{}) + Errorf(format string, args ...interface{}) + Fatal(i ...interface{}) + Fatalf(format string, args ...interface{}) + Panic(i ...interface{}) + Panicf(format string, args ...interface{}) +} + +func getLevelText(lv int) string { + if s, ok := lvText[lv]; ok { + return s + } else { + return "TRACE" + } +} + +func FormatLevel(lv int) string { + return getLevelText(lv) +} + +func ParseLevel(s string) (lv int) { + lv = DebugLevel + s = strings.ToUpper(s) + for k, v := range lvText { + if s == v { + lv = k + return + } + } + return +} + +func ParseLevelWithoutDefault(s string) (lv int) { + lv = -1 + s = strings.ToUpper(s) + for k, v := range lvText { + if s == v { + lv = k + return + } + } + return +} diff --git a/plugin.go b/plugin.go new file mode 100644 index 0000000..4652649 --- /dev/null +++ b/plugin.go @@ -0,0 +1,13 @@ +package kos + +import "context" + +type Plugin interface { + Name() string + Mount(ctx context.Context) (err error) + BeforeStart() (err error) + AfterStart() (err error) + BeforeStop() (err error) + AfterStop() (err error) + Umount() (err error) +} diff --git a/service.go b/service.go new file mode 100644 index 0000000..0c16291 --- /dev/null +++ b/service.go @@ -0,0 +1,347 @@ +package kos + +import ( + "context" + "errors" + "flag" + "fmt" + "git.nspix.com/golang/kos/entry" + "git.nspix.com/golang/kos/entry/cli" + "git.nspix.com/golang/kos/entry/http" + "git.nspix.com/golang/kos/pkg/log" + "git.nspix.com/golang/kos/util/env" + "github.com/sourcegraph/conc" + "net" + "net/http/pprof" + "os" + "os/signal" + "runtime" + "strconv" + "sync" + "sync/atomic" + "syscall" + "time" +) + +var ( + ErrStopping = errors.New("stopping") + + cliFlag = flag.Bool("cli", false, "Go application interactive mode") +) + +type ( + application struct { + ctx context.Context + cancelFunc context.CancelCauseFunc + opts *Options + gateway *entry.Gateway + http *http.Server + command *cli.Server + uptime time.Time + info *Info + plugins sync.Map + waitGroup conc.WaitGroup + exitFlag int32 + } +) + +func (app *application) Log() log.Logger { + return log.GetLogger() +} + +func (app *application) Healthy() string { + if atomic.LoadInt32(&app.gateway.State().Processing) == 1 && atomic.LoadInt32(&app.gateway.State().Accepting) == 1 { + return StateHealthy + } + if atomic.LoadInt32(&app.gateway.State().Processing) == 1 { + return StateNoAccepting + } + if atomic.LoadInt32(&app.gateway.State().Accepting) == 1 { + return StateNoProgress + } + return StateUnavailable +} + +func (app *application) Info() *Info { + return app.info +} + +func (app *application) Http() *http.Server { + return app.http +} + +func (app *application) Command() *cli.Server { + return app.command +} + +func (app *application) Handle(path string, cb HandleFunc) { + if app.http != nil { + app.http.Handle(http.MethodPost, path, func(ctx *http.Context) (err error) { + return cb(ctx) + }) + } + if app.command != nil { + app.command.Handle(path, "", func(ctx *cli.Context) (err error) { + return cb(ctx) + }) + } +} + +func (app *application) httpServe() (err error) { + var ( + l net.Listener + ) + app.http = http.New(app.ctx) + if l, err = app.gateway.Apply( + entry.Feature(http.MethodGet), + entry.Feature(http.MethodHead), + entry.Feature(http.MethodPost), + entry.Feature(http.MethodPut), + entry.Feature(http.MethodPatch), + entry.Feature(http.MethodDelete), + entry.Feature(http.MethodConnect), + entry.Feature(http.MethodOptions), + entry.Feature(http.MethodTrace), + ); err != nil { + return + } + if app.opts.EnableDebug { + app.http.Handle(http.MethodGet, "/debug/pprof/", http.Wrap(pprof.Index)) + app.http.Handle(http.MethodGet, "/debug/pprof/goroutine", http.Wrap(pprof.Index)) + app.http.Handle(http.MethodGet, "/debug/pprof/heap", http.Wrap(pprof.Index)) + app.http.Handle(http.MethodGet, "/debug/pprof/mutex", http.Wrap(pprof.Index)) + app.http.Handle(http.MethodGet, "/debug/pprof/threadcreate", http.Wrap(pprof.Index)) + app.http.Handle(http.MethodGet, "/debug/pprof/cmdline", http.Wrap(pprof.Cmdline)) + app.http.Handle(http.MethodGet, "/debug/pprof/profile", http.Wrap(pprof.Profile)) + app.http.Handle(http.MethodGet, "/debug/pprof/symbol", http.Wrap(pprof.Symbol)) + app.http.Handle(http.MethodGet, "/debug/pprof/trace", http.Wrap(pprof.Trace)) + } + timer := time.NewTimer(time.Millisecond * 200) + defer timer.Stop() + errChan := make(chan error, 1) + app.waitGroup.Go(func() { + select { + case errChan <- app.http.Serve(l): + log.Infof("http server closed") + } + }) + select { + case err = <-errChan: + case <-timer.C: + log.Infof("http server started") + } + return +} + +func (app *application) commandServe() (err error) { + var ( + l net.Listener + ) + app.command = cli.New(app.ctx) + if l, err = app.gateway.Apply( + cli.Feature, + ); err != nil { + return + } + timer := time.NewTimer(time.Millisecond * 200) + defer timer.Stop() + errChan := make(chan error, 1) + app.waitGroup.Go(func() { + select { + case errChan <- app.command.Serve(l): + log.Infof("command server closed") + } + }) + select { + case err = <-errChan: + case <-timer.C: + log.Infof("command server started") + } + return +} + +func (app *application) gotoInteractive() (err error) { + var ( + client *cli.Client + ) + client = cli.NewClient( + app.ctx, + net.JoinHostPort(app.opts.Address, strconv.Itoa(app.opts.Port)), + ) + return client.Shell() +} + +func (app *application) buildInfo() *Info { + info := &Info{ + ID: app.opts.Name, + Name: app.opts.Name, + Version: app.opts.Version, + Status: StateHealthy, + Address: app.opts.Address, + Port: app.opts.Port, + Metadata: app.opts.Metadata, + } + if info.Metadata == nil { + info.Metadata = make(map[string]string) + } + info.Metadata["os"] = runtime.GOOS + info.Metadata["numOfCPU"] = strconv.Itoa(runtime.NumCPU()) + info.Metadata["goVersion"] = runtime.Version() + info.Metadata["shortName"] = app.opts.ShortName() + info.Metadata["upTime"] = app.uptime.Format(time.DateTime) + return info +} + +func (app *application) preStart() (err error) { + var ( + addr string + ) + app.ctx, app.cancelFunc = context.WithCancelCause(app.opts.Context) + if *cliFlag && !app.opts.DisableCommand { + if err = app.gotoInteractive(); err != nil { + fmt.Println(err) + os.Exit(1) + } + os.Exit(0) + } + app.info = app.buildInfo() + app.Log().Infof("server starting") + env.Set(EnvAppName, app.opts.ShortName()) + env.Set(EnvAppVersion, app.opts.Version) + addr = net.JoinHostPort(app.opts.Address, strconv.Itoa(app.opts.Port)) + app.Log().Infof("server listen on: %s", addr) + app.gateway = entry.New(addr) + if err = app.gateway.Start(app.ctx); err != nil { + return + } + if !app.opts.DisableHttp { + if err = app.httpServe(); err != nil { + return + } + } + if !app.opts.DisableCommand { + if err = app.commandServe(); err != nil { + return + } + } + app.plugins.Range(func(key, value any) bool { + if plugin, ok := value.(Plugin); ok { + if err = plugin.BeforeStart(); err != nil { + return false + } + } + return true + }) + if app.opts.server != nil { + if err = app.opts.server.Start(app.ctx); err != nil { + app.Log().Warnf("server start error: %s", err.Error()) + return + } + } + if !app.opts.DisableStateApi { + app.Handle("/-/run/state", func(ctx Context) (err error) { + return ctx.Success(State{ + ID: app.opts.Name, + Name: app.opts.Name, + Version: app.opts.Version, + Uptime: time.Now().Sub(app.uptime).String(), + Gateway: app.gateway.State(), + }) + }) + app.Handle("/-/healthy", func(ctx Context) (err error) { + return ctx.Success(app.Healthy()) + }) + } + app.plugins.Range(func(key, value any) bool { + if plugin, ok := value.(Plugin); ok { + if err = plugin.AfterStart(); err != nil { + return false + } + } + return true + }) + app.Log().Infof("server started") + return +} + +func (app *application) preStop() (err error) { + if !atomic.CompareAndSwapInt32(&app.exitFlag, 0, 1) { + return + } + app.Log().Infof("server stopping") + app.cancelFunc(ErrStopping) + app.plugins.Range(func(key, value any) bool { + if plugin, ok := value.(Plugin); ok { + if err = plugin.BeforeStop(); err != nil { + return false + } + } + return true + }) + if app.http != nil { + if err = app.http.Shutdown(); err != nil { + app.Log().Warnf("server http shutdown error: %s", err.Error()) + } + } + if app.command != nil { + if err = app.command.Shutdown(); err != nil { + app.Log().Warnf("server command shutdown error: %s", err.Error()) + } + } + if err = app.gateway.Stop(); err != nil { + app.Log().Warnf("server gateway shutdown error: %s", err.Error()) + } + app.plugins.Range(func(key, value any) bool { + if plugin, ok := value.(Plugin); ok { + if err = plugin.AfterStop(); err != nil { + return false + } + } + return true + }) + app.waitGroup.Wait() + app.Log().Infof("server stopped") + return +} + +func (app *application) Use(plugin Plugin) (err error) { + var ( + ok bool + ) + if _, ok = app.plugins.Load(plugin.Name()); ok { + return fmt.Errorf("plugin %s already registered", plugin.Name()) + } + if err = plugin.Mount(app.ctx); err != nil { + return + } + app.plugins.Store(plugin.Name(), plugin) + return +} + +func (app *application) Run() (err error) { + if err = app.preStart(); err != nil { + return + } + ch := make(chan os.Signal, 1) + if app.opts.Signals == nil { + app.opts.Signals = []os.Signal{syscall.SIGTERM, syscall.SIGINT, syscall.SIGQUIT, syscall.SIGKILL} + } + signal.Notify(ch, app.opts.Signals...) + select { + case <-ch: + case <-app.ctx.Done(): + } + return app.preStop() +} + +func New(cbs ...Option) *application { + opts := NewOptions() + for _, cb := range cbs { + cb(opts) + } + app := &application{ + opts: opts, + uptime: time.Now(), + } + return app +} diff --git a/types.go b/types.go new file mode 100644 index 0000000..ae3daec --- /dev/null +++ b/types.go @@ -0,0 +1,48 @@ +package kos + +import ( + "context" + "git.nspix.com/golang/kos/entry" + "git.nspix.com/golang/kos/entry/cli" + "git.nspix.com/golang/kos/entry/http" +) + +type ( + // Server custom attach server + Server interface { + Start(ctx context.Context) (err error) + Stop() (err error) + } + + // HandleFunc request handle func + HandleFunc func(ctx Context) (err error) + + // Application app interface + Application interface { + Info() *Info + Http() *http.Server + Command() *cli.Server + Handle(method string, cb HandleFunc) + } + + // Info application information + Info struct { + ID string `json:"id"` + Name string `json:"name"` + Version string `json:"version"` + VcsVersion string `json:"vcsVersion"` + Status string `json:"status"` + Address string `json:"address"` + Port int `json:"port"` + Metadata map[string]string `json:"metadata"` + } + + // State application runtime state + State struct { + ID string `json:"id"` + Name string `json:"name"` + Version string `json:"version"` + Uptime string `json:"uptime"` + Gateway *entry.State `json:"gateway"` + } +) diff --git a/util/crypto/aes/aes.go b/util/crypto/aes/aes.go new file mode 100644 index 0000000..21d4738 --- /dev/null +++ b/util/crypto/aes/aes.go @@ -0,0 +1,68 @@ +package crypto + +import ( + "bytes" + "crypto/aes" + "crypto/cipher" + "fmt" +) + +func PKCS7Padding(ciphertext []byte, blockSize int) []byte { + m := blockSize - len(ciphertext)%blockSize + n := bytes.Repeat([]byte{byte(m)}, m) + return append(ciphertext, n...) +} + +func PKCS7UnPadding(origData []byte) []byte { + m := len(origData) + n := int(origData[m-1]) + if m > n { + return origData[:(m - n)] + } else { + return origData + } +} + +func Encrypt(buf, key []byte) ([]byte, error) { + var ( + err error + blockSize int + block cipher.Block + ) + if block, err = aes.NewCipher(key); err != nil { + return nil, err + } + defer func() { + if v := recover(); v != nil { + err = fmt.Errorf("decrypt error %v", v) + } + }() + blockSize = block.BlockSize() + buf = PKCS7Padding(buf, blockSize) + blockMode := cipher.NewCBCEncrypter(block, key[:blockSize]) + tmp := make([]byte, len(buf)) + blockMode.CryptBlocks(tmp, buf) + return tmp, nil +} + +func Decrypt(buf, key []byte) ([]byte, error) { + var ( + err error + blockSize int + block cipher.Block + ) + if block, err = aes.NewCipher(key); err != nil { + return nil, err + } + defer func() { + if v := recover(); v != nil { + err = fmt.Errorf("decrypt error %v", v) + } + }() + blockSize = block.BlockSize() + blockMode := cipher.NewCBCDecrypter(block, key[:blockSize]) + origData := make([]byte, len(buf)) + blockMode.CryptBlocks(origData, buf) + origData = PKCS7UnPadding(origData) + return origData, nil +} diff --git a/util/env/env.go b/util/env/env.go new file mode 100644 index 0000000..303c3a5 --- /dev/null +++ b/util/env/env.go @@ -0,0 +1,41 @@ +package env + +import ( + "os" + "strconv" + "strings" +) + +func Get(name string, val string) string { + value := strings.TrimSpace(os.Getenv(name)) + if value == "" { + return val + } else { + return value + } +} + +func Integer(name string, val int64) int64 { + value := Get(name, "") + if n, err := strconv.ParseInt(value, 10, 64); err == nil { + return n + } else { + return val + } +} + +func Float(name string, val float64) float64 { + value := Get(name, "") + if n, err := strconv.ParseFloat(value, 64); err == nil { + return n + } else { + return val + } +} + +func Set(name string, val string) { + value := os.Getenv(name) + if value == "" { + os.Setenv(name, val) + } +} diff --git a/util/fetch/constant.go b/util/fetch/constant.go new file mode 100644 index 0000000..9ef39c2 --- /dev/null +++ b/util/fetch/constant.go @@ -0,0 +1,10 @@ +package fetch + +const ( + UserAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/104.0.5112.81 Safari/537.36 Edg/104.0.1293.54" +) + +const ( + JSON = "application/json" + XML = "application/xml" +) diff --git a/util/fetch/fetch.go b/util/fetch/fetch.go new file mode 100644 index 0000000..0fb3e31 --- /dev/null +++ b/util/fetch/fetch.go @@ -0,0 +1,177 @@ +package fetch + +import ( + "bytes" + "context" + "crypto/tls" + "encoding/json" + "encoding/xml" + "fmt" + "io" + "net" + "net/http" + "net/url" + "strings" + "time" +) + +var ( + httpClient = http.Client{ + Timeout: time.Second * 15, + Transport: &http.Transport{ + Proxy: http.ProxyFromEnvironment, + TLSClientConfig: &tls.Config{ + InsecureSkipVerify: true, + }, + DialContext: (&net.Dialer{ + Timeout: 30 * time.Second, + KeepAlive: 30 * time.Second, + }).DialContext, + ForceAttemptHTTP2: false, + MaxIdleConns: 10, + IdleConnTimeout: 30 * time.Second, + TLSHandshakeTimeout: 10 * time.Second, + ExpectContinueTimeout: 1 * time.Second, + }, + } +) + +func Get(ctx context.Context, urlString string, cbs ...Option) (res *http.Response, err error) { + var ( + uri *url.URL + req *http.Request + ) + opts := newOptions() + for _, cb := range cbs { + cb(opts) + } + if uri, err = url.Parse(urlString); err != nil { + return + } + if opts.Params != nil { + qs := uri.Query() + for k, v := range opts.Params { + qs.Set(k, v) + } + uri.RawQuery = qs.Encode() + } + if req, err = http.NewRequest(http.MethodGet, uri.String(), nil); err != nil { + return + } + if opts.Header != nil { + for k, v := range opts.Header { + req.Header.Set(k, v) + } + } + return Do(ctx, req) +} + +func Post(ctx context.Context, urlString string, cbs ...Option) (res *http.Response, err error) { + var ( + buf []byte + uri *url.URL + req *http.Request + contentType string + reader io.Reader + ) + opts := newOptions() + for _, cb := range cbs { + cb(opts) + } + if uri, err = url.Parse(urlString); err != nil { + return + } + if opts.Params != nil { + qs := uri.Query() + for k, v := range opts.Params { + qs.Set(k, v) + } + uri.RawQuery = qs.Encode() + } + if opts.Data != nil { + switch v := opts.Data.(type) { + case string: + reader = strings.NewReader(v) + contentType = "x-www-form-urlencoded" + case []byte: + reader = bytes.NewReader(v) + contentType = "x-www-form-urlencoded" + default: + if buf, err = json.Marshal(v); err == nil { + reader = bytes.NewReader(buf) + contentType = "application/json" + } else { + return + } + } + } + if req, err = http.NewRequest(http.MethodPost, uri.String(), reader); err != nil { + return + } + if opts.Header != nil { + for k, v := range opts.Header { + req.Header.Set(k, v) + } + } + req.Header.Set("Content-Type", contentType) + return Do(ctx, req) +} + +func Do(ctx context.Context, req *http.Request) (res *http.Response, err error) { + return httpClient.Do(req.WithContext(ctx)) +} + +func Request(ctx context.Context, urlString string, response any, cbs ...Option) (err error) { + var ( + buf []byte + uri *url.URL + res *http.Response + req *http.Request + contentType string + ) + opts := newOptions() + for _, cb := range cbs { + cb(opts) + } + if uri, err = url.Parse(urlString); err != nil { + return + } + if opts.Params != nil { + qs := uri.Query() + for k, v := range opts.Params { + qs.Set(k, v) + } + uri.RawQuery = qs.Encode() + } + if req, err = http.NewRequest(http.MethodGet, uri.String(), nil); err != nil { + return + } + if opts.Header != nil { + for k, v := range opts.Header { + req.Header.Set(k, v) + } + } + if res, err = Do(ctx, req); err != nil { + return + } + defer func() { + _ = res.Body.Close() + }() + if res.StatusCode != http.StatusOK { + if buf, err = io.ReadAll(res.Body); err == nil && len(buf) > 0 { + err = fmt.Errorf("remote server response %s(%d): %s", res.Status, res.StatusCode, string(buf)) + } else { + err = fmt.Errorf("remote server response %d: %s", res.StatusCode, res.Status) + } + return + } + contentType = strings.ToLower(res.Header.Get("Content-Type")) + if strings.Contains(contentType, JSON) { + err = json.NewDecoder(res.Body).Decode(response) + } else if strings.Contains(contentType, XML) { + err = xml.NewDecoder(res.Body).Decode(response) + } else { + err = fmt.Errorf("unsupported content type: %s", contentType) + } + return +} diff --git a/util/fetch/options.go b/util/fetch/options.go new file mode 100644 index 0000000..5ccfc0b --- /dev/null +++ b/util/fetch/options.go @@ -0,0 +1,50 @@ +package fetch + +import "net/http" + +type ( + Options struct { + Url string + Method string + Header map[string]string + Params map[string]string + Data any + } + Option func(o *Options) +) + +func WithUrl(s string) Option { + return func(o *Options) { + o.Url = s + } +} + +func WithMethod(s string) Option { + return func(o *Options) { + o.Method = s + } +} + +func WithHeader(h map[string]string) Option { + return func(o *Options) { + o.Header = h + } +} + +func WithParams(h map[string]string) Option { + return func(o *Options) { + o.Params = h + } +} + +func WithData(v any) Option { + return func(o *Options) { + o.Data = v + } +} + +func newOptions() *Options { + return &Options{ + Method: http.MethodGet, + } +} diff --git a/util/fs/dir.go b/util/fs/dir.go new file mode 100644 index 0000000..ffff1c1 --- /dev/null +++ b/util/fs/dir.go @@ -0,0 +1,32 @@ +package fs + +import ( + "errors" + "os" +) + +// IsDir Tells whether the filename is a directory +func IsDir(filename string) (bool, error) { + fd, err := os.Stat(filename) + if err != nil { + return false, err + } + fm := fd.Mode() + return fm.IsDir(), nil +} + +// DirectoryOrCreate checking directory, is not exists will create +func DirectoryOrCreate(dirname string) error { + if fi, err := os.Stat(dirname); err != nil { + if errors.Is(err, os.ErrNotExist) { + return os.MkdirAll(dirname, 0755) + } else { + return err + } + } else { + if fi.IsDir() { + return nil + } + return errors.New("file not directory") + } +} diff --git a/util/fs/file.go b/util/fs/file.go new file mode 100644 index 0000000..ee66687 --- /dev/null +++ b/util/fs/file.go @@ -0,0 +1 @@ +package fs diff --git a/util/ip/external.go b/util/ip/external.go new file mode 100644 index 0000000..3d0fe4b --- /dev/null +++ b/util/ip/external.go @@ -0,0 +1,44 @@ +package ip + +import ( + "net" + "strings" +) + +func External() (res []string) { + var ( + err error + addrs []net.Addr + inters []net.Interface + ) + if inters, err = net.Interfaces(); err != nil { + return + } + for _, inter := range inters { + if !strings.HasPrefix(inter.Name, "lo") { + if addrs, err = inter.Addrs(); err != nil { + continue + } + for _, addr := range addrs { + if ipNet, ok := addr.(*net.IPNet); ok { + if ipNet.IP.IsLoopback() || ipNet.IP.IsLinkLocalMulticast() || ipNet.IP.IsLinkLocalUnicast() { + continue + } + if ip4 := ipNet.IP.To4(); ip4 != nil { + switch true { + case ip4[0] == 10: + continue + case ip4[0] == 172 && ip4[1] >= 16 && ip4[1] <= 31: + continue + case ip4[0] == 192 && ip4[1] == 168: + continue + default: + res = append(res, ipNet.IP.String()) + } + } + } + } + } + } + return +} diff --git a/util/ip/internal.go b/util/ip/internal.go new file mode 100644 index 0000000..46ecd61 --- /dev/null +++ b/util/ip/internal.go @@ -0,0 +1,36 @@ +package ip + +import ( + "net" + "strings" +) + +// Internal get internal ip. +func Internal() string { + var ( + err error + addrs []net.Addr + inters []net.Interface + ) + if inters, err = net.Interfaces(); err != nil { + return "" + } + for _, inter := range inters { + if !isUp(inter.Flags) { + continue + } + if !strings.HasPrefix(inter.Name, "lo") { + if addrs, err = inter.Addrs(); err != nil { + continue + } + for _, addr := range addrs { + if ipNet, ok := addr.(*net.IPNet); ok && !ipNet.IP.IsLoopback() { + if ipNet.IP.To4() != nil { + return ipNet.IP.String() + } + } + } + } + } + return "" +} diff --git a/util/ip/ip.go b/util/ip/ip.go new file mode 100644 index 0000000..88d9f80 --- /dev/null +++ b/util/ip/ip.go @@ -0,0 +1,28 @@ +package ip + +import ( + "encoding/binary" + "net" +) + +// isUp Interface is up +func isUp(v net.Flags) bool { + return v&net.FlagUp == net.FlagUp +} + +// ToLong Converts a string containing an (IPv4) Internet Protocol dotted address into a long integer +func ToLong(ipAddress string) uint32 { + ip := net.ParseIP(ipAddress) + if ip == nil { + return 0 + } + return binary.BigEndian.Uint32(ip.To4()) +} + +// FromLong Converts a long integer address into a string in (IPv4) Internet standard dotted format +func FromLong(properAddress uint32) string { + ipByte := make([]byte, 4) + binary.BigEndian.PutUint32(ipByte, properAddress) + ip := net.IP(ipByte) + return ip.String() +} diff --git a/util/pool/buffer.go b/util/pool/buffer.go new file mode 100644 index 0000000..9e58cce --- /dev/null +++ b/util/pool/buffer.go @@ -0,0 +1,22 @@ +package pool + +import ( + "bytes" + "sync" +) + +var ( + bufferPool sync.Pool +) + +func GetBuffer() *bytes.Buffer { + if v := bufferPool.Get(); v != nil { + return v.(*bytes.Buffer) + } + return bytes.NewBuffer([]byte{}) +} + +func PutBuffer(b *bytes.Buffer) { + b.Reset() + bufferPool.Put(b) +} diff --git a/util/pool/bytes.go b/util/pool/bytes.go new file mode 100644 index 0000000..cdcfcdf --- /dev/null +++ b/util/pool/bytes.go @@ -0,0 +1,50 @@ +package pool + +import "sync" + +var ( + bufPool5k sync.Pool + bufPool2k sync.Pool + bufPool1k sync.Pool + bufPool sync.Pool +) + +func GetBytes(size int) []byte { + if size <= 0 { + return nil + } + var x interface{} + if size >= 5*1024 { + x = bufPool5k.Get() + } else if size >= 2*1024 { + x = bufPool2k.Get() + } else if size >= 1*1024 { + x = bufPool1k.Get() + } else { + x = bufPool.Get() + } + if x == nil { + return make([]byte, size) + } + buf := x.([]byte) + if cap(buf) < size { + return make([]byte, size) + } + return buf[:size] +} + +func PutBytes(buf []byte) { + size := cap(buf) + if size <= 0 { + return + } + if size >= 5*1024 { + bufPool5k.Put(buf) + } else if size >= 2*1024 { + bufPool2k.Put(buf) + } else if size >= 1*1024 { + bufPool1k.Put(buf) + } else { + bufPool.Put(buf) + } +} diff --git a/util/random/int.go b/util/random/int.go new file mode 100644 index 0000000..97348cc --- /dev/null +++ b/util/random/int.go @@ -0,0 +1,9 @@ +package random + +import ( + "math/rand" +) + +func Int(min, max int64) int64 { + return min + rand.Int63n(max-min) +} diff --git a/util/random/ip.go b/util/random/ip.go new file mode 100644 index 0000000..2aad1ea --- /dev/null +++ b/util/random/ip.go @@ -0,0 +1,21 @@ +package random + +import ( + "strconv" + "strings" +) + +var ( + ipSet = strings.Split("58.14.0.0,58.16.0.0,58.24.0.0,58.30.0.0,58.32.0.0,58.66.0.0,58.68.128.0,58.82.0.0,58.87.64.0,58.99.128.0,58.100.0.0,58.116.0.0,58.128.0.0,58.144.0.0,58.154.0.0,58.192.0.0,58.240.0.0,59.32.0.0,59.64.0.0,59.80.0.0,59.107.0.0,59.108.0.0,59.151.0.0,59.155.0.0,59.172.0.0,59.191.0.0,59.191.240.0,59.192.0.0,60.0.0.0,60.55.0.0,60.63.0.0,60.160.0.0,60.194.0.0,60.200.0.0,60.208.0.0,60.232.0.0,60.235.0.0,60.245.128.0,60.247.0.0,60.252.0.0,60.253.128.0,60.255.0.0,61.4.80.0,61.4.176.0,61.8.160.0,61.28.0.0,61.29.128.0,61.45.128.0,61.47.128.0,61.48.0.0,61.87.192.0,61.128.0.0,61.232.0.0,61.236.0.0,61.240.0.0,114.28.0.0,114.54.0.0,114.60.0.0,114.64.0.0,114.68.0.0,114.80.0.0,116.1.0.0,116.2.0.0,116.4.0.0,116.8.0.0,116.13.0.0,116.16.0.0,116.52.0.0,116.56.0.0,116.58.128.0,116.58.208.0,116.60.0.0,116.66.0.0,116.69.0.0,116.70.0.0,116.76.0.0,116.89.144.0,116.90.184.0,116.95.0.0,116.112.0.0,116.116.0.0,116.128.0.0,116.192.0.0,116.193.16.0,116.193.32.0,116.194.0.0,116.196.0.0,116.198.0.0,116.199.0.0,116.199.128.0,116.204.0.0,116.207.0.0,116.208.0.0,116.212.160.0,116.213.64.0,116.213.128.0,116.214.32.0,116.214.64.0,116.214.128.0,116.215.0.0,116.216.0.0,116.224.0.0,116.242.0.0,116.244.0.0,116.248.0.0,116.252.0.0,116.254.128.0,116.255.128.0,117.8.0.0,117.21.0.0,117.22.0.0,117.24.0.0,117.32.0.0,117.40.0.0,117.44.0.0,117.48.0.0,117.53.48.0,117.53.176.0,117.57.0.0,117.58.0.0,117.59.0.0,117.60.0.0,117.64.0.0,117.72.0.0,117.74.64.0,117.74.128.0,117.75.0.0,117.76.0.0,117.80.0.0,117.100.0.0,117.103.16.0,117.103.128.0,117.106.0.0,117.112.0.0,117.120.64.0,117.120.128.0,117.121.0.0,117.121.128.0,117.121.192.0,117.122.128.0,117.124.0.0,117.128.0.0,118.24.0.0,118.64.0.0,118.66.0.0,118.67.112.0,118.72.0.0,118.80.0.0,118.84.0.0,118.88.32.0,118.88.64.0,118.88.128.0,118.89.0.0,118.91.240.0,118.102.16.0,118.112.0.0,118.120.0.0,118.124.0.0,118.126.0.0,118.132.0.0,118.144.0.0,118.178.0.0,118.180.0.0,118.184.0.0,118.192.0.0,118.212.0.0,118.224.0.0,118.228.0.0,118.230.0.0,118.239.0.0,118.242.0.0,118.244.0.0,118.248.0.0,119.0.0.0,119.2.0.0,119.2.128.0,119.3.0.0,119.4.0.0,119.8.0.0,119.10.0.0,119.15.136.0,119.16.0.0,119.18.192.0,119.18.208.0,119.18.224.0,119.19.0.0,119.20.0.0,119.27.64.0,119.27.160.0,119.27.192.0,119.28.0.0,119.30.48.0,119.31.192.0,119.32.0.0,119.40.0.0,119.40.64.0,119.40.128.0,119.41.0.0,119.42.0.0,119.42.136.0,119.42.224.0,119.44.0.0,119.48.0.0,119.57.0.0,119.58.0.0,119.59.128.0,119.60.0.0,119.62.0.0,119.63.32.0,119.75.208.0,119.78.0.0,119.80.0.0,119.84.0.0,119.88.0.0,119.96.0.0,119.108.0.0,119.112.0.0,119.128.0.0,119.144.0.0,119.148.160.0,119.161.128.0,119.162.0.0,119.164.0.0,119.176.0.0,119.232.0.0,119.235.128.0,119.248.0.0,119.253.0.0,119.254.0.0,120.0.0.0,120.24.0.0,120.30.0.0,120.32.0.0,120.48.0.0,120.52.0.0,120.64.0.0,120.72.32.0,120.72.128.0,120.76.0.0,120.80.0.0,120.90.0.0,120.92.0.0,120.94.0.0,120.128.0.0,120.136.128.0,120.137.0.0,120.192.0.0,121.0.16.0,121.4.0.0,121.8.0.0,121.16.0.0,121.32.0.0,121.40.0.0,121.46.0.0,121.48.0.0,121.51.0.0,121.52.160.0,121.52.208.0,121.52.224.0,121.55.0.0,121.56.0.0,121.58.0.0,121.58.144.0,121.59.0.0,121.60.0.0,121.68.0.0,121.76.0.0,121.79.128.0,121.89.0.0,121.100.128.0,121.101.208.0,121.192.0.0,121.201.0.0,121.204.0.0,121.224.0.0,121.248.0.0,121.255.0.0,122.0.64.0,122.0.128.0,122.4.0.0,122.8.0.0,122.48.0.0,122.49.0.0,122.51.0.0,122.64.0.0,122.96.0.0,122.102.0.0,122.102.64.0,122.112.0.0,122.119.0.0,122.136.0.0,122.144.128.0,122.152.192.0,122.156.0.0,122.192.0.0,122.198.0.0,122.200.64.0,122.204.0.0,122.224.0.0,122.240.0.0,122.248.48.0,123.0.128.0,123.4.0.0,123.8.0.0,123.49.128.0,123.52.0.0,123.56.0.0,123.64.0.0,123.96.0.0,123.98.0.0,123.99.128.0,123.100.0.0,123.101.0.0,123.103.0.0,123.108.128.0,123.108.208.0,123.112.0.0,123.128.0.0,123.136.80.0,123.137.0.0,123.138.0.0,123.144.0.0,123.160.0.0,123.176.80.0,123.177.0.0,123.178.0.0,123.180.0.0,123.184.0.0,123.196.0.0,123.199.128.0,123.206.0.0,123.232.0.0,123.242.0.0,123.244.0.0,123.249.0.0,123.253.0.0,124.6.64.0,124.14.0.0,124.16.0.0,124.20.0.0,124.28.192.0,124.29.0.0,124.31.0.0,124.40.112.0,124.40.128.0,124.42.0.0,124.47.0.0,124.64.0.0,124.66.0.0,124.67.0.0,124.68.0.0,124.72.0.0,124.88.0.0,124.108.8.0,124.108.40.0,124.112.0.0,124.126.0.0,124.128.0.0,124.147.128.0,124.156.0.0,124.160.0.0,124.172.0.0,124.192.0.0,124.196.0.0,124.200.0.0,124.220.0.0,124.224.0.0,124.240.0.0,124.240.128.0,124.242.0.0,124.243.192.0,124.248.0.0,124.249.0.0,124.250.0.0,124.254.0.0,125.31.192.0,125.32.0.0,125.58.128.0,125.61.128.0,125.62.0.0,125.64.0.0,125.96.0.0,125.98.0.0,125.104.0.0,125.112.0.0,125.169.0.0,125.171.0.0,125.208.0.0,125.210.0.0,125.213.0.0,125.214.96.0,125.215.0.0,125.216.0.0,125.254.128.0,134.196.0.0,159.226.0.0,161.207.0.0,162.105.0.0,166.111.0.0,167.139.0.0,168.160.0.0,169.211.1.0,192.83.122.0,192.83.169.0,192.124.154.0,192.188.170.0,198.17.7.0,202.0.110.0,202.0.176.0,202.4.128.0,202.4.252.0,202.8.128.0,202.10.64.0,202.14.88.0,202.14.235.0,202.14.236.0,202.14.238.0,202.20.120.0,202.22.248.0,202.38.0.0,202.38.64.0,202.38.128.0,202.38.136.0,202.38.138.0,202.38.140.0,202.38.146.0,202.38.149.0,202.38.150.0,202.38.152.0,202.38.156.0,202.38.158.0,202.38.160.0,202.38.164.0,202.38.168.0,202.38.176.0,202.38.184.0,202.38.192.0,202.41.152.0,202.41.240.0,202.43.144.0,202.46.32.0,202.46.224.0,202.60.112.0,202.63.248.0,202.69.4.0,202.69.16.0,202.70.0.0,202.74.8.0,202.75.208.0,202.85.208.0,202.90.0.0,202.90.224.0,202.90.252.0,202.91.0.0,202.91.128.0,202.91.176.0,202.91.224.0,202.92.0.0,202.92.252.0,202.93.0.0,202.93.252.0,202.95.0.0,202.95.252.0,202.96.0.0,202.112.0.0,202.120.0.0,202.122.0.0,202.122.32.0,202.122.64.0,202.122.112.0,202.122.128.0,202.123.96.0,202.124.24.0,202.125.176.0,202.127.0.0,202.127.12.0,202.127.16.0,202.127.40.0,202.127.48.0,202.127.112.0,202.127.128.0,202.127.160.0,202.127.192.0,202.127.208.0,202.127.212.0,202.127.216.0,202.127.224.0,202.130.0.0,202.130.224.0,202.131.16.0,202.131.48.0,202.131.208.0,202.136.48.0,202.136.208.0,202.136.224.0,202.141.160.0,202.142.16.0,202.143.16.0,202.148.96.0,202.149.160.0,202.149.224.0,202.150.16.0,202.152.176.0,202.153.48.0,202.158.160.0,202.160.176.0,202.164.0.0,202.164.25.0,202.165.96.0,202.165.176.0,202.165.208.0,202.168.160.0,202.170.128.0,202.170.216.0,202.173.8.0,202.173.224.0,202.179.240.0,202.180.128.0,202.181.112.0,202.189.80.0,202.192.0.0,203.18.50.0,203.79.0.0,203.80.144.0,203.81.16.0,203.83.56.0,203.86.0.0,203.86.64.0,203.88.32.0,203.88.192.0,203.89.0.0,203.90.0.0,203.90.128.0,203.90.192.0,203.91.32.0,203.91.96.0,203.91.120.0,203.92.0.0,203.92.160.0,203.93.0.0,203.94.0.0,203.95.0.0,203.95.96.0,203.99.16.0,203.99.80.0,203.100.32.0,203.100.80.0,203.100.96.0,203.100.192.0,203.110.160.0,203.118.192.0,203.119.24.0,203.119.32.0,203.128.32.0,203.128.96.0,203.130.32.0,203.132.32.0,203.134.240.0,203.135.96.0,203.135.160.0,203.142.219.0,203.148.0.0,203.152.64.0,203.156.192.0,203.158.16.0,203.161.192.0,203.166.160.0,203.171.224.0,203.174.7.0,203.174.96.0,203.175.128.0,203.175.192.0,203.176.168.0,203.184.80.0,203.187.160.0,203.190.96.0,203.191.16.0,203.191.64.0,203.191.144.0,203.192.0.0,203.196.0.0,203.207.64.0,203.207.128.0,203.208.0.0,203.208.16.0,203.208.32.0,203.209.224.0,203.212.0.0,203.212.80.0,203.222.192.0,203.223.0.0,210.2.0.0,210.5.0.0,210.5.144.0,210.12.0.0,210.14.64.0,210.14.112.0,210.14.128.0,210.15.0.0,210.15.128.0,210.16.128.0,210.21.0.0,210.22.0.0,210.23.32.0,210.25.0.0,210.26.0.0,210.28.0.0,210.32.0.0,210.51.0.0,210.52.0.0,210.56.192.0,210.72.0.0,210.76.0.0,210.78.0.0,210.79.64.0,210.79.224.0,210.82.0.0,210.87.128.0,210.185.192.0,210.192.96.0,211.64.0.0,211.80.0.0,211.96.0.0,211.136.0.0,211.144.0.0,211.160.0.0,218.0.0.0,218.56.0.0,218.64.0.0,218.96.0.0,218.104.0.0,218.108.0.0,218.185.192.0,218.192.0.0,218.240.0.0,218.249.0.0,219.72.0.0,219.82.0.0,219.128.0.0,219.216.0.0,219.224.0.0,219.242.0.0,219.244.0.0,220.101.192.0,220.112.0.0,220.152.128.0,220.154.0.0,220.160.0.0,220.192.0.0,220.231.0.0,220.231.128.0,220.232.64.0,220.234.0.0,220.242.0.0,220.248.0.0,220.252.0.0,221.0.0.0,221.8.0.0,221.12.0.0,221.12.128.0,221.13.0.0,221.14.0.0,221.122.0.0,221.129.0.0,221.130.0.0,221.133.224.0,221.136.0.0,221.172.0.0,221.176.0.0,221.192.0.0,221.196.0.0,221.198.0.0,221.199.0.0,221.199.128.0,221.199.192.0,221.199.224.0,221.200.0.0,221.208.0.0,221.224.0.0,222.16.0.0,222.32.0.0,222.64.0.0,222.125.0.0,222.126.128.0,222.128.0.0,222.160.0.0,222.168.0.0,222.176.0.0,222.192.0.0,222.240.0.0,222.248.0.0", ",") +) + +func IP() string { + ip := ipSet[Int(0, int64(len(ipSet))-1)] + result := strings.Split(ip, ".") + for k, v := range result { + if v == "0" { + result[k] = strconv.Itoa(int(Int(0, 255))) + } + } + return strings.Join(result, ".") +} diff --git a/util/random/string.go b/util/random/string.go new file mode 100644 index 0000000..24ba24a --- /dev/null +++ b/util/random/string.go @@ -0,0 +1,28 @@ +package random + +import ( + "math/rand" + "strings" +) + +const ( + Uppercase = "ABCDEFGHIJKLMNOPQRSTUVWXYZ" + Lowercase = "abcdefghijklmnopqrstuvwxyz" + Alphabetic = Uppercase + Lowercase + Numeric = "0123456789" + Alphanumeric = Alphabetic + Numeric + Symbols = "`" + `~!@#$%^&*()-_+={}[]|\;:"<>,./?` + Hex = Numeric + "abcdef" +) + +func String(length uint8, charsets ...string) string { + charset := strings.Join(charsets, "") + if charset == "" { + charset = Alphanumeric + } + b := make([]byte, length) + for i := range b { + b[i] = charset[rand.Int63()%int64(len(charset))] + } + return string(b) +} diff --git a/util/reflect/reflect.go b/util/reflect/reflect.go new file mode 100644 index 0000000..4efca96 --- /dev/null +++ b/util/reflect/reflect.go @@ -0,0 +1,161 @@ +package reflect + +import ( + "fmt" + "reflect" + "strconv" + "strings" +) + +func findField(v reflect.Value, field string) reflect.Value { + var ( + pos int + tagValue string + refValue reflect.Value + refType reflect.Type + fieldType reflect.StructField + allowTags = []string{"json", "yaml", "xml"} + ) + refValue = v.FieldByName(field) + if !refValue.IsValid() { + refType = v.Type() + for i := 0; i < refType.NumField(); i++ { + fieldType = refType.Field(i) + for _, tagName := range allowTags { + tagValue = fieldType.Tag.Get(tagName) + if tagValue == "" { + continue + } + if pos = strings.Index(tagValue, ","); pos != -1 { + tagValue = tagValue[:pos] + } + if tagValue == field { + return v.Field(i) + } + } + } + } + return refValue +} + +func safeAssignment(variable reflect.Value, value interface{}) (err error) { + var ( + n int64 + un uint64 + fn float64 + kind reflect.Kind + ) + rv := reflect.ValueOf(value) + kind = variable.Kind() + if kind != reflect.Slice && kind != reflect.Array && kind != reflect.Map && kind == rv.Kind() { + variable.Set(rv) + return + } + switch kind { + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + switch rv.Kind() { + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + variable.SetInt(rv.Int()) + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: + variable.SetInt(int64(rv.Uint())) + case reflect.Float32, reflect.Float64: + variable.SetInt(int64(rv.Float())) + case reflect.String: + if n, err = strconv.ParseInt(rv.String(), 10, 64); err == nil { + variable.SetInt(n) + } + default: + err = fmt.Errorf("unsupported kind %s", rv.Kind()) + } + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: + switch rv.Kind() { + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + variable.SetUint(uint64(rv.Int())) + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: + variable.SetUint(rv.Uint()) + case reflect.Float32, reflect.Float64: + variable.SetUint(uint64(rv.Float())) + case reflect.String: + if un, err = strconv.ParseUint(rv.String(), 10, 64); err == nil { + variable.SetUint(un) + } + default: + err = fmt.Errorf("unsupported kind %s", rv.Kind()) + } + case reflect.Float32, reflect.Float64: + switch rv.Kind() { + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + variable.SetFloat(float64(rv.Int())) + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: + variable.SetFloat(float64(rv.Uint())) + case reflect.Float32, reflect.Float64: + variable.SetFloat(rv.Float()) + case reflect.String: + if fn, err = strconv.ParseFloat(rv.String(), 64); err == nil { + variable.SetFloat(fn) + } + default: + err = fmt.Errorf("unsupported kind %s", rv.Kind()) + } + case reflect.String: + switch rv.Kind() { + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + variable.SetString(strconv.FormatInt(rv.Int(), 10)) + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: + variable.SetString(strconv.FormatUint(rv.Uint(), 10)) + case reflect.Float32, reflect.Float64: + variable.SetString(strconv.FormatFloat(rv.Float(), 'f', -1, 64)) + case reflect.String: + variable.SetString(rv.String()) + default: + variable.SetString(fmt.Sprint(value)) + } + default: + err = fmt.Errorf("unsupported kind %s", kind) + } + return +} + +func Set(hacky interface{}, field string, value interface{}) (err error) { + var ( + n int + refField reflect.Value + ) + refVal := reflect.ValueOf(hacky) + if refVal.Kind() == reflect.Ptr { + refVal = reflect.Indirect(refVal) + } + if refVal.Kind() != reflect.Struct { + return fmt.Errorf("%s kind is %v", refVal.Type().String(), refField.Kind()) + } + refField = findField(refVal, field) + if !refField.IsValid() { + return fmt.Errorf("%s field `%s` not found", refVal.Type(), field) + } + rv := reflect.ValueOf(value) + fieldKind := refField.Kind() + if fieldKind != reflect.Slice && fieldKind != reflect.Array && fieldKind != reflect.Map && fieldKind == rv.Kind() { + refField.Set(rv) + return + } + switch fieldKind { + case reflect.Array, reflect.Slice: + innerType := refField.Type().Elem() + if rv.Kind() == reflect.Array || rv.Kind() == reflect.Slice { + sliceVar := reflect.MakeSlice(refField.Type(), rv.Len(), rv.Len()) + n = 0 + for i := 0; i < rv.Len(); i++ { + srcVal := rv.Index(i) + dstVal := reflect.New(innerType).Elem() + if err = safeAssignment(dstVal, srcVal); err == nil { + sliceVar.Index(n).Set(dstVal) + n++ + } + } + refField.Set(sliceVar.Slice(0, n)) + } + default: + err = safeAssignment(refField, value) + } + return +} diff --git a/util/sys/homedir.go b/util/sys/homedir.go new file mode 100644 index 0000000..e1636b5 --- /dev/null +++ b/util/sys/homedir.go @@ -0,0 +1,48 @@ +package sys + +import ( + "os" + "path/filepath" + "runtime" +) + +// HomeDir return user home directory +func HomeDir() string { + if runtime.GOOS == "windows" { + return os.Getenv("HOMEDRIVE") + os.Getenv("HOMEPATH") + } + if h := os.Getenv("HOME"); h != "" { + return h + } + return "/" +} + +// HiddenFile get hidden file prefix +func HiddenFile(name string) string { + switch runtime.GOOS { + case "windows": + return "~" + name + default: + return "." + name + } +} + +// CacheDir return user cache directory +func CacheDir() string { + switch runtime.GOOS { + case "darwin": + return filepath.Join(HomeDir(), "Library", "Caches") + case "windows": + for _, ev := range []string{"APPDATA", "CSIDL_APPDATA", "TEMP", "TMP"} { + if v := os.Getenv(ev); v != "" { + return v + } + } + // Worst case: + return HomeDir() + } + if xdg := os.Getenv("XDG_CACHE_HOME"); xdg != "" { + return xdg + } + return filepath.Join(HomeDir(), ".cache") +}