Compare commits

..

14 Commits

40 changed files with 1521 additions and 502 deletions

62
.github/workflows/nfctl.yml vendored Normal file
View File

@ -0,0 +1,62 @@
name: Auto Build
on:
push:
branches:
- 'master'
env:
RELEASE_VERSION: v24.07.13-r1
jobs:
build-job:
runs-on: ubuntu-latest
permissions:
id-token: write
contents: write
pull-requests: write
repository-projects: write
steps:
- name: checkout repository
uses: actions/checkout@v4
- name: install golang
uses: actions/setup-go@v4
with:
go-version: '1.20'
- name: build linux amd64
run: CGO_ENABLE=0 GOOS=linux GOARCH=amd64 go build -ldflags='-s -w' -o "dist/nfctl-linux_amd64-${{ env.RELEASE_VERSION }}" nft/nfctl/main.go
- name: build linux arm64
run: CGO_ENABLE=0 GOOS=linux GOARCH=arm64 go build -ldflags='-s -w' -o "dist/nfctl-linux_arm64-${{ env.RELEASE_VERSION }}" nft/nfctl/main.go
- name: build windows amd64
run: CGO_ENABLE=0 GOOS=windows GOARCH=amd64 go build -ldflags='-s -w' -o "dist/nfctl-win_amd64-${{ env.RELEASE_VERSION }}.exe" nft/nfctl/main.go
- name: build windows arm64
run: CGO_ENABLE=0 GOOS=windows GOARCH=arm64 go build -ldflags='-s -w' -o "dist/nfctl-win_arm64-${{ env.RELEASE_VERSION }}.exe" nft/nfctl/main.go
- name: build darwin amd64
run: CGO_ENABLE=0 GOOS=darwin GOARCH=amd64 go build -ldflags='-s -w' -o "dist/nfctl-darwin_arm64-${{ env.RELEASE_VERSION }}" nft/nfctl/main.go
- name: build darwin arm64
run: CGO_ENABLE=0 GOOS=darwin GOARCH=arm64 go build -ldflags='-s -w' -o "dist/nfctl-darwin_arm64-${{ env.RELEASE_VERSION }}" nft/nfctl/main.go
- name: show all builds
run: ls -lash dist
- name: create releases
id: create_releases
uses: "marvinpinto/action-automatic-releases@latest"
with:
automatic_release_tag: "Release-nfctl-${{ env.RELEASE_VERSION }}"
repo_token: "${{ secrets.GITHUB_TOKEN }}"
title: "Release_${{ env.RELEASE_VERSION }}"
prerelease: false
files: |
dist/nfctl-linux_amd64-${{ env.RELEASE_VERSION }}
dist/nfctl-linux_arm64-${{ env.RELEASE_VERSION }}
dist/nfctl-win_amd64-${{ env.RELEASE_VERSION }}.exe
dist/nfctl-win_arm64-${{ env.RELEASE_VERSION }}.exe
dist/nfctl-darwin_arm64-${{ env.RELEASE_VERSION }}
dist/nfctl-darwin_arm64-${{ env.RELEASE_VERSION }}

1
.gitignore vendored
View File

@ -1,3 +1,4 @@
.idea .idea
.vscode .vscode
.DS_Store .DS_Store
xtest

18
app.go
View File

@ -19,8 +19,6 @@ var (
regSafePrefix = regexp.MustCompile("[^a-zA-Z0-9/-]+") regSafePrefix = regexp.MustCompile("[^a-zA-Z0-9/-]+")
regRemoveRepeatedChar = regexp.MustCompile("/{2,}") regRemoveRepeatedChar = regexp.MustCompile("/{2,}")
mimePlain = []string{"text/plain"}
) )
type App struct { type App struct {
@ -165,8 +163,9 @@ func (a *App) handleHTTPRequest(c *Ctx) {
// Find route in tree // Find route in tree
value := root.getValue(rPath, c.params, c.skippedNodes, unescape) value := root.getValue(rPath, c.params, c.skippedNodes, unescape)
if value.params != nil { if value.params != nil {
c.Params = *value.params c.params = value.params
} }
if value.handlers != nil { if value.handlers != nil {
c.handlers = value.handlers c.handlers = value.handlers
c.fullPath = value.fullPath c.fullPath = value.fullPath
@ -175,7 +174,6 @@ func (a *App) handleHTTPRequest(c *Ctx) {
serveError(c, errorHandler) serveError(c, errorHandler)
} }
c.writermem.WriteHeaderNow()
return return
} }
if httpMethod != http.MethodConnect && rPath != "/" { if httpMethod != http.MethodConnect && rPath != "/" {
@ -204,17 +202,19 @@ func (a *App) handleHTTPRequest(c *Ctx) {
} }
if len(allowed) > 0 { if len(allowed) > 0 {
c.handlers = a.combineHandlers() c.handlers = a.combineHandlers(a.config.MethodNotAllowedHandler)
serveError(c, a.config.MethodNotAllowedHandler) _ = c.Next()
return return
} }
} }
c.handlers = a.combineHandlers() c.handlers = a.combineHandlers(a.config.NotFoundHandler)
serveError(c, a.config.NotFoundHandler) _ = c.Next()
return
} }
func errorHandler(c *Ctx) error { func errorHandler(c *Ctx) error {
@ -273,6 +273,6 @@ func redirectRequest(c *Ctx) {
//debugPrint("redirecting request %d: %s --> %s", code, rPath, rURL) //debugPrint("redirecting request %d: %s --> %s", code, rPath, rURL)
http.Redirect(c.writer, req, rURL, code) http.Redirect(c.Writer, req, rURL, code)
c.writermem.WriteHeaderNow() c.writermem.WriteHeaderNow()
} }

100
ctx.go
View File

@ -3,23 +3,28 @@ package nf
import ( import (
"bytes" "bytes"
"encoding/json" "encoding/json"
"errors"
"fmt" "fmt"
"github.com/loveuer/nf/internal/sse"
"io" "io"
"mime/multipart" "mime/multipart"
"net" "net"
"net/http" "net/http"
"strings" "strings"
"sync"
)
var (
forwardHeaders = []string{"CF-Connecting-IP", "X-Forwarded-For", "X-Real-Ip"}
) )
type Ctx struct { type Ctx struct {
lock sync.Mutex
writermem responseWriter writermem responseWriter
// origin objects Writer ResponseWriter
writer http.ResponseWriter
Request *http.Request Request *http.Request
// request info
path string path string
method string method string
// response info
StatusCode int StatusCode int
app *App app *App
@ -29,7 +34,6 @@ type Ctx struct {
locals map[string]interface{} locals map[string]interface{}
skippedNodes *[]skippedNode skippedNodes *[]skippedNode
fullPath string fullPath string
Params Params
} }
func newContext(app *App, writer http.ResponseWriter, request *http.Request) *Ctx { func newContext(app *App, writer http.ResponseWriter, request *http.Request) *Ctx {
@ -38,8 +42,7 @@ func newContext(app *App, writer http.ResponseWriter, request *http.Request) *Ct
v := make(Params, 0, app.maxParams) v := make(Params, 0, app.maxParams)
ctx := &Ctx{ ctx := &Ctx{
writer: writer, lock: sync.Mutex{},
writermem: responseWriter{},
Request: request, Request: request,
path: request.URL.Path, path: request.URL.Path,
method: request.Method, method: request.Method,
@ -54,11 +57,13 @@ func newContext(app *App, writer http.ResponseWriter, request *http.Request) *Ct
} }
ctx.writermem = responseWriter{ ctx.writermem = responseWriter{
ResponseWriter: ctx.writer, ResponseWriter: writer,
size: -1, size: -1,
status: 0, status: 0,
} }
ctx.Writer = &ctx.writermem
return ctx return ctx
} }
@ -144,7 +149,15 @@ func (c *Ctx) verify() error {
} }
func (c *Ctx) Param(key string) string { func (c *Ctx) Param(key string) string {
return c.Params.ByName(key) return c.params.ByName(key)
}
func (c *Ctx) SetParam(key, value string) {
c.lock.Lock()
defer c.lock.Unlock()
params := append(*c.params, Param{Key: key, Value: value})
c.params = &params
} }
func (c *Ctx) Form(key string) string { func (c *Ctx) Form(key string) string {
@ -161,6 +174,14 @@ func (c *Ctx) FormFile(key string) (*multipart.FileHeader, error) {
return fh, err return fh, err
} }
func (c *Ctx) MultipartForm() (*multipart.Form, error) {
if err := c.Request.ParseMultipartForm(c.app.config.BodyLimit); err != nil {
return nil, err
}
return c.Request.MultipartForm, nil
}
func (c *Ctx) Query(key string) string { func (c *Ctx) Query(key string) string {
return c.Request.URL.Query().Get(key) return c.Request.URL.Query().Get(key)
} }
@ -174,11 +195,24 @@ func (c *Ctx) Get(key string, defaultValue ...string) string {
return value return value
} }
func (c *Ctx) IP() string { func (c *Ctx) IP(useProxyHeader ...bool) string {
ip, _, err := net.SplitHostPort(strings.TrimSpace(c.Request.RemoteAddr)) ip, _, err := net.SplitHostPort(strings.TrimSpace(c.Request.RemoteAddr))
if err != nil { if err != nil {
return "" return ""
} }
if len(useProxyHeader) > 0 && useProxyHeader[0] {
for _, h := range forwardHeaders {
for _, rip := range strings.Split(c.Request.Header.Get(h), ",") {
realIP := net.ParseIP(strings.Replace(rip, " ", "", -1))
if check := net.ParseIP(realIP.String()); check != nil {
ip = realIP.String()
break
}
}
}
}
return ip return ip
} }
@ -228,11 +262,6 @@ func (c *Ctx) BodyParser(out interface{}) error {
} }
func (c *Ctx) QueryParser(out interface{}) error { func (c *Ctx) QueryParser(out interface{}) error {
//v := reflect.ValueOf(out)
//
//if v.Kind() == reflect.Ptr && v.Elem().Kind() != reflect.Map {
//}
return parseToStruct("query", out, c.Request.URL.Query()) return parseToStruct("query", out, c.Request.URL.Query())
} }
@ -241,8 +270,12 @@ func (c *Ctx) QueryParser(out interface{}) error {
=============================================================== */ =============================================================== */
func (c *Ctx) Status(code int) *Ctx { func (c *Ctx) Status(code int) *Ctx {
c.lock.Lock()
defer c.lock.Unlock()
c.writermem.WriteHeader(code) c.writermem.WriteHeader(code)
c.StatusCode = c.writermem.status c.StatusCode = c.writermem.status
return c return c
} }
@ -254,6 +287,12 @@ func (c *Ctx) SetHeader(key string, value string) {
c.writermem.Header().Set(key, value) c.writermem.Header().Set(key, value)
} }
func (c *Ctx) SendStatus(code int) error {
c.Status(code)
c.writermem.WriteHeaderNow()
return nil
}
func (c *Ctx) SendString(data string) error { func (c *Ctx) SendString(data string) error {
c.SetHeader("Content-Type", "text/plain") c.SetHeader("Content-Type", "text/plain")
_, err := c.Write([]byte(data)) _, err := c.Write([]byte(data))
@ -262,7 +301,7 @@ func (c *Ctx) SendString(data string) error {
func (c *Ctx) Writef(format string, values ...interface{}) (int, error) { func (c *Ctx) Writef(format string, values ...interface{}) (int, error) {
c.SetHeader("Content-Type", "text/plain") c.SetHeader("Content-Type", "text/plain")
return c.writer.Write([]byte(fmt.Sprintf(format, values...))) return c.Write([]byte(fmt.Sprintf(format, values...)))
} }
func (c *Ctx) JSON(data interface{}) error { func (c *Ctx) JSON(data interface{}) error {
@ -277,16 +316,29 @@ func (c *Ctx) JSON(data interface{}) error {
return nil return nil
} }
func (c *Ctx) RawWriter() http.ResponseWriter { func (c *Ctx) SSEvent(event string, data interface{}) error {
return c.writer c.Set("Content-Type", "text/event-stream")
c.Set("Cache-Control", "no-cache")
c.Set("Transfer-Encoding", "chunked")
return sse.Encode(c.Writer, sse.Event{Event: event, Data: data})
}
func (c *Ctx) Flush() error {
if f, ok := c.Writer.(http.Flusher); ok {
f.Flush()
return nil
}
return errors.New("http.Flusher is not implemented")
}
func (c *Ctx) HTML(html string) error {
c.SetHeader("Content-Type", "text/html")
_, err := c.Write([]byte(html))
return err
} }
func (c *Ctx) Write(data []byte) (int, error) { func (c *Ctx) Write(data []byte) (int, error) {
return c.writermem.Write(data) return c.writermem.Write(data)
} }
func (c *Ctx) HTML(html string) error {
c.SetHeader("Content-Type", "text/html")
_, err := c.writer.Write([]byte(html))
return err
}

36
go.mod
View File

@ -1,3 +1,39 @@
module github.com/loveuer/nf module github.com/loveuer/nf
go 1.20 go 1.20
require (
github.com/fatih/color v1.17.0
github.com/go-git/go-billy/v5 v5.5.0
github.com/go-git/go-git/v5 v5.12.0
github.com/google/uuid v1.6.0
github.com/spf13/cobra v1.8.1
)
require (
dario.cat/mergo v1.0.0 // indirect
github.com/Microsoft/go-winio v0.6.1 // indirect
github.com/ProtonMail/go-crypto v1.0.0 // indirect
github.com/cloudflare/circl v1.3.7 // indirect
github.com/cyphar/filepath-securejoin v0.2.4 // indirect
github.com/emirpasic/gods v1.18.1 // indirect
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect
github.com/kevinburke/ssh_config v1.2.0 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/pjbgf/sha1cd v0.3.0 // indirect
github.com/savioxavier/termlink v1.3.0 // indirect
github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 // indirect
github.com/skeema/knownhosts v1.2.2 // indirect
github.com/spf13/pflag v1.0.5 // indirect
github.com/xanzy/ssh-agent v0.3.3 // indirect
golang.org/x/crypto v0.21.0 // indirect
golang.org/x/mod v0.12.0 // indirect
golang.org/x/net v0.22.0 // indirect
golang.org/x/sys v0.18.0 // indirect
golang.org/x/tools v0.13.0 // indirect
gopkg.in/warnings.v0 v0.1.2 // indirect
)

150
go.sum
View File

@ -0,0 +1,150 @@
dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk=
dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk=
github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY=
github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migciow=
github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM=
github.com/ProtonMail/go-crypto v1.0.0 h1:LRuvITjQWX+WIfr930YHG2HNfjR1uOfyf5vE0kC2U78=
github.com/ProtonMail/go-crypto v1.0.0/go.mod h1:EjAoLdwvbIOoOQr3ihjnSoLZRtE8azugULFRteWMNc0=
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8=
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio=
github.com/bwesterb/go-ristretto v1.2.3/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0=
github.com/cloudflare/circl v1.3.3/go.mod h1:5XYMA4rFBvNIrhs50XuiBJ15vF2pZn4nnUKZrLbUZFA=
github.com/cloudflare/circl v1.3.7 h1:qlCDlTPz2n9fu58M0Nh1J/JzcFpfgkFHHX3O35r5vcU=
github.com/cloudflare/circl v1.3.7/go.mod h1:sRTcRWXGLrKw6yIGJ+l7amYJFfAXbZG0kBSc8r4zxgA=
github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/cyphar/filepath-securejoin v0.2.4 h1:Ugdm7cg7i6ZK6x3xDF1oEu1nfkyfH53EtKeQYTC3kyg=
github.com/cyphar/filepath-securejoin v0.2.4/go.mod h1:aPGpWjXOXUn2NCNjFvBE6aRxGGx79pTxQpKOJNYHHl4=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/elazarl/goproxy v0.0.0-20230808193330-2592e75ae04a h1:mATvB/9r/3gvcejNsXKSkQ6lcIaNec2nyfOdlTBR2lU=
github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc=
github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ=
github.com/fatih/color v1.17.0 h1:GlRw1BRJxkpqUCBKzKOw098ed57fEsKeNjpTe3cSjK4=
github.com/fatih/color v1.17.0/go.mod h1:YZ7TlrGPkiz6ku9fK3TLD/pl3CpsiFyu8N92HLgmosI=
github.com/gliderlabs/ssh v0.3.7 h1:iV3Bqi942d9huXnzEF2Mt+CY9gLu8DNM4Obd+8bODRE=
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI=
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic=
github.com/go-git/go-billy/v5 v5.5.0 h1:yEY4yhzCDuMGSv83oGxiBotRzhwhNr8VZyphhiu+mTU=
github.com/go-git/go-billy/v5 v5.5.0/go.mod h1:hmexnoNsr2SJU1Ju67OaNz5ASJY3+sHgFRpCtpDCKow=
github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399 h1:eMje31YglSBqCdIqdhKBW8lokaMrL3uTkpGYlE2OOT4=
github.com/go-git/go-git/v5 v5.12.0 h1:7Md+ndsjrzZxbddRDZjF14qK+NN56sy6wkqaVrjZtys=
github.com/go-git/go-git/v5 v5.12.0/go.mod h1:FTM9VKtnI2m65hNI/TenDDDnUf2Q9FHnXYjuz9i5OEY=
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE=
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A=
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo=
github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4=
github.com/kevinburke/ssh_config v1.2.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/onsi/gomega v1.27.10 h1:naR28SdDFlqrG6kScpT8VWpu1xWY5nJRCF3XaYyBjhI=
github.com/pjbgf/sha1cd v0.3.0 h1:4D5XXmUUBUl/xQ6IjCkEAbqXskkq/4O7LmGn0AqMDs4=
github.com/pjbgf/sha1cd v0.3.0/go.mod h1:nZ1rrWOcGJ5uZgEEVL1VUM9iRQiZvWdbZjkKyFzPPsI=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/savioxavier/termlink v1.3.0 h1:3Gl4FzQjUyiHzmoEDfmWEhgIwDiJY4poOQHP+k8ReA4=
github.com/savioxavier/termlink v1.3.0/go.mod h1:5T5ePUlWbxCHIwyF8/Ez1qufOoGM89RCg9NvG+3G3gc=
github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 h1:n661drycOFuPLCN3Uc8sB6B/s6Z4t2xvBgU1htSHuq8=
github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4=
github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
github.com/skeema/knownhosts v1.2.2 h1:Iug2P4fLmDw9f41PB6thxUkNUkJzB5i+1/exaj40L3A=
github.com/skeema/knownhosts v1.2.2/go.mod h1:xYbVRSPxqBZFrdmDyMmsOs+uX1UZC3nTN3ThzgDxUwo=
github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM=
github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM=
github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.3.1-0.20221117191849-2c476679df9a/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4=
golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU=
golang.org/x/crypto v0.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA=
golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.12.0 h1:rmsUpXtvNzj340zd98LZ4KntptpfRHwpFOHG188oHXc=
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
golang.org/x/net v0.22.0 h1:9sGLhx7iRIHEiX0oAJ3MRZMUCElJgy7Br1nO+AMN3Tc=
golang.org/x/net v0.22.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.3.0 h1:ftCYgMx6zT/asHUrPw8BLLscYtGznsLAnjq5RH9P66E=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4=
golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U=
golang.org/x/term v0.18.0 h1:FcHjZXDMxI8mM3nwhX9HlKop4C0YQvCVCdwYl2wOtE8=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/tools v0.13.0 h1:Iey4qkscZuv0VvIt8E0neZjtPVQFSc870HQ448QgEmQ=
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME=
gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

106
internal/sse/sse-encoder.go Normal file
View File

@ -0,0 +1,106 @@
package sse
import (
"encoding/json"
"fmt"
"io"
"net/http"
"reflect"
"strconv"
"strings"
)
// Server-Sent Events
// W3C Working Draft 29 October 2009
// http://www.w3.org/TR/2009/WD-eventsource-20091029/
const ContentType = "text/event-stream"
var contentType = []string{ContentType}
var noCache = []string{"no-cache"}
var fieldReplacer = strings.NewReplacer(
"\n", "\\n",
"\r", "\\r")
var dataReplacer = strings.NewReplacer(
"\n", "\ndata:",
"\r", "\\r")
type Event struct {
Event string
Id string
Retry uint
Data interface{}
}
func Encode(writer io.Writer, event Event) error {
w := checkWriter(writer)
writeId(w, event.Id)
writeEvent(w, event.Event)
writeRetry(w, event.Retry)
return writeData(w, event.Data)
}
func writeId(w stringWriter, id string) {
if len(id) > 0 {
w.WriteString("id:")
fieldReplacer.WriteString(w, id)
w.WriteString("\n")
}
}
func writeEvent(w stringWriter, event string) {
if len(event) > 0 {
w.WriteString("event:")
fieldReplacer.WriteString(w, event)
w.WriteString("\n")
}
}
func writeRetry(w stringWriter, retry uint) {
if retry > 0 {
w.WriteString("retry:")
w.WriteString(strconv.FormatUint(uint64(retry), 10))
w.WriteString("\n")
}
}
func writeData(w stringWriter, data interface{}) error {
w.WriteString("data:")
switch kindOfData(data) {
case reflect.Struct, reflect.Slice, reflect.Map:
err := json.NewEncoder(w).Encode(data)
if err != nil {
return err
}
w.WriteString("\n")
default:
dataReplacer.WriteString(w, fmt.Sprint(data))
w.WriteString("\n\n")
}
return nil
}
func (r Event) Render(w http.ResponseWriter) error {
r.WriteContentType(w)
return Encode(w, r)
}
func (r Event) WriteContentType(w http.ResponseWriter) {
header := w.Header()
header["Content-Type"] = contentType
if _, exist := header["Cache-Control"]; !exist {
header["Cache-Control"] = noCache
}
}
func kindOfData(data interface{}) reflect.Kind {
value := reflect.ValueOf(data)
valueType := value.Kind()
if valueType == reflect.Ptr {
valueType = value.Elem().Kind()
}
return valueType
}

24
internal/sse/writer.go Normal file
View File

@ -0,0 +1,24 @@
package sse
import "io"
type stringWriter interface {
io.Writer
WriteString(string) (int, error)
}
type stringWrapper struct {
io.Writer
}
func (w stringWrapper) WriteString(str string) (int, error) {
return w.Writer.Write([]byte(str))
}
func checkWriter(writer io.Writer) stringWriter {
if w, ok := writer.(stringWriter); ok {
return w
} else {
return stringWrapper{writer}
}
}

View File

@ -2,9 +2,11 @@ package nf
import ( import (
"fmt" "fmt"
"log" "github.com/google/uuid"
"github.com/loveuer/nf/nft/log"
"os" "os"
"runtime/debug" "runtime/debug"
"strings"
"time" "time"
) )
@ -27,51 +29,44 @@ func NewRecover(enableStackTrace bool) HandlerFunc {
} }
} }
func NewLogger() HandlerFunc { func NewLogger(traceHeader ...string) HandlerFunc {
l := log.New(os.Stdout, "[NF] ", 0) Header := "X-Trace-ID"
if len(traceHeader) > 0 && traceHeader[0] != "" {
durationFormat := func(num int64) string { Header = traceHeader[0]
var (
unit = "ns"
)
if num > 1000 {
num = num / 1000
unit = "µs"
}
if num > 1000 {
num = num / 1000
unit = "ms"
}
if num > 1000 {
num = num / 1000
unit = "s"
}
return fmt.Sprintf("%3d %2s", num, unit)
} }
return func(c *Ctx) error { return func(c *Ctx) error {
start := time.Now() var (
now = time.Now()
trace = c.Get(Header)
logFn func(msg string, data ...any)
ip = c.IP()
)
if trace == "" {
trace = uuid.Must(uuid.NewV7()).String()
}
c.SetHeader(Header, trace)
traces := strings.Split(trace, "-")
shortTrace := traces[len(traces)-1]
err := c.Next() err := c.Next()
duration := time.Since(now)
var ( msg := fmt.Sprintf("NF | %s | %15s | %3d | %s | %6s | %s", shortTrace, ip, c.StatusCode, HumanDuration(duration.Nanoseconds()), c.Method(), c.Path())
duration = time.Now().Sub(start).Nanoseconds()
status = c.StatusCode
path = c.path
method = c.Request.Method
)
l.Printf("%s | %5s | %d | %s | %s", switch {
start.Format("06/01/02T15:04:05"), case c.StatusCode >= 500:
method, logFn = log.Error
status, case c.StatusCode >= 400:
durationFormat(duration), logFn = log.Warn
path, default:
) logFn = log.Info
}
logFn(msg)
return err return err
} }

67
nft/log/default.go Normal file
View File

@ -0,0 +1,67 @@
package log
import (
"fmt"
"os"
"sync"
)
var (
nilLogger = func(prefix, timestamp, msg string, data ...any) {}
normalLogger = func(prefix, timestamp, msg string, data ...any) {
fmt.Printf(prefix+"| "+timestamp+" | "+msg+"\n", data...)
}
panicLogger = func(prefix, timestamp, msg string, data ...any) {
panic(fmt.Sprintf(prefix+"| "+timestamp+" | "+msg+"\n", data...))
}
fatalLogger = func(prefix, timestamp, msg string, data ...any) {
fmt.Printf(prefix+"| "+timestamp+" | "+msg+"\n", data...)
os.Exit(1)
}
defaultLogger = &logger{
Mutex: sync.Mutex{},
timeFormat: "2006-01-02T15:04:05",
writer: os.Stdout,
level: LogLevelInfo,
debug: nilLogger,
info: normalLogger,
warn: normalLogger,
error: normalLogger,
panic: panicLogger,
fatal: fatalLogger,
}
)
func SetTimeFormat(format string) {
defaultLogger.SetTimeFormat(format)
}
func SetLogLevel(level LogLevel) {
defaultLogger.SetLogLevel(level)
}
func Debug(msg string, data ...any) {
defaultLogger.Debug(msg, data...)
}
func Info(msg string, data ...any) {
defaultLogger.Info(msg, data...)
}
func Warn(msg string, data ...any) {
defaultLogger.Warn(msg, data...)
}
func Error(msg string, data ...any) {
defaultLogger.Error(msg, data...)
}
func Panic(msg string, data ...any) {
defaultLogger.Panic(msg, data...)
}
func Fatal(msg string, data ...any) {
defaultLogger.Fatal(msg, data...)
}

115
nft/log/log.go Normal file
View File

@ -0,0 +1,115 @@
package log
import (
"github.com/fatih/color"
"io"
"sync"
"time"
)
type LogLevel uint32
const (
LogLevelDebug = iota
LogLevelInfo
LogLevelWarn
LogLevelError
LogLevelPanic
LogLevelFatal
)
type logger struct {
sync.Mutex
timeFormat string
writer io.Writer
level LogLevel
debug func(prefix, timestamp, msg string, data ...any)
info func(prefix, timestamp, msg string, data ...any)
warn func(prefix, timestamp, msg string, data ...any)
error func(prefix, timestamp, msg string, data ...any)
panic func(prefix, timestamp, msg string, data ...any)
fatal func(prefix, timestamp, msg string, data ...any)
}
var (
red = color.New(color.FgRed)
hired = color.New(color.FgHiRed)
green = color.New(color.FgGreen)
yellow = color.New(color.FgYellow)
white = color.New(color.FgWhite)
)
func (l *logger) SetTimeFormat(format string) {
l.Lock()
defer l.Unlock()
l.timeFormat = format
}
func (l *logger) SetLogLevel(level LogLevel) {
l.Lock()
defer l.Unlock()
if level > LogLevelDebug {
l.debug = nilLogger
} else {
l.debug = normalLogger
}
if level > LogLevelInfo {
l.info = nilLogger
} else {
l.info = normalLogger
}
if level > LogLevelWarn {
l.warn = nilLogger
} else {
l.warn = normalLogger
}
if level > LogLevelError {
l.error = nilLogger
} else {
l.error = normalLogger
}
if level > LogLevelPanic {
l.panic = nilLogger
} else {
l.panic = panicLogger
}
if level > LogLevelFatal {
l.fatal = nilLogger
} else {
l.fatal = fatalLogger
}
}
func (l *logger) Debug(msg string, data ...any) {
l.debug(white.Sprint("Debug "), time.Now().Format(l.timeFormat), msg, data...)
}
func (l *logger) Info(msg string, data ...any) {
l.info(green.Sprint("Info "), time.Now().Format(l.timeFormat), msg, data...)
}
func (l *logger) Warn(msg string, data ...any) {
l.warn(yellow.Sprint("Warn "), time.Now().Format(l.timeFormat), msg, data...)
}
func (l *logger) Error(msg string, data ...any) {
l.error(red.Sprint("Error "), time.Now().Format(l.timeFormat), msg, data...)
}
func (l *logger) Panic(msg string, data ...any) {
l.panic(hired.Sprint("Panic "), time.Now().Format(l.timeFormat), msg, data...)
}
func (l *logger) Fatal(msg string, data ...any) {
l.fatal(hired.Sprint("Fatal "), time.Now().Format(l.timeFormat), msg, data...)
}
type WroteLogger interface {
Info(msg string, data ...any)
}

21
nft/log/new.go Normal file
View File

@ -0,0 +1,21 @@
package log
import (
"os"
"sync"
)
func New() *logger {
return &logger{
Mutex: sync.Mutex{},
timeFormat: "2006-01-02T15:04:05",
writer: os.Stdout,
level: LogLevelInfo,
debug: nilLogger,
info: normalLogger,
warn: normalLogger,
error: normalLogger,
panic: panicLogger,
fatal: fatalLogger,
}
}

36
nft/nfctl/clone/clone.go Normal file
View File

@ -0,0 +1,36 @@
package clone
import (
"fmt"
"github.com/go-git/go-git/v5"
_ "github.com/go-git/go-git/v5"
"github.com/go-git/go-git/v5/plumbing/transport/http"
"github.com/loveuer/nf/nft/log"
"net/url"
)
func Clone(pwd string, ins *url.URL) error {
uri := fmt.Sprintf("%s://%s%s", ins.Scheme, ins.Host, ins.Path)
opt := &git.CloneOptions{
URL: uri,
Depth: 1,
InsecureSkipTLS: true,
SingleBranch: true,
}
if ins.User != nil {
password, _ := ins.User.Password()
opt.Auth = &http.BasicAuth{
Username: ins.User.Username(),
Password: password,
}
}
log.Info("start clone %s", uri)
_, err := git.PlainClone(pwd, false, opt)
if err != nil {
return err
}
return nil
}

19
nft/nfctl/cmd/cmd.go Normal file
View File

@ -0,0 +1,19 @@
package cmd
import "github.com/spf13/cobra"
var (
Root = &cobra.Command{
Use: "nfctl",
Short: "nfctl: easy start your nf backend work",
}
)
func init() {
initNew()
Root.AddCommand(
versionCmd,
cmdNew,
)
}

133
nft/nfctl/cmd/new.go Normal file
View File

@ -0,0 +1,133 @@
package cmd
import (
"errors"
"fmt"
"github.com/loveuer/nf/nft/log"
"github.com/loveuer/nf/nft/nfctl/clone"
"github.com/loveuer/nf/nft/nfctl/opt"
"github.com/loveuer/nf/nft/nfctl/tp"
"github.com/spf13/cobra"
"net/url"
"os"
"path"
)
var (
cmdNew = &cobra.Command{
Use: "new",
Short: "nfctl new: start new project",
Example: `nfctl new {project} -t ultone [recommend]
nfctl new {project} -t https://github.com/loveuer/ultone.git
nfctl new {project} --template http://username:token@my.gitlab.com/my-zone/my-repo.git
`,
SilenceUsage: true,
}
template string
disableInit bool
preTemplateMap = map[string]string{
"ultone": "https://gitcode.com/loveuer/ultone.git",
}
)
func initNew() {
cmdNew.Flags().BoolVar(&opt.Debug, "debug", false, "debug mode")
cmdNew.Flags().StringVarP(&template, "template", "t", "", "template name/url[example:ultone, https://github.com/xxx/yyy.git]")
cmdNew.Flags().BoolVar(&disableInit, "without-init", false, "don't run template init script")
cmdNew.RunE = func(cmd *cobra.Command, args []string) error {
if opt.Debug {
log.SetLogLevel(log.LogLevelDebug)
}
var (
err error
urlIns *url.URL
pwd string
projectDir string
initBs []byte
renderBs []byte
scripts []tp.Cmd
)
if len(args) == 0 {
return fmt.Errorf("project name required")
}
if pwd, err = os.Getwd(); err != nil {
return fmt.Errorf("get work dir err")
}
projectDir = path.Join(pwd, args[0])
if _, err = os.Stat(projectDir); !errors.Is(err, os.ErrNotExist) {
return fmt.Errorf("project folder already exist")
}
if err = os.MkdirAll(projectDir, 0750); err != nil {
return fmt.Errorf("create project dir err: %v", err)
}
defer func() {
if err != nil {
_ = os.RemoveAll(projectDir)
}
}()
if template == "" {
// todo no template new project
return fmt.Errorf("😥create basic project(without template) comming soon...")
}
cloneUrl := template
if ptUrl, ok := preTemplateMap[cloneUrl]; ok {
cloneUrl = ptUrl
}
if urlIns, err = url.Parse(cloneUrl); err != nil {
return fmt.Errorf("invalid clone url: %v", err)
}
if err = clone.Clone(projectDir, urlIns); err != nil {
return fmt.Errorf("clone template err: %v", err)
}
if initBs, err = os.ReadFile(path.Join(projectDir, ".nfctl")); err != nil {
if errors.Is(err, os.ErrNotExist) {
return nil
}
return fmt.Errorf("read nfctl script file err: %v", err)
}
if renderBs, err = tp.RenderVar(initBs, map[string]any{
"PROJECT_NAME": args[0],
}); err != nil {
return fmt.Errorf("render template init script err: %v", err)
}
if scripts, err = tp.ParseCmd(projectDir, renderBs); err != nil {
return fmt.Errorf("parse template init script err: %v", err)
}
for _, script := range scripts {
if opt.Debug {
log.Debug("start script:\n%s\n", script.String())
}
if err = script.Execute(); err != nil {
return fmt.Errorf("execute template init script err: %v", err)
}
}
if err = os.RemoveAll(path.Join(projectDir, ".git")); err != nil {
log.Warn("remove .git folder err: %s", err.Error())
}
log.Info("🎉 create project [%s] 成功!!!", args[0])
return nil
}
}

17
nft/nfctl/cmd/version.go Normal file
View File

@ -0,0 +1,17 @@
package cmd
import (
"github.com/loveuer/nf/nft/log"
"github.com/loveuer/nf/nft/nfctl/version"
"github.com/spf13/cobra"
)
var (
versionCmd = &cobra.Command{
Use: "version",
Short: "print nfctl version and exit",
Run: func(cmd *cobra.Command, args []string) {
log.Info("version: %s", version.Version)
},
}
)

26
nft/nfctl/main.go Normal file
View File

@ -0,0 +1,26 @@
package main
import (
"context"
"github.com/loveuer/nf/nft/nfctl/cmd"
"github.com/loveuer/nf/nft/nfctl/version"
"os/signal"
"syscall"
"time"
)
func main() {
ctx, cancel := signal.NotifyContext(context.Background(), syscall.SIGHUP, syscall.SIGINT, syscall.SIGTERM, syscall.SIGQUIT)
defer cancel()
version.Check()
defer version.Fn()
_ = cmd.Root.ExecuteContext(ctx)
select {
case <-time.After(3 * time.Second):
case <-ctx.Done():
case <-version.OkCh:
}
}

5
nft/nfctl/opt/var.go Normal file
View File

@ -0,0 +1,5 @@
package opt
var (
Debug bool
)

23
nft/nfctl/readme.md Normal file
View File

@ -0,0 +1,23 @@
# nfctl
# 通过 nfctl 快速开启后台项目
### 1. Installation
-`go install github.com/loveuer/nf/nft/nfctl@latest`
- ② download prebuild binary [release](https://github.com/loveuer/nf/releases)
### 2. Usage
- `nfctl new {project}`
- `nfctl new project -t ultone`
- `nfctl new project -t https://github.com/xxx/yyy.git`
- `nfctl new project --template https://gitcode/loveuer/ultone.git`
- `nfctl new project --template https://{username}:{password/token}@my.gitlab.com/name/project.git`
### 3. nfctl init script
- `为方便模版的初始化, 可以采用 nfctl init script, 当 nfctl new project -t xxx 从模版开始项目时会自动执行`
- `具体的编写规则如下:`
* [init 脚本规则](https://github.com/loveuer/nf/nft/nfctl/script.md) 或者
* [国内](https://gitcode.com/loveuer/nf/nft/nfctl/script.md)

80
nft/nfctl/tp/parse.go Normal file
View File

@ -0,0 +1,80 @@
package tp
import (
"bufio"
"bytes"
"fmt"
"strings"
)
func ParseCmd(pwd string, content []byte) ([]Cmd, error) {
var (
err error
cmds = make([]Cmd, 0)
start = false
)
scanner := bufio.NewScanner(bytes.NewReader(content))
scanner.Buffer(make([]byte, 1024), 1024*1024*10)
record := make([]string, 0)
for scanner.Scan() {
line := strings.TrimSpace(scanner.Text())
if len(line) == 0 {
continue
}
if !start && strings.HasPrefix(line, "#") {
continue
}
if strings.HasPrefix(line, "!") {
if start {
return nil, fmt.Errorf("invalid content: unEOF cmd block found")
}
start = true
record = append(record, line)
continue
}
if strings.HasPrefix(line, "EOF") {
start = false
if len(record) == 0 {
continue
}
var cmd Cmd
if cmd, err = ParseBlock(pwd, record); err != nil {
return nil, err
}
cmds = append(cmds, cmd)
record = record[:0]
continue
}
if start {
record = append(record, line)
}
}
if err = scanner.Err(); err != nil {
return nil, err
}
return cmds, err
}
func ParseBlock(pwd string, lines []string) (Cmd, error) {
switch lines[0] {
case "!replace content":
return newReplaceContent(pwd, lines[1:])
case "!replace name":
return newReplaceName(pwd, lines[1:])
case "!generate":
return newGenerate(pwd, lines[1:])
}
return nil, fmt.Errorf("invalid cmd block: unknown type: %s", lines[0])
}

View File

@ -0,0 +1,37 @@
package tp
import (
"github.com/loveuer/nf/nft/log"
"os"
"testing"
)
func TestParseInitFile(t *testing.T) {
bs, err := os.ReadFile("xtest")
if err != nil {
log.Fatal(err.Error())
}
data := map[string]any{
"PROJECT_NAME": "myproject",
}
result, err := RenderVar(bs, data)
if err != nil {
log.Fatal(err.Error())
}
pwd, _ := os.Getwd()
cmds, err := ParseCmd(pwd, result)
if err != nil {
log.Fatal(err.Error())
}
for _, item := range cmds {
log.Info("one cmd => %s\n\n", item.String())
if err = item.Execute(); err != nil {
log.Fatal(err.Error())
}
}
}

29
nft/nfctl/tp/render.go Normal file
View File

@ -0,0 +1,29 @@
package tp
import (
"bytes"
"text/template"
)
var (
_t *template.Template
)
func init() {
_t = template.New("tp")
}
func RenderVar(t []byte, data map[string]any) ([]byte, error) {
tr, err := _t.Parse(string(t))
if err != nil {
return nil, err
}
var buf bytes.Buffer
if err = tr.Execute(&buf, data); err != nil {
return nil, err
}
return buf.Bytes(), nil
}

316
nft/nfctl/tp/tp.go Normal file
View File

@ -0,0 +1,316 @@
package tp
import (
"bufio"
"bytes"
"fmt"
"github.com/loveuer/nf/nft/log"
"io/fs"
"os"
"path"
"path/filepath"
"regexp"
"strings"
)
type Cmd interface {
String() string
Execute() error
}
var (
_ Cmd = (*Generate)(nil)
_ Cmd = (*ReplaceContent)(nil)
_ Cmd = (*ReplaceName)(nil)
)
type Generate struct {
pwd string
filename string
content []string
}
func (t *Generate) String() string {
return fmt.Sprintf("!generate\n%s\n%s\n", t.filename, strings.Join(t.content, "\n"))
}
func (t *Generate) Execute() error {
var (
err error
location = t.filename
input *os.File
)
log.Debug("[Generate] generate[%s]", t.filename)
if !path.IsAbs(t.filename) {
location = path.Join(t.pwd, t.filename)
}
if err = os.MkdirAll(path.Dir(location), 0644); err != nil {
return err
}
if !strings.HasSuffix(location, "/") {
if input, err = os.OpenFile(location, os.O_CREATE|os.O_TRUNC|os.O_RDWR, 0744); err != nil {
return err
}
if len(t.content) > 0 {
content := strings.Join(t.content, "\n")
_, err = input.WriteString(content)
return err
}
}
return nil
}
func newGenerate(pwd string, lines []string) (*Generate, error) {
if len(lines) == 0 {
return nil, fmt.Errorf("generate cmd require file/folder name")
}
return &Generate{
pwd: pwd,
filename: lines[0],
content: lines[1:],
}, nil
}
type replaceNameMatchType int
const (
replaceNameMatchReg replaceNameMatchType = iota + 1
replaceNameMatchExact
replaceNameMatchPrefix
replaceNameMatchSuffix
)
func (rm replaceNameMatchType) Label() string {
switch rm {
case replaceNameMatchReg:
return "reg"
case replaceNameMatchExact:
return "exact"
case replaceNameMatchPrefix:
return "prefix"
case replaceNameMatchSuffix:
return "suffix"
}
log.Panic("unknown replace match type: %v", rm)
return ""
}
type ReplaceContent struct {
pwd string
name string
content string
targetName string
matchType replaceNameMatchType
fromContent string
targetEmpty bool
targetContent string
}
func (t *ReplaceContent) String() string {
return fmt.Sprintf("!replace content\n%s\n%s\n", t.name, t.content)
}
func (t *ReplaceContent) Execute() error {
var (
fn filepath.WalkFunc
handler = func(location string) error {
bs, err := os.ReadFile(location)
if err != nil {
return err
}
log.Debug("[ReplaceContent] handle[%s] replace [%s] => [%s]", location, t.fromContent, t.targetContent)
newbs, err := t.executeFile(bs)
if err != nil {
return err
}
return os.WriteFile(location, newbs, 0644)
}
)
switch t.matchType {
case replaceNameMatchExact:
fn = func(location string, info fs.FileInfo, err error) error {
if location == path.Join(t.pwd, t.targetName) {
log.Debug("[ReplaceContent] exact match: %s", location)
return handler(location)
}
return nil
}
case replaceNameMatchPrefix:
fn = func(location string, info fs.FileInfo, err error) error {
if strings.HasPrefix(path.Base(location), t.targetName) {
log.Debug("[ReplaceContent] prefix match: %s", location)
return handler(location)
}
return nil
}
case replaceNameMatchSuffix:
fn = func(location string, info fs.FileInfo, err error) error {
if strings.HasSuffix(location, t.targetName) {
log.Debug("[ReplaceContent] suffix match: %s", location)
return handler(location)
}
return nil
}
case replaceNameMatchReg:
fn = func(location string, info fs.FileInfo, err error) error {
if match, err := regexp.MatchString(t.targetName, location); err == nil && match {
log.Debug("[ReplaceContent] reg match: %s", location)
return handler(location)
}
return nil
}
}
return filepath.Walk(t.pwd, fn)
}
func (t *ReplaceContent) executeFile(raw []byte) ([]byte, error) {
scanner := bufio.NewScanner(bytes.NewReader(raw))
scanner.Buffer(make([]byte, 1024), 1024*1024)
lines := make([]string, 0)
for scanner.Scan() {
line := scanner.Text()
lines = append(
lines,
strings.ReplaceAll(line, t.fromContent, t.targetContent),
)
}
return []byte(strings.Join(lines, "\n")), nil
}
func newReplaceContent(pwd string, lines []string) (*ReplaceContent, error) {
if len(lines) != 2 {
return nil, fmt.Errorf("invalid replace_content cmd: required 2 lines params")
}
var (
name = lines[0]
content = lines[1]
matchType replaceNameMatchType
)
names := strings.SplitN(name, " ", 2)
if len(names) != 2 {
return nil, fmt.Errorf("invalid replace_content cmd: name line, required: [reg/exact/prefix/shuffix] {filename}")
}
switch names[0] {
case "exact":
matchType = replaceNameMatchExact
case "reg":
matchType = replaceNameMatchReg
case "prefix":
matchType = replaceNameMatchPrefix
case "suffix":
matchType = replaceNameMatchSuffix
default:
return nil, fmt.Errorf("invalid replace_content name match type, example: [reg *.go] [exact go.mod]")
}
var (
targetName string = names[1]
targetEmpty = false
targetContent string
)
contents := strings.SplitN(content, "=>", 2)
fromContent := strings.TrimSpace(contents[0])
if len(contents) == 1 {
targetEmpty = true
} else {
if targetContent = strings.TrimSpace(contents[1]); targetContent == "" || targetContent == `""` || targetContent == `''` {
targetEmpty = true
}
}
return &ReplaceContent{
pwd: pwd,
name: name,
content: content,
matchType: matchType,
targetName: targetName,
fromContent: fromContent,
targetEmpty: targetEmpty,
targetContent: targetContent,
}, nil
}
type ReplaceName struct {
pwd string
line string
targetEmpty bool
fromContent string
targetContent string
}
func (t *ReplaceName) String() string {
return fmt.Sprintf("!replace name\n%s\n", t.line)
}
func (t *ReplaceName) Execute() error {
fullpath := path.Join(t.pwd, t.fromContent)
if t.targetEmpty {
return os.RemoveAll(fullpath)
}
ftpath := path.Join(t.pwd, t.targetContent)
return os.Rename(fullpath, ftpath)
}
func newReplaceName(pwd string, lines []string) (*ReplaceName, error) {
if len(lines) != 1 {
return nil, fmt.Errorf("replace_name need one line param, for example: mian.go => main.go")
}
var (
content = lines[0]
targetEmpty = false
fromContent string
targetContent string
)
contents := strings.SplitN(content, "=>", 2)
fromContent = strings.TrimSpace(contents[0])
if len(contents) == 1 {
targetEmpty = true
} else {
if targetContent = strings.TrimSpace(contents[1]); targetContent == "" || targetContent == `""` || targetContent == `''` {
targetEmpty = true
}
}
if !targetEmpty {
if (strings.HasPrefix(targetContent, `"`) && strings.HasSuffix(targetContent, `"`)) || (strings.HasPrefix(targetContent, `'`) && strings.HasSuffix(targetContent, `'`)) {
targetContent = targetContent[1 : len(targetContent)-1]
}
}
return &ReplaceName{
pwd: pwd,
line: content,
targetEmpty: targetEmpty,
fromContent: fromContent,
targetContent: targetContent,
}, nil
}

View File

@ -0,0 +1,68 @@
package version
import (
"bufio"
"fmt"
"github.com/fatih/color"
"github.com/loveuer/nf/nft/log"
"github.com/savioxavier/termlink"
"net/http"
"strings"
"sync"
)
const Version = "v24.07.13-r1"
var (
lk = &sync.Mutex{}
empty = func() {}
upgrade = func(v string) func() {
return func() {
color.Green("\n🎉 🎉 🎉 [nfctl] New Version Found: %s", v)
color.Cyan("Upgrade it with: [go install github.com/loveuer/nf/nft/nfctl@master]")
fmt.Print("Or Download by: ")
color.Cyan(termlink.Link("Releases", "https://github.com/loveuer/nf/releases"))
fmt.Println()
}
}
Fn = empty
OkCh = make(chan struct{}, 1)
)
func Check() {
ready := make(chan struct{})
go func() {
ready <- struct{}{}
uri := "https://raw.gitcode.com/loveuer/nf/raw/master/nft/nfctl/version/version.go"
prefix := "const Version = "
resp, err := http.Get(uri)
if err != nil {
log.Debug("[Check] http get[%s] err: %v", uri, err.Error())
return
}
defer resp.Body.Close()
scanner := bufio.NewScanner(resp.Body)
scanner.Buffer(make([]byte, 16*1024), 1024*1024)
for scanner.Scan() {
line := scanner.Text()
log.Debug("[Check] version.go line: %s", line)
if strings.HasPrefix(line, prefix) {
v := strings.TrimPrefix(line, prefix)
if len(v) > 2 {
v = v[1 : len(v)-1]
}
if v != "" && v > Version {
lk.Lock()
Fn = upgrade(v)
lk.Unlock()
OkCh <- struct{}{}
return
}
}
}
}()
<-ready
}

View File

@ -1 +0,0 @@
package nf

View File

@ -3,6 +3,7 @@ package nf
import ( import (
"bufio" "bufio"
"io" "io"
"log"
"net" "net"
"net/http" "net/http"
) )
@ -41,6 +42,7 @@ type ResponseWriter interface {
type responseWriter struct { type responseWriter struct {
http.ResponseWriter http.ResponseWriter
written bool
size int size int
status int status int
} }
@ -60,7 +62,7 @@ func (w *responseWriter) reset(writer http.ResponseWriter) {
func (w *responseWriter) WriteHeader(code int) { func (w *responseWriter) WriteHeader(code int) {
if code > 0 && w.status != code { if code > 0 && w.status != code {
if w.Written() { if w.Written() {
// todo: debugPrint("[WARNING] Headers were already written. Wanted to override status code %d with %d", w.status, code) log.Printf("[NF] WARNING: Headers were already written. Wanted to override status code %d with %d", w.status, code)
return return
} }
w.status = code w.status = code
@ -102,7 +104,7 @@ func (w *responseWriter) Size() int {
} }
func (w *responseWriter) Written() bool { func (w *responseWriter) Written() bool {
return w.size != noWritten return w.size != noWritten || w.written
} }
// Hijack implements the http.Hijacker interface. // Hijack implements the http.Hijacker interface.

21
util.go
View File

@ -202,3 +202,24 @@ func bufApp(buf *[]byte, s string, w int, c byte) {
} }
b[w] = c b[w] = c
} }
func HumanDuration(nano int64) string {
duration := float64(nano)
unit := "ns"
if duration >= 1000 {
duration /= 1000
unit = "us"
}
if duration >= 1000 {
duration /= 1000
unit = "ms"
}
if duration >= 1000 {
duration /= 1000
unit = " s"
}
return fmt.Sprintf("%6.2f%s", duration, unit)
}

View File

@ -1,14 +0,0 @@
### basic - get
GET http://127.0.0.1/hello/nf
### test resp error
GET http://127.0.0.1/error
### test basic post
POST http://127.0.0.1/data
Content-Type: application/json
{
"name": "nice"
}

View File

@ -1,17 +0,0 @@
package main
import (
"github.com/loveuer/nf"
"log"
)
func main() {
app := nf.New(nf.Config{EnableNotImplementHandler: true})
api := app.Group("/api")
api.Get("/1", func(c *nf.Ctx) error {
return c.SendString("nice")
})
log.Fatal(app.Run(":80"))
}

View File

@ -1,9 +0,0 @@
### body_limit
POST http://127.0.0.1/data
Content-Type: application/json
{
"name": "zyp",
"age": 19,
"likes": ["2233"]
}

View File

@ -1,50 +0,0 @@
package main
import (
"github.com/loveuer/nf"
"log"
)
func main() {
app := nf.New(nf.Config{BodyLimit: 30})
app.Post("/data", func(c *nf.Ctx) error {
type Req struct {
Name string `json:"name"`
Age int `json:"age"`
Likes []string `json:"likes"`
}
var (
err error
req = new(Req)
)
if err = c.BodyParser(req); err != nil {
return c.JSON(nf.Map{"status": 400, "err": err.Error()})
}
return c.JSON(nf.Map{"status": 200, "data": req})
})
app.Post("/url", func(c *nf.Ctx) error {
type Req struct {
Name string `form:"name"`
Age int `form:"age"`
Likes []string `form:"likes"`
}
var (
err error
req = new(Req)
)
if err = c.BodyParser(req); err != nil {
return c.JSON(nf.Map{"status": 400, "err": err.Error()})
}
return c.JSON(nf.Map{"status": 200, "data": req})
})
log.Fatal(app.Run("0.0.0.0:80"))
}

View File

@ -1,28 +0,0 @@
package main
import (
"github.com/loveuer/nf"
"log"
)
func main() {
app := nf.New(nf.Config{DisableLogger: false})
app.Get("/hello", func(c *nf.Ctx) error {
return c.SendString("world")
})
app.Use(ml())
log.Fatal(app.Run(":80"))
}
func ml() nf.HandlerFunc {
return func(c *nf.Ctx) error {
index := []byte(`<h1>my not found</h1>`)
c.Set("Content-Type", "text/html")
c.Status(403)
_, err := c.Write(index)
return err
}
}

View File

@ -1,34 +0,0 @@
package main
import (
"errors"
"github.com/loveuer/nf"
"github.com/loveuer/nf/nft/resp"
"log"
)
func main() {
app := nf.New()
api := app.Group("/api")
api.Get("/hello",
auth(),
func(c *nf.Ctx) error {
return resp.Resp403(c, errors.New("in hello"))
},
)
log.Fatal(app.Run(":80"))
}
func auth() nf.HandlerFunc {
return func(c *nf.Ctx) error {
token := c.Query("token")
if token != "zyp" {
return resp.Resp401(c, errors.New("no auth"))
}
return c.Next()
}
}

View File

@ -1,28 +0,0 @@
package main
import (
"github.com/loveuer/nf"
"log"
)
func main() {
app := nf.New()
app.Get("/nice", h1, h2)
log.Fatal(app.Run(":80"))
}
func h1(c *nf.Ctx) error {
you := c.Query("to")
if you == "you" {
return c.JSON(nf.Map{"status": 201, "msg": "nice to meet you"})
}
//return c.Next()
return nil
}
func h2(c *nf.Ctx) error {
return c.JSON(nf.Map{"status": 200, "msg": "hello world"})
}

View File

@ -1,5 +0,0 @@
### test multi handlers no next
GET http://127.0.0.1:3333/nice?to=you
### test multi handlers do next
GET http://127.0.0.1:3333/nice?to=nf

View File

@ -1,24 +0,0 @@
package main
import (
"github.com/loveuer/nf"
"log"
)
func main() {
app := nf.New(nf.Config{
DisableRecover: false,
})
app.Get("/hello/:name", func(c *nf.Ctx) error {
name := c.Param("name")
if name == "nf" {
panic("name is nf")
}
return c.JSON("nice")
})
log.Fatal(app.Run("0.0.0.0:80"))
}

View File

@ -1,5 +0,0 @@
### panic test
GET http://127.0.0.1/hello/nf
### if covered?
GET http://127.0.0.1/hello/world

View File

@ -1,36 +0,0 @@
package main
import (
"github.com/loveuer/nf"
"log"
)
func main() {
app := nf.New()
app.Get("/hello", func(c *nf.Ctx) error {
type Req struct {
Name string `query:"name"`
Age int `query:"age"`
Likes []string `query:"likes"`
}
var (
err error
req = new(Req)
rm = make(map[string]interface{})
)
//if err = c.QueryParser(req); err != nil {
// return nf.NewNFError(400, "1:"+err.Error())
//}
if err = c.QueryParser(&rm); err != nil {
return nf.NewNFError(400, "2:"+err.Error())
}
return c.JSON(nf.Map{"status": 200, "data": req, "map": rm})
})
log.Fatal(app.Run("0.0.0.0:80"))
}

View File

@ -1,52 +0,0 @@
package main
import (
"context"
"github.com/loveuer/nf"
"log"
"time"
)
var (
app = nf.New()
quit = make(chan bool)
)
func main() {
app.Get("/name", handleGet)
go func() {
err := app.Run(":80")
log.Print("run with err=", err)
quit <- true
}()
<-quit
}
func handleGet(c *nf.Ctx) error {
type Req struct {
Name string `query:"name"`
Addr []string `query:"addr"`
}
var (
err error
req = Req{}
)
if err = c.QueryParser(&req); err != nil {
return nf.NewNFError(400, err.Error())
}
if req.Name == "quit" {
go func() {
time.Sleep(2 * time.Second)
log.Print("app quit = ", app.Shutdown(context.TODO()))
}()
}
return c.JSON(nf.Map{"req_map": req})
}

View File

@ -1,119 +0,0 @@
package main
import (
"bytes"
"crypto/rand"
"crypto/rsa"
"crypto/tls"
"crypto/x509"
"crypto/x509/pkix"
"encoding/pem"
"github.com/loveuer/nf"
"log"
"math/big"
"net"
"time"
)
func main() {
app := nf.New(nf.Config{
DisableHttpErrorLog: true,
})
app.Get("/hello/:name", func(c *nf.Ctx) error {
return c.SendString("hello, " + c.Param("name"))
})
st, _, _ := GenerateTlsConfig()
log.Fatal(app.RunTLS(":443", st))
}
func GenerateTlsConfig() (serverTLSConf *tls.Config, clientTLSConf *tls.Config, err error) {
ca := &x509.Certificate{
SerialNumber: big.NewInt(2019),
Subject: pkix.Name{
Organization: []string{"Company, INC."},
Country: []string{"US"},
Province: []string{""},
Locality: []string{"San Francisco"},
StreetAddress: []string{"Golden Gate Bridge"},
PostalCode: []string{"94016"},
},
NotBefore: time.Now(),
NotAfter: time.Now().AddDate(99, 0, 0),
IsCA: true,
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth, x509.ExtKeyUsageServerAuth},
KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign,
BasicConstraintsValid: true,
}
// create our private and public key
caPrivKey, err := rsa.GenerateKey(rand.Reader, 4096)
if err != nil {
return nil, nil, err
}
// create the CA
caBytes, err := x509.CreateCertificate(rand.Reader, ca, ca, &caPrivKey.PublicKey, caPrivKey)
if err != nil {
return nil, nil, err
}
// pem encode
caPEM := new(bytes.Buffer)
pem.Encode(caPEM, &pem.Block{
Type: "CERTIFICATE",
Bytes: caBytes,
})
caPrivKeyPEM := new(bytes.Buffer)
pem.Encode(caPrivKeyPEM, &pem.Block{
Type: "RSA PRIVATE KEY",
Bytes: x509.MarshalPKCS1PrivateKey(caPrivKey),
})
// set up our server certificate
cert := &x509.Certificate{
SerialNumber: big.NewInt(2019),
Subject: pkix.Name{
Organization: []string{"Company, INC."},
Country: []string{"US"},
Province: []string{""},
Locality: []string{"San Francisco"},
StreetAddress: []string{"Golden Gate Bridge"},
PostalCode: []string{"94016"},
},
IPAddresses: []net.IP{net.IPv4(127, 0, 0, 1), net.IPv6loopback},
NotBefore: time.Now(),
NotAfter: time.Now().AddDate(1, 0, 0),
SubjectKeyId: []byte{1, 2, 3, 4, 6},
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth, x509.ExtKeyUsageServerAuth},
KeyUsage: x509.KeyUsageDigitalSignature,
}
certPrivKey, err := rsa.GenerateKey(rand.Reader, 4096)
if err != nil {
return nil, nil, err
}
certBytes, err := x509.CreateCertificate(rand.Reader, cert, ca, &certPrivKey.PublicKey, caPrivKey)
if err != nil {
return nil, nil, err
}
certPEM := new(bytes.Buffer)
pem.Encode(certPEM, &pem.Block{
Type: "CERTIFICATE",
Bytes: certBytes,
})
certPrivKeyPEM := new(bytes.Buffer)
pem.Encode(certPrivKeyPEM, &pem.Block{
Type: "RSA PRIVATE KEY",
Bytes: x509.MarshalPKCS1PrivateKey(certPrivKey),
})
serverCert, err := tls.X509KeyPair(certPEM.Bytes(), certPrivKeyPEM.Bytes())
if err != nil {
return nil, nil, err
}
serverTLSConf = &tls.Config{
Certificates: []tls.Certificate{serverCert},
}
certpool := x509.NewCertPool()
certpool.AppendCertsFromPEM(caPEM.Bytes())
clientTLSConf = &tls.Config{
RootCAs: certpool,
}
return
}