Compare commits

...

4 Commits

21 changed files with 182 additions and 486 deletions

1
.gitignore vendored
View File

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

6
app.go
View File

@ -19,8 +19,6 @@ var (
regSafePrefix = regexp.MustCompile("[^a-zA-Z0-9/-]+")
regRemoveRepeatedChar = regexp.MustCompile("/{2,}")
mimePlain = []string{"text/plain"}
)
type App struct {
@ -165,8 +163,9 @@ func (a *App) handleHTTPRequest(c *Ctx) {
// Find route in tree
value := root.getValue(rPath, c.params, c.skippedNodes, unescape)
if value.params != nil {
c.Params = *value.params
c.params = value.params
}
if value.handlers != nil {
c.handlers = value.handlers
c.fullPath = value.fullPath
@ -175,7 +174,6 @@ func (a *App) handleHTTPRequest(c *Ctx) {
serveError(c, errorHandler)
}
c.writermem.WriteHeaderNow()
return
}
if httpMethod != http.MethodConnect && rPath != "/" {

59
ctx.go
View File

@ -3,23 +3,24 @@ package nf
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"github.com/loveuer/nf/internal/sse"
"io"
"mime/multipart"
"net"
"net/http"
"strings"
"sync"
)
type Ctx struct {
lock sync.Mutex
writermem responseWriter
// origin objects
writer http.ResponseWriter
Request *http.Request
// request info
path string
method string
// response info
StatusCode int
app *App
@ -29,7 +30,6 @@ type Ctx struct {
locals map[string]interface{}
skippedNodes *[]skippedNode
fullPath string
Params Params
}
func newContext(app *App, writer http.ResponseWriter, request *http.Request) *Ctx {
@ -38,8 +38,8 @@ func newContext(app *App, writer http.ResponseWriter, request *http.Request) *Ct
v := make(Params, 0, app.maxParams)
ctx := &Ctx{
lock: sync.Mutex{},
writer: writer,
writermem: responseWriter{},
Request: request,
path: request.URL.Path,
method: request.Method,
@ -144,7 +144,15 @@ func (c *Ctx) verify() error {
}
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 {
@ -236,11 +244,6 @@ func (c *Ctx) BodyParser(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())
}
@ -249,8 +252,12 @@ func (c *Ctx) QueryParser(out interface{}) error {
=============================================================== */
func (c *Ctx) Status(code int) *Ctx {
c.lock.Lock()
defer c.lock.Unlock()
c.writermem.WriteHeader(code)
c.StatusCode = c.writermem.status
return c
}
@ -263,7 +270,8 @@ func (c *Ctx) SetHeader(key string, value string) {
}
func (c *Ctx) SendStatus(code int) error {
c.writermem.WriteHeader(code)
c.Status(code)
c.writermem.WriteHeaderNow()
return nil
}
@ -275,7 +283,7 @@ func (c *Ctx) SendString(data string) error {
func (c *Ctx) Writef(format string, values ...interface{}) (int, error) {
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 {
@ -290,12 +298,25 @@ func (c *Ctx) JSON(data interface{}) error {
return nil
}
func (c *Ctx) RawWriter() http.ResponseWriter {
return c.writer
func (c *Ctx) SSEvent(event string, data interface{}) error {
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) Write(data []byte) (int, error) {
return c.writermem.Write(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) RawWriter() http.ResponseWriter {
return c.writer
}
func (c *Ctx) HTML(html string) error {
@ -303,3 +324,7 @@ func (c *Ctx) HTML(html string) error {
_, err := c.writer.Write([]byte(html))
return err
}
func (c *Ctx) Write(data []byte) (int, error) {
return c.writermem.Write(data)
}

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

@ -1 +0,0 @@
package nf

View File

@ -3,6 +3,7 @@ package nf
import (
"bufio"
"io"
"log"
"net"
"net/http"
)
@ -60,7 +61,7 @@ func (w *responseWriter) reset(writer http.ResponseWriter) {
func (w *responseWriter) WriteHeader(code int) {
if code > 0 && w.status != code {
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
}
w.status = code

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,21 +0,0 @@
package main
import (
"github.com/loveuer/nf"
"log"
)
func main() {
app := nf.New(nf.Config{})
app.Get("/ok", func(c *nf.Ctx) error {
return c.SendStatus(200)
})
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,33 +0,0 @@
package main
import (
"github.com/loveuer/nf"
"github.com/loveuer/nf/nft/resp"
"log"
)
func main() {
app := nf.New(nf.Config{BodyLimit: 10 * 1024 * 1024})
app.Post("/upload", func(c *nf.Ctx) error {
fs, err := c.MultipartForm()
if err != nil {
return resp.Resp400(c, err.Error())
}
fm := make(map[string][]string)
for key := range fs.File {
if _, exist := fm[key]; !exist {
fm[key] = make([]string, 0)
}
for f := range fs.File[key] {
fm[key] = append(fm[key], fs.File[key][f].Filename)
}
}
return resp.Resp200(c, nf.Map{"value": fs.Value, "files": fm})
})
log.Fatal(app.Run(":13322"))
}

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
}