├── .gitignore ├── .travis.yml ├── LICENSE ├── Makefile ├── README.md ├── demo ├── .gitignore ├── ezhttpd │ ├── handle.go │ ├── index.html │ ├── main.go │ ├── router.go │ └── view.go └── helloworld │ ├── config.yaml │ ├── main.go │ ├── statics │ ├── img.jpg │ └── project.css │ └── templates │ ├── base.html │ ├── content.html │ ├── foot.html │ ├── head.html │ ├── index.html │ └── nav.html ├── error.go ├── go.mod ├── go.sum ├── helper.go ├── misc.go ├── pprof.go ├── router ├── brace.go ├── brace_test.go ├── colon.go ├── colon_test.go ├── regex.go ├── regex_test.go ├── router.go ├── router_test.go ├── wildcard.go └── wildcard_test.go ├── router_test.go ├── server.go ├── session.go ├── session ├── cookie.go ├── cookie_test.go ├── factory.go ├── session.go ├── storage.go └── storage_test.go └── view ├── json.go ├── json_test.go ├── preload.go ├── preload_test.go ├── simple.go ├── simple_test.go ├── static.go ├── static_test.go ├── template.go ├── template_test.go ├── view.go └── view_test.go /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled Object files, Static and Dynamic libs (Shared Objects) 2 | *.o 3 | *.a 4 | *.so 5 | 6 | # Folders 7 | _obj 8 | _test 9 | 10 | # Architecture specific extensions/prefixes 11 | *.[568vq] 12 | [568vq].out 13 | 14 | *.cgo1.go 15 | *.cgo2.c 16 | _cgo_defun.c 17 | _cgo_gotypes.go 18 | _cgo_export.* 19 | 20 | _testmain.go 21 | 22 | *.exe 23 | /vendor 24 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | go: 3 | - 1.13.x 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Xing 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | default: test 2 | 3 | fmt: 4 | go fmt github.com/mikespook/possum 5 | 6 | coverage: fmt 7 | go test ./ -coverprofile=coverage.out 8 | go tool cover -func=coverage.out 9 | go tool cover -html=coverage.out 10 | rm coverage.out 11 | 12 | test: fmt 13 | go vet github.com/mikespook/possum 14 | go test github.com/mikespook/possum 15 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Possum 2 | ====== 3 | 4 | [![Build Status][travis-img]][travis] 5 | [![GoDoc][godoc-img]][godoc] 6 | [![Coverage Status](https://coveralls.io/repos/mikespook/possum/badge.svg?branch=master&service=github)](https://coveralls.io/github/mikespook/possum?branch=master) 7 | 8 | Possum is a micro web library for Go. 9 | 10 | It has following modules: 11 | 12 | * Routers 13 | * Views 14 | * Session 15 | * Helpers 16 | 17 | Install 18 | ======= 19 | 20 | Install the package: 21 | 22 | ```bash 23 | go get github.com/mikespook/possum 24 | ``` 25 | 26 | Usage 27 | ===== 28 | 29 | Importing the package and sub-packages: 30 | 31 | ```go 32 | import ( 33 | "github.com/mikespook/possum" 34 | "github.com/mikespook/possum/router" 35 | "github.com/mikespook/possum/view" 36 | ) 37 | ``` 38 | 39 | Possum uses `Context` for passing data, handling request and rendering response. 40 | 41 | This is how to create a new server mux for Possum: 42 | 43 | ```go 44 | mux := possum.NewServerMux() 45 | ``` 46 | 47 | And assign a customized error handler: 48 | 49 | ```go 50 | mux.ErrorHandle = func(err error) { 51 | fmt.Println(err) 52 | } 53 | ``` 54 | 55 | `PreRequest` and `PostResponse` are useful for pre-checking or customizing logs: 56 | 57 | ```go 58 | mux.PreRequest = func(ctx *possum.Context) error { 59 | host, port, err := net.SplitHostPort(ctx.Request.RemoteAddr) 60 | if err != nil { 61 | return err 62 | } 63 | if host != "127.0.0.1" { 64 | return possum.NewError(http.StatusForbidden, "Localhost only") 65 | } 66 | return nil 67 | } 68 | 69 | mux.PostResponse = func(ctx *possum.Context) error { 70 | fmt.Printf("[%d] %s:%s \"%s\"", ctx.Response.Status, 71 | ctx.Request.RemoteAddr, ctx.Request.Method, 72 | ctx.Request.URL.String()) 73 | } 74 | ``` 75 | 76 | A specific path can bind to a different combination of routers, handlers and views: 77 | 78 | ```go 79 | f := session.NewFactory(session.CookieStorage('session-id', nil)) 80 | 81 | func helloword(ctx *Context) error { 82 | ctx.StartSession(f) 83 | return nil 84 | } 85 | 86 | mux.HandlerFunc(router.Simple("/json"), helloword, view.Json(view.CharSetUTF8)) 87 | 88 | if err := view.InitHtmlTemplates("*.html"); err != nil { 89 | return 90 | } 91 | mux.HandleFunc(router.Wildcard("/html/*/*"), 92 | helloworld, view.Html("base.html", "utf-8")) 93 | 94 | if err := view.InitWatcher("*.html", view.InitTextTemplates, nil); 95 | err != nil { 96 | return 97 | } 98 | mux.HandleFunc(router.RegEx("/html/(.*)/[a-z]"), 99 | helloworld, view.Text("base.html", "utf-8")) 100 | 101 | mux.HandleFunc(router.Colon("/:img/:id"), 102 | nil, view.File("img.jpg", "image/jpeg")) 103 | ``` 104 | 105 | Also, a PProf methods can be initialized by `mux.InitPProf`: 106 | 107 | ```go 108 | mux.InitPProf("/_pprof") 109 | ``` 110 | 111 | It will serve profiles and debug informations through `http://ip:port/_pprof`. 112 | 113 | E.g.: 114 | 115 | ![][pprof] 116 | 117 | And finally, it is a standard way for listening and serving: 118 | 119 | ```go 120 | http.ListenAndServe(":8080", mux) 121 | ``` 122 | 123 | For more details, please see the [demo][demo]. 124 | 125 | Contributors 126 | ============ 127 | 128 | (_Alphabetic order_) 129 | 130 | * [Xing Xing][blog] [@Twitter][twitter] 131 | 132 | Open Source - MIT Software License 133 | ================================== 134 | 135 | See LICENSE. 136 | 137 | [travis-img]: https://travis-ci.org/mikespook/possum.png?branch=master 138 | [travis]: https://travis-ci.org/mikespook/possum 139 | [blog]: http://mikespook.com 140 | [twitter]: http://twitter.com/mikespook 141 | [godoc-img]: https://godoc.org/github.com/mikespook/gorbac?status.png 142 | [godoc]: https://godoc.org/github.com/mikespook/possum 143 | [coveralls-img]: https://coveralls.io/repos/mikespook/possum/badge.svg?branch=master&service=github 144 | [coveralls]: https://coveralls.io/github/mikespook/possum?branch=master 145 | [demo]: https://github.com/mikespook/possum/tree/master/demo 146 | [pprof]: https://pbs.twimg.com/media/CE4k3SIUMAAZiLy.png 147 | -------------------------------------------------------------------------------- /demo/.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled Object files, Static and Dynamic libs (Shared Objects) 2 | *.o 3 | *.a 4 | *.so 5 | 6 | # Folders 7 | _obj 8 | _test 9 | 10 | # Architecture specific extensions/prefixes 11 | *.[568vq] 12 | [568vq].out 13 | 14 | *.cgo1.go 15 | *.cgo2.c 16 | _cgo_defun.c 17 | _cgo_gotypes.go 18 | _cgo_export.* 19 | 20 | _testmain.go 21 | 22 | *.exe 23 | 24 | demo 25 | -------------------------------------------------------------------------------- /demo/ezhttpd/handle.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "html/template" 7 | "io/ioutil" 8 | "mime" 9 | "net/http" 10 | "os" 11 | "path" 12 | "strings" 13 | 14 | "github.com/mikespook/possum" 15 | "github.com/mikespook/possum/view" 16 | ) 17 | 18 | const tplDir = ` 19 | 20 | 21 | 22 | Example 23 | 24 | 25 | 26 |

27 | ROOT:// 28 | {{$current := .current}} 29 | {{range $key, $name := .path}} 30 | {{$name}} 31 | {{end}}

32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | {{range .list}} 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | {{end}} 52 | 53 |
NameSizeModeModify Time
{{if .IsDir}}Dir{{end}}{{.Name}}{{.Size}}{{.Mode}}{{.ModTime}}
54 | 55 | ` 56 | 57 | func newStaticHandle(dir, autoindex string) possum.HandlerFunc { 58 | return func(w http.ResponseWriter, req *http.Request) (data interface{}, statusCode int) { 59 | f := path.Join(dir, req.URL.Path) 60 | fi, err := os.Stat(f) 61 | if err != nil { 62 | switch { 63 | case os.IsNotExist(err): 64 | statusCode = http.StatusNotFound 65 | data = fmt.Sprintf("File Not Found: %s", f) 66 | case os.IsPermission(err): 67 | statusCode = http.StatusForbidden 68 | data = fmt.Sprintf("Access Forbidden: %s", f) 69 | default: 70 | statusCode = http.StatusInternalServerError 71 | } 72 | return 73 | } 74 | if fi.IsDir() { 75 | if autoindex != "" { 76 | ai := path.Join(f, autoindex) 77 | if _, err := os.Stat(ai); !os.IsNotExist(err) { 78 | f = ai 79 | goto F 80 | } 81 | } 82 | fis, err := ioutil.ReadDir(f) 83 | if err != nil { 84 | return err, http.StatusInternalServerError 85 | } 86 | t, err := template.New("static").Parse(tplDir) 87 | var buf bytes.Buffer 88 | current := path.Clean(req.URL.Path) 89 | if current == "/" { 90 | current = "" 91 | } 92 | err = t.Execute(&buf, map[string]interface{}{ 93 | "current": current, 94 | "path": splitPath(current), 95 | "list": fis, 96 | }) 97 | if err != nil { 98 | return err, http.StatusInternalServerError 99 | } 100 | data = viewData{ 101 | contentType: view.ContentTypeHTML, 102 | body: buf.Bytes(), 103 | } 104 | return data, http.StatusOK 105 | } 106 | F: 107 | body, err := ioutil.ReadFile(f) 108 | if err != nil { 109 | return err, http.StatusInternalServerError 110 | } 111 | data = viewData{ 112 | contentType: mime.TypeByExtension(path.Ext(f)), 113 | body: body, 114 | } 115 | return data, http.StatusOK 116 | } 117 | } 118 | 119 | func splitPath(dir string) (r map[string]string) { 120 | r = make(map[string]string) 121 | path := strings.Split(dir, "/") 122 | key := "" 123 | for _, name := range path[1:] { 124 | key = fmt.Sprintf("%s/%s", key, name) 125 | r[key] = name 126 | } 127 | return 128 | } 129 | -------------------------------------------------------------------------------- /demo/ezhttpd/index.html: -------------------------------------------------------------------------------- 1 | OK 2 | -------------------------------------------------------------------------------- /demo/ezhttpd/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "net/http" 7 | "os" 8 | 9 | "github.com/mikespook/golib/log" 10 | "github.com/mikespook/golib/signal" 11 | "github.com/mikespook/possum" 12 | ) 13 | 14 | var ( 15 | addr, dir, autoindex string 16 | errWrongType = fmt.Errorf("Wrong Type Assertion") 17 | errAccessDeny = fmt.Errorf("Access Deny") 18 | ) 19 | 20 | func init() { 21 | flag.StringVar(&addr, "addr", "127.0.0.1:80", "Served address") 22 | flag.StringVar(&autoindex, "autoindex", "index.html", "Auto-index file") 23 | flag.Parse() 24 | } 25 | 26 | func main() { 27 | if dir = flag.Arg(0); dir == "" { 28 | dir = "." 29 | } 30 | 31 | mux := possum.New() 32 | mux.Add(staticRouter{}, newStaticHandle(dir, autoindex), staticView{}) 33 | mux.ErrorHandle = func(err error) { 34 | log.Error(err) 35 | } 36 | mux.PreResponse = func(w http.ResponseWriter, req *http.Request) { 37 | log.Debugf("%s:%s \"%s\"", req.RemoteAddr, req.Method, req.URL.String()) 38 | } 39 | log.Messagef("Addr: %s", addr) 40 | go func() { 41 | if err := http.ListenAndServe(addr, mux); err != nil { 42 | log.Error(err) 43 | if err := signal.Send(os.Getpid(), os.Interrupt); err != nil { 44 | panic(err) 45 | } 46 | } 47 | }() 48 | signal.Bind(os.Interrupt, func() uint { 49 | log.Message("Exit") 50 | return signal.BreakExit 51 | }) 52 | signal.Wait() 53 | } 54 | -------------------------------------------------------------------------------- /demo/ezhttpd/router.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "net/url" 4 | 5 | type staticRouter struct{} 6 | 7 | func (r staticRouter) Match(path string) (url.Values, bool) { 8 | return nil, true 9 | } 10 | -------------------------------------------------------------------------------- /demo/ezhttpd/view.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | 7 | "github.com/mikespook/possum/view" 8 | ) 9 | 10 | type viewData struct { 11 | contentType string 12 | body []byte 13 | } 14 | 15 | type staticView struct{} 16 | 17 | func (v staticView) Render(data interface{}) (output []byte, h http.Header, err error) { 18 | if data == nil { 19 | return nil, nil, errAccessDeny 20 | } 21 | switch param := data.(type) { 22 | case viewData: 23 | header := make(http.Header) 24 | header.Set("Content-Type", param.contentType) 25 | return param.body, header, nil 26 | case string: 27 | header := make(http.Header) 28 | header.Set("Content-Type", 29 | fmt.Sprintf("%s; charset=%s", view.ContentTypePlain, view.CharSetUTF8)) 30 | return []byte(param), header, nil 31 | } 32 | return nil, nil, errWrongType 33 | } 34 | -------------------------------------------------------------------------------- /demo/helloworld/config.yaml: -------------------------------------------------------------------------------- 1 | addr: 127.0.0.1:8080 2 | pprof: /_pprof 3 | log: 4 | file: 5 | level: all 6 | test: true 7 | -------------------------------------------------------------------------------- /demo/helloworld/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "io/ioutil" 6 | "net/http" 7 | "os" 8 | 9 | "github.com/mikespook/golib/log" 10 | "github.com/mikespook/golib/signal" 11 | "github.com/mikespook/possum" 12 | "github.com/mikespook/possum/router" 13 | "github.com/mikespook/possum/view" 14 | "gopkg.in/yaml.v2" 15 | ) 16 | 17 | type configLog struct { 18 | File, Level string 19 | } 20 | 21 | // Config struct 22 | type Config struct { 23 | Addr string 24 | PProf string 25 | Log configLog 26 | Test bool 27 | } 28 | 29 | // LoadConfig loads yaml to config instance 30 | func LoadConfig(filename string) (config *Config, err error) { 31 | data, err := ioutil.ReadFile(filename) 32 | if err != nil { 33 | return 34 | } 35 | err = yaml.Unmarshal(data, &config) 36 | return 37 | } 38 | 39 | var configFile string 40 | 41 | func init() { 42 | flag.StringVar(&configFile, "config", "config.yaml", "Path to the configuration file") 43 | flag.Parse() 44 | } 45 | 46 | func main() { 47 | if configFile == "" { 48 | flag.Usage() 49 | return 50 | } 51 | config, err := LoadConfig(configFile) 52 | if err != nil { 53 | log.Error(err) 54 | flag.Usage() 55 | return 56 | } 57 | if err := log.Init(config.Log.File, log.StrToLevel(config.Log.Level), log.DefaultCallDepth); err != nil { 58 | log.Error(err) 59 | } 60 | 61 | if config.Test { 62 | if err := view.InitWatcher("./templates/*.html", view.InitHtmlTemplates, nil); err != nil { 63 | log.Error(err) 64 | return 65 | } 66 | if err := view.InitWatcher("./templates/*.html", view.InitTextTemplates, nil); err != nil { 67 | log.Error(err) 68 | return 69 | } 70 | } else { 71 | if err := view.InitHtmlTemplates("./templates/*.html"); err != nil { 72 | log.Error(err) 73 | return 74 | } 75 | 76 | if err := view.InitTextTemplates("./templates/*.html"); err != nil { 77 | log.Error(err) 78 | return 79 | } 80 | } 81 | 82 | psm := possum.New() 83 | 84 | psm.Add(router.Simple("/"), nil, view.Html("index.html", "", "")) 85 | psm.Add(router.Simple("/json"), helloworld, view.Json("utf-8")) 86 | psm.Add(router.Wildcard("/json/*/*/*"), helloworld, view.Json("utf-8")) 87 | psm.Add(router.Simple("/html"), helloworld, view.Html("base.html", "", "")) 88 | psm.Add(router.Simple("/text"), helloworld, view.Text("base.html", "", "")) 89 | psm.Add(router.Simple("/project.css"), nil, 90 | view.StaticFile("statics/project.css", "text/css")) 91 | tmp, err := view.PreloadFile("statics/img.jpg", "image/jpeg") 92 | if err != nil { 93 | log.Error(err) 94 | return 95 | } 96 | psm.Add(router.Simple("/img.jpg"), nil, tmp) 97 | psm.ErrorHandle = func(err error) { 98 | log.Error(err) 99 | } 100 | psm.PreResponse = func(w http.ResponseWriter, req *http.Request) { 101 | log.Debugf("[%s] %s \"%s\"", req.Method, req.RemoteAddr, req.URL.String()) 102 | } 103 | if config.PProf != "" { 104 | log.Messagef("PProf: http://%s%s", config.Addr, config.PProf) 105 | possum.InitPProf(psm, config.PProf) 106 | } 107 | log.Messagef("Addr: %s", config.Addr) 108 | go func() { 109 | if err := http.ListenAndServe(config.Addr, psm); err != nil { 110 | log.Error(err) 111 | if err := signal.Send(os.Getpid(), os.Interrupt); err != nil { 112 | panic(err) 113 | } 114 | } 115 | }() 116 | signal.Bind(os.Interrupt, func() uint { 117 | log.Message("Exit") 118 | return signal.BreakExit 119 | }) 120 | signal.Wait() 121 | } 122 | 123 | func helloworld(w http.ResponseWriter, req *http.Request) (interface{}, int) { 124 | data := map[string]interface{}{ 125 | "content": map[string]string{ 126 | "msg": "hello", 127 | "target": "world", 128 | "params": req.URL.Query().Encode(), 129 | }, 130 | } 131 | w.Header().Set("Test", "Hello world") 132 | return data, http.StatusCreated 133 | } 134 | -------------------------------------------------------------------------------- /demo/helloworld/statics/img.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mikespook/possum/9816518f75d547a80f1fde114b7ca1d22427ca78/demo/helloworld/statics/img.jpg -------------------------------------------------------------------------------- /demo/helloworld/statics/project.css: -------------------------------------------------------------------------------- 1 | html,body { overflow-x: hidden; } 2 | body { padding-top: 70px; } 3 | footer { padding: 30px 0; } 4 | 5 | .goods { 6 | position: relative; 7 | overflow: hidden; 8 | display: block; 9 | width: 100%; 10 | max-height: 500px; 11 | padding: 15px 0; 12 | } 13 | 14 | .goods .name { 15 | position: absolute; 16 | top: 30%; 17 | left: 0; 18 | color: #fff; 19 | font-weight: 300; 20 | font-size: 24pt; 21 | width : 100%; 22 | text-align: center; 23 | } 24 | 25 | .goods .price { 26 | position: absolute; 27 | bottom: 0; 28 | right: 0; 29 | font-weight: 800; 30 | font-size: 18pt; 31 | color: #f00; 32 | } 33 | 34 | .goods .overly { 35 | height: 100%; 36 | position: absolute; 37 | left: 0; 38 | top: 0; 39 | visibility: hidden; 40 | width: 100%; 41 | display: block; 42 | } 43 | 44 | .goods:hover .overly { 45 | background: rgba(0, 0, 0, 0.3); 46 | visibility: visible; 47 | } 48 | -------------------------------------------------------------------------------- /demo/helloworld/templates/base.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | {{template "head"}} 4 | 5 | {{template "nav"}} 6 | {{template "content" .content}} 7 | {{template "foot"}} 8 | 9 | 10 | -------------------------------------------------------------------------------- /demo/helloworld/templates/content.html: -------------------------------------------------------------------------------- 1 | {{define "content"}} 2 |
3 | 29 |
30 |
31 |
32 |
33 | 40 |
41 |
42 | 49 |
50 |
51 | 58 |
59 |
60 | 67 |
68 |
69 | 76 |
77 |
78 | 85 |
86 | 87 |
88 | 95 |
96 |
97 | 104 |
105 |
106 |
107 | {{end}} 108 | -------------------------------------------------------------------------------- /demo/helloworld/templates/foot.html: -------------------------------------------------------------------------------- 1 | {{define "foot"}} 2 | 5 | 6 | 7 | {{end}} 8 | -------------------------------------------------------------------------------- /demo/helloworld/templates/head.html: -------------------------------------------------------------------------------- 1 | {{define "head"}} 2 | 3 | 4 | 5 | 6 | Possum - A micro web library for Go 7 | 8 | 9 | 10 | 14 | 15 | {{end}} 16 | -------------------------------------------------------------------------------- /demo/helloworld/templates/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | {{template "head"}} 4 | 5 | {{template "nav"}} 6 |
7 | 16 |
17 | {{template "foot"}} 18 | 19 | 20 | -------------------------------------------------------------------------------- /demo/helloworld/templates/nav.html: -------------------------------------------------------------------------------- 1 | {{define "nav"}} 2 | 22 | {{end}} 23 | -------------------------------------------------------------------------------- /error.go: -------------------------------------------------------------------------------- 1 | package possum 2 | 3 | import ( 4 | "errors" 5 | "log" 6 | "net/http" 7 | ) 8 | 9 | var ( 10 | errMethodNotAllowed = errors.New("possum: method not allowed") 11 | ) 12 | 13 | func handleError(w http.ResponseWriter, errPanic error) { 14 | e, ok := errPanic.(Error) 15 | var status int 16 | var message string 17 | if ok { 18 | status = e.Status 19 | } else { 20 | status = http.StatusInternalServerError 21 | } 22 | message = e.Error() 23 | // use ErrorHandler to re-rander error output 24 | w.WriteHeader(status) 25 | if _, err := w.Write([]byte(message)); err != nil { 26 | log.Panicln(err) 27 | } 28 | } 29 | 30 | // Error implements error interface 31 | type Error struct { 32 | Status int 33 | Message string 34 | } 35 | 36 | func (err Error) Error() string { 37 | return err.Message 38 | } 39 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/mikespook/possum 2 | 3 | go 1.13 4 | 5 | require ( 6 | github.com/fsnotify/fsnotify v1.4.7 // indirect 7 | github.com/mikespook/golib v0.0.0-20151119134446-38fe6917d34b 8 | golang.org/x/sys v0.0.0-20191210023423-ac6580df4449 // indirect 9 | gopkg.in/fsnotify.v1 v1.4.7 10 | gopkg.in/mgo.v2 v2.0.0-20190816093944-a6b53ec6cb22 // indirect 11 | gopkg.in/yaml.v2 v2.2.7 12 | ) 13 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I= 2 | github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= 3 | github.com/mikespook/golib v0.0.0-20151119134446-38fe6917d34b h1:v99/2GZfv/W/HuD17/QcHucnDtUsY9strTTwG82EuZY= 4 | github.com/mikespook/golib v0.0.0-20151119134446-38fe6917d34b/go.mod h1:3lBg8mdFpiF0C4ibbSFsxr+rgPWWkFlEVD/s11OG5k4= 5 | golang.org/x/sys v0.0.0-20191210023423-ac6580df4449 h1:gSbV7h1NRL2G1xTg/owz62CST1oJBmxy4QpMMregXVQ= 6 | golang.org/x/sys v0.0.0-20191210023423-ac6580df4449/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 7 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 8 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 9 | gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4= 10 | gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= 11 | gopkg.in/mgo.v2 v2.0.0-20190816093944-a6b53ec6cb22 h1:VpOs+IwYnYBaFnrNAeB8UUWtL3vEUnzSCL1nVjPhqrw= 12 | gopkg.in/mgo.v2 v2.0.0-20190816093944-a6b53ec6cb22/go.mod h1:yeKp02qBN3iKW1OzL3MGk2IdtZzaj7SFntXj72NppTA= 13 | gopkg.in/yaml.v2 v2.2.7 h1:VUgggvou5XRW9mHwD/yXxIYSMtY0zoKQf/v226p2nyo= 14 | gopkg.in/yaml.v2 v2.2.7/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 15 | -------------------------------------------------------------------------------- /helper.go: -------------------------------------------------------------------------------- 1 | package possum 2 | 3 | import ( 4 | "math/rand" 5 | "net/http" 6 | ) 7 | 8 | // Method takes one map as a paramater. 9 | // Keys of this map are HTTP method mapping to HandlerFunc(s). 10 | func Method(m map[string]HandlerFunc) HandlerFunc { 11 | return func(w http.ResponseWriter, req *http.Request) (interface{}, int) { 12 | h, ok := m[req.Method] 13 | if ok { 14 | return h(w, req) 15 | } 16 | return errMethodNotAllowed, http.StatusMethodNotAllowed 17 | } 18 | } 19 | 20 | // Chain combins a slide of HandlerFunc(s) in to one request. TODO 21 | func Chain(h ...HandlerFunc) HandlerFunc { 22 | f := func(w http.ResponseWriter, req *http.Request) (data interface{}, status int) { 23 | for _, v := range h { 24 | data, status = v(w, req) 25 | } 26 | return data, status 27 | } 28 | return f 29 | } 30 | 31 | // Rand picks one HandlerFunc(s) in the slide. 32 | func Rand(h ...HandlerFunc) HandlerFunc { 33 | return func(w http.ResponseWriter, req *http.Request) (interface{}, int) { 34 | return h[rand.Intn(len(h))](w, req) 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /misc.go: -------------------------------------------------------------------------------- 1 | package possum 2 | -------------------------------------------------------------------------------- /pprof.go: -------------------------------------------------------------------------------- 1 | package possum 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | "net/http/pprof" 7 | rpprof "runtime/pprof" 8 | "strconv" 9 | "strings" 10 | "text/template" 11 | 12 | "github.com/mikespook/possum/router" 13 | ) 14 | 15 | // InitPProf registers pprof handlers to the ServeMux. 16 | // The pprof handlers can be specified a customized prefix. 17 | func InitPProf(psm *Possum, prefix string) { 18 | if prefix == "" { 19 | prefix = "/debug/pprof" 20 | } 21 | psm.Add(router.Wildcard(fmt.Sprintf("%s/*", prefix)), pprofIndex(prefix), nil) 22 | psm.Add(router.Simple(fmt.Sprintf("%s/cmdline", prefix)), convHandlerFunc(pprof.Cmdline), nil) 23 | psm.Add(router.Simple(fmt.Sprintf("%s/profile", prefix)), convHandlerFunc(pprof.Profile), nil) 24 | psm.Add(router.Simple(fmt.Sprintf("%s/symbol", prefix)), convHandlerFunc(pprof.Symbol), nil) 25 | } 26 | 27 | func convHandlerFunc(f http.HandlerFunc) HandlerFunc { 28 | return func(w http.ResponseWriter, req *http.Request) (interface{}, int) { 29 | f(w, req) 30 | return nil, http.StatusOK 31 | } 32 | } 33 | 34 | const pprofTemp = ` 35 | 36 | %[1]s/ 37 | 40 | 41 | 42 |

Debug information

43 | 48 |

Profiles

49 | 50 | {{range .}} 51 |
{{.Count}}{{.Name}} 52 | {{end}} 53 |
30-second CPU 54 |
55 | 56 | 57 | ` 58 | 59 | func pprofIndex(prefix string) HandlerFunc { 60 | var indexTmpl = template.Must(template.New("index").Parse(fmt.Sprintf(pprofTemp, prefix))) 61 | if prefix[len(prefix)-1] != '/' { 62 | prefix = fmt.Sprintf("%s/", prefix) 63 | } 64 | f := func(w http.ResponseWriter, r *http.Request) (interface{}, int) { 65 | if strings.HasPrefix(r.URL.Path, prefix) { 66 | name := strings.TrimPrefix(r.URL.Path, prefix) 67 | if name != "" { 68 | w.Header().Set("Content-Type", "text/plain; charset=utf-8") 69 | debug, _ := strconv.Atoi(r.FormValue("debug")) 70 | p := rpprof.Lookup(string(name)) 71 | if p == nil { 72 | return fmt.Sprintf("Unknown profile: %s\n", name), http.StatusNotFound 73 | } 74 | p.WriteTo(w, debug) 75 | return nil, http.StatusOK 76 | } 77 | } 78 | profiles := rpprof.Profiles() 79 | if err := indexTmpl.Execute(w, profiles); err != nil { 80 | return err, http.StatusInternalServerError 81 | } 82 | return nil, http.StatusOK 83 | } 84 | return f 85 | } 86 | -------------------------------------------------------------------------------- /router/brace.go: -------------------------------------------------------------------------------- 1 | package router 2 | 3 | import ( 4 | "net/url" 5 | "strings" 6 | ) 7 | 8 | type brace struct { 9 | matches []string 10 | } 11 | 12 | // Brace matches path with REST-full resources URI in brace form. 13 | // e.g., "/foo/v1/bar/v2" will map to "/{foo}/{bar}" form, 14 | // while the value of "foo" and "bar" are "v1" and "v2", respectively. 15 | func Brace(path string) *brace { 16 | matches := strings.Split(path, "/") 17 | return &brace{ 18 | matches: matches, 19 | } 20 | } 21 | 22 | func (b *brace) Match(path string) (url.Values, bool) { 23 | matches := strings.Split(path, "/") 24 | i := 0 25 | params := url.Values{} 26 | var resKey, resValue string 27 | for _, v := range b.matches { 28 | if v != "" && v[0] == '{' && v[len(v)-1] == '}' { 29 | if matches[i] == v[1:len(v)-1] { 30 | resKey = matches[i] 31 | i++ 32 | resValue = matches[i] 33 | } else { 34 | return nil, false 35 | } 36 | params.Add(resKey, resValue) 37 | } else if matches[i] != v { 38 | return nil, false 39 | } 40 | i++ 41 | } 42 | return params, true 43 | } 44 | -------------------------------------------------------------------------------- /router/brace_test.go: -------------------------------------------------------------------------------- 1 | package router 2 | 3 | import "testing" 4 | 5 | func TestBrace(t *testing.T) { 6 | for k, v := range tCases["Brace"] { 7 | r := Brace(k) 8 | testingRouter(t, r, v) 9 | } 10 | } 11 | 12 | func BenchmarkBrace(b *testing.B) { 13 | for k, v := range tCases["Brace"] { 14 | r := Brace(k) 15 | benchmarkingRouter(b, r, v) 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /router/colon.go: -------------------------------------------------------------------------------- 1 | package router 2 | 3 | import ( 4 | "net/url" 5 | "strings" 6 | ) 7 | 8 | type colon struct { 9 | matches []string 10 | } 11 | 12 | // Colon matches path with REST-full resources URI in perfix colon form. 13 | // e.g., "/foo/v1/bar/v2" will map to "/:foo/:bar" form, 14 | // while the value of "foo" and "bar" are "v1" and "v2", respectively. 15 | func Colon(path string) *colon { 16 | matches := strings.Split(path, "/") 17 | return &colon{ 18 | matches: matches, 19 | } 20 | } 21 | 22 | func (c *colon) Match(path string) (url.Values, bool) { 23 | matches := strings.Split(path, "/") 24 | i := 0 25 | params := url.Values{} 26 | var resKey, resValue string 27 | for _, v := range c.matches { 28 | if v != "" && v[0] == ':' { 29 | if matches[i] == v[1:] { 30 | resKey = matches[i] 31 | i++ 32 | resValue = matches[i] 33 | } else { 34 | return nil, false 35 | } 36 | params.Add(resKey, resValue) 37 | } else if matches[i] != v { 38 | return nil, false 39 | } 40 | i++ 41 | } 42 | return params, true 43 | } 44 | -------------------------------------------------------------------------------- /router/colon_test.go: -------------------------------------------------------------------------------- 1 | package router 2 | 3 | import "testing" 4 | 5 | func TestColon(t *testing.T) { 6 | for k, v := range tCases["Colon"] { 7 | r := Colon(k) 8 | testingRouter(t, r, v) 9 | } 10 | } 11 | 12 | func BenchmarkColon(b *testing.B) { 13 | for k, v := range tCases["Colon"] { 14 | r := Colon(k) 15 | benchmarkingRouter(b, r, v) 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /router/regex.go: -------------------------------------------------------------------------------- 1 | package router 2 | 3 | import ( 4 | "net/url" 5 | "regexp" 6 | ) 7 | 8 | type regex struct { 9 | r *regexp.Regexp 10 | } 11 | 12 | // RegEx matches path using regular patterns. 13 | // TODO Dumpping sub-patterns into url.Values. 14 | func RegEx(path string) *regex { 15 | r, err := regexp.Compile(path) 16 | if err != nil { 17 | panic(err) 18 | } 19 | return ®ex{ 20 | r: r, 21 | } 22 | } 23 | 24 | func (r *regex) Match(path string) (url.Values, bool) { 25 | return nil, r.r.MatchString(path) 26 | } 27 | -------------------------------------------------------------------------------- /router/regex_test.go: -------------------------------------------------------------------------------- 1 | package router 2 | 3 | import "testing" 4 | 5 | func TestRegEx(t *testing.T) { 6 | for k, v := range tCases["RegEx"] { 7 | r := RegEx(k) 8 | testingRouter(t, r, v) 9 | } 10 | } 11 | 12 | func BenchmarkRegEx(b *testing.B) { 13 | for k, v := range tCases["RegEx"] { 14 | r := RegEx(k) 15 | benchmarkingRouter(b, r, v) 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /router/router.go: -------------------------------------------------------------------------------- 1 | package router 2 | 3 | import "net/url" 4 | 5 | // Router is an interface to match specific path. 6 | type Router interface { 7 | Match(string) (url.Values, bool) 8 | } 9 | 10 | type Base struct { 11 | Path string 12 | } 13 | 14 | // Simple router strictly matches paths. 15 | func Simple(path string) *Base { 16 | return &Base{path} 17 | } 18 | 19 | func (r *Base) Match(path string) (url.Values, bool) { 20 | return nil, path == r.Path 21 | } 22 | -------------------------------------------------------------------------------- /router/router_test.go: -------------------------------------------------------------------------------- 1 | package router 2 | 3 | import "testing" 4 | 5 | type aTCase map[string]bool 6 | 7 | var tCases = map[string]map[string]aTCase{ 8 | "Colon": { 9 | "/test/:a/:b/test": aTCase{ 10 | "/test/a/1/b/2/test": true, 11 | "/test/b/1/a/2/test": false, 12 | "test/a/1/b/2/test": false, 13 | }}, 14 | "Brace": { 15 | "/test/{a}/{b}/test": aTCase{ 16 | "/test/a/1/b/2/test": true, 17 | "/test/b/1/a/2/test": false, 18 | "test/a/1/b/2/test": false, 19 | }}, 20 | "RegEx": { 21 | "/test/(.*)/test": aTCase{ 22 | "/test/a/1/b/2/test": true, 23 | "/foo/b/1/a/2/test": false, 24 | "test/a/1/b/2/test": false, 25 | }}, 26 | "Wildcard": { 27 | "/test/*/*/test": aTCase{ 28 | "/test/foo/bar/test": true, 29 | "/foo/b/1/a/2/test": false, 30 | "test/a/1/b/2/test": false, 31 | }, 32 | }, 33 | "Simple": { 34 | "/test/foo/bar/test": aTCase{ 35 | "/test/foo/bar/test": true, 36 | "/foo/b/1/a/2/test": false, 37 | "test/a/1/b/2/test": false, 38 | }, 39 | }, 40 | } 41 | 42 | func testingRouter(t *testing.T, r Router, a aTCase) { 43 | for k, v := range a { 44 | if params, ok := r.Match(k); ok != v { 45 | t.Errorf("%v expected %t, got %t", params, v, ok) 46 | } else { 47 | t.Log(params) 48 | } 49 | } 50 | 51 | } 52 | 53 | func benchmarkingRouter(b *testing.B, r Router, a aTCase) { 54 | for i := 0; i < b.N; i++ { 55 | for k, v := range a { 56 | if params, ok := r.Match(k); ok != v { 57 | b.Errorf("%v expected %t, got %t", params, v, ok) 58 | } 59 | } 60 | } 61 | } 62 | 63 | func TestSimple(t *testing.T) { 64 | for k, v := range tCases["Simple"] { 65 | r := Simple(k) 66 | testingRouter(t, r, v) 67 | } 68 | } 69 | 70 | func BenchmarkSimple(b *testing.B) { 71 | for k, v := range tCases["Simple"] { 72 | r := Simple(k) 73 | benchmarkingRouter(b, r, v) 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /router/wildcard.go: -------------------------------------------------------------------------------- 1 | package router 2 | 3 | import ( 4 | "net/url" 5 | "strings" 6 | ) 7 | 8 | type wildcard struct { 9 | matches []string 10 | } 11 | 12 | // Wildcard matches paths with wildcard form. 13 | // E.g., "/foo/v1/bar/v2" will match the form "/foo/*/bar/*", 14 | // and "/foo/v1/v2/bar" will match the form "/foo/*/*/bar", but 15 | // will not match "/foo/*/bar". 16 | func Wildcard(path string) *wildcard { 17 | matches := strings.Split(path, "/") 18 | return &wildcard{ 19 | matches: matches, 20 | } 21 | } 22 | 23 | func (r *wildcard) Match(path string) (url.Values, bool) { 24 | matches := strings.Split(path, "/") 25 | if len(matches) != len(r.matches) { 26 | return nil, false 27 | } 28 | p := url.Values{} 29 | for k, v := range r.matches { 30 | if v != "*" && matches[k] != v { 31 | return nil, false 32 | } 33 | if v == "*" && matches[k] != "" { 34 | p.Add(matches[k], matches[k]) 35 | } 36 | } 37 | return p, true 38 | } 39 | -------------------------------------------------------------------------------- /router/wildcard_test.go: -------------------------------------------------------------------------------- 1 | package router 2 | 3 | import "testing" 4 | 5 | func TestWildcard(t *testing.T) { 6 | for k, v := range tCases["Wildcard"] { 7 | r := Wildcard(k) 8 | testingRouter(t, r, v) 9 | } 10 | } 11 | 12 | func BenchmarkWildcard(b *testing.B) { 13 | for k, v := range tCases["Wildcard"] { 14 | r := Wildcard(k) 15 | benchmarkingRouter(b, r, v) 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /router_test.go: -------------------------------------------------------------------------------- 1 | package possum 2 | 3 | import ( 4 | "net/http" 5 | "net/http/httptest" 6 | "runtime" 7 | "testing" 8 | 9 | "github.com/mikespook/possum/router" 10 | "github.com/mikespook/possum/view" 11 | ) 12 | 13 | // http://developer.github.com/v3/ 14 | var githubAPI = []route{ 15 | // OAuth Authorizations 16 | {"GET", "/authorizations"}, 17 | {"GET", "/authorizations/:id"}, 18 | {"POST", "/authorizations"}, 19 | //{"PUT", "/authorizations/clients/:client_id"}, 20 | //{"PATCH", "/authorizations/:id"}, 21 | {"DELETE", "/authorizations/:id"}, 22 | {"GET", "/applications/:client_id/tokens/:access_token"}, 23 | {"DELETE", "/applications/:client_id/tokens"}, 24 | {"DELETE", "/applications/:client_id/tokens/:access_token"}, 25 | 26 | // Activity 27 | {"GET", "/events"}, 28 | {"GET", "/repos/:owner/:repo/events"}, 29 | {"GET", "/networks/:owner/:repo/events"}, 30 | {"GET", "/orgs/:org/events"}, 31 | {"GET", "/users/:user/received_events"}, 32 | {"GET", "/users/:user/received_events/public"}, 33 | {"GET", "/users/:user/events"}, 34 | {"GET", "/users/:user/events/public"}, 35 | {"GET", "/users/:user/events/orgs/:org"}, 36 | {"GET", "/feeds"}, 37 | {"GET", "/notifications"}, 38 | {"GET", "/repos/:owner/:repo/notifications"}, 39 | {"PUT", "/notifications"}, 40 | {"PUT", "/repos/:owner/:repo/notifications"}, 41 | {"GET", "/notifications/threads/:id"}, 42 | //{"PATCH", "/notifications/threads/:id"}, 43 | {"GET", "/notifications/threads/:id/subscription"}, 44 | {"PUT", "/notifications/threads/:id/subscription"}, 45 | {"DELETE", "/notifications/threads/:id/subscription"}, 46 | {"GET", "/repos/:owner/:repo/stargazers"}, 47 | {"GET", "/users/:user/starred"}, 48 | {"GET", "/user/starred"}, 49 | {"GET", "/user/starred/:owner/:repo"}, 50 | {"PUT", "/user/starred/:owner/:repo"}, 51 | {"DELETE", "/user/starred/:owner/:repo"}, 52 | {"GET", "/repos/:owner/:repo/subscribers"}, 53 | {"GET", "/users/:user/subscriptions"}, 54 | {"GET", "/user/subscriptions"}, 55 | {"GET", "/repos/:owner/:repo/subscription"}, 56 | {"PUT", "/repos/:owner/:repo/subscription"}, 57 | {"DELETE", "/repos/:owner/:repo/subscription"}, 58 | {"GET", "/user/subscriptions/:owner/:repo"}, 59 | {"PUT", "/user/subscriptions/:owner/:repo"}, 60 | {"DELETE", "/user/subscriptions/:owner/:repo"}, 61 | 62 | // Gists 63 | {"GET", "/users/:user/gists"}, 64 | {"GET", "/gists"}, 65 | //{"GET", "/gists/public"}, 66 | //{"GET", "/gists/starred"}, 67 | {"GET", "/gists/:id"}, 68 | {"POST", "/gists"}, 69 | //{"PATCH", "/gists/:id"}, 70 | {"PUT", "/gists/:id/star"}, 71 | {"DELETE", "/gists/:id/star"}, 72 | {"GET", "/gists/:id/star"}, 73 | {"POST", "/gists/:id/forks"}, 74 | {"DELETE", "/gists/:id"}, 75 | 76 | // Git Data 77 | {"GET", "/repos/:owner/:repo/git/blobs/:sha"}, 78 | {"POST", "/repos/:owner/:repo/git/blobs"}, 79 | {"GET", "/repos/:owner/:repo/git/commits/:sha"}, 80 | {"POST", "/repos/:owner/:repo/git/commits"}, 81 | //{"GET", "/repos/:owner/:repo/git/refs/*ref"}, 82 | {"GET", "/repos/:owner/:repo/git/refs"}, 83 | {"POST", "/repos/:owner/:repo/git/refs"}, 84 | //{"PATCH", "/repos/:owner/:repo/git/refs/*ref"}, 85 | //{"DELETE", "/repos/:owner/:repo/git/refs/*ref"}, 86 | {"GET", "/repos/:owner/:repo/git/tags/:sha"}, 87 | {"POST", "/repos/:owner/:repo/git/tags"}, 88 | {"GET", "/repos/:owner/:repo/git/trees/:sha"}, 89 | {"POST", "/repos/:owner/:repo/git/trees"}, 90 | 91 | // Issues 92 | {"GET", "/issues"}, 93 | {"GET", "/user/issues"}, 94 | {"GET", "/orgs/:org/issues"}, 95 | {"GET", "/repos/:owner/:repo/issues"}, 96 | {"GET", "/repos/:owner/:repo/issues/:number"}, 97 | {"POST", "/repos/:owner/:repo/issues"}, 98 | //{"PATCH", "/repos/:owner/:repo/issues/:number"}, 99 | {"GET", "/repos/:owner/:repo/assignees"}, 100 | {"GET", "/repos/:owner/:repo/assignees/:assignee"}, 101 | {"GET", "/repos/:owner/:repo/issues/:number/comments"}, 102 | //{"GET", "/repos/:owner/:repo/issues/comments"}, 103 | //{"GET", "/repos/:owner/:repo/issues/comments/:id"}, 104 | {"POST", "/repos/:owner/:repo/issues/:number/comments"}, 105 | //{"PATCH", "/repos/:owner/:repo/issues/comments/:id"}, 106 | //{"DELETE", "/repos/:owner/:repo/issues/comments/:id"}, 107 | {"GET", "/repos/:owner/:repo/issues/:number/events"}, 108 | //{"GET", "/repos/:owner/:repo/issues/events"}, 109 | //{"GET", "/repos/:owner/:repo/issues/events/:id"}, 110 | {"GET", "/repos/:owner/:repo/labels"}, 111 | {"GET", "/repos/:owner/:repo/labels/:name"}, 112 | {"POST", "/repos/:owner/:repo/labels"}, 113 | //{"PATCH", "/repos/:owner/:repo/labels/:name"}, 114 | {"DELETE", "/repos/:owner/:repo/labels/:name"}, 115 | {"GET", "/repos/:owner/:repo/issues/:number/labels"}, 116 | {"POST", "/repos/:owner/:repo/issues/:number/labels"}, 117 | {"DELETE", "/repos/:owner/:repo/issues/:number/labels/:name"}, 118 | {"PUT", "/repos/:owner/:repo/issues/:number/labels"}, 119 | {"DELETE", "/repos/:owner/:repo/issues/:number/labels"}, 120 | {"GET", "/repos/:owner/:repo/milestones/:number/labels"}, 121 | {"GET", "/repos/:owner/:repo/milestones"}, 122 | {"GET", "/repos/:owner/:repo/milestones/:number"}, 123 | {"POST", "/repos/:owner/:repo/milestones"}, 124 | //{"PATCH", "/repos/:owner/:repo/milestones/:number"}, 125 | {"DELETE", "/repos/:owner/:repo/milestones/:number"}, 126 | 127 | // Miscellaneous 128 | {"GET", "/emojis"}, 129 | {"GET", "/gitignore/templates"}, 130 | {"GET", "/gitignore/templates/:name"}, 131 | {"POST", "/markdown"}, 132 | {"POST", "/markdown/raw"}, 133 | {"GET", "/meta"}, 134 | {"GET", "/rate_limit"}, 135 | 136 | // Organizations 137 | {"GET", "/users/:user/orgs"}, 138 | {"GET", "/user/orgs"}, 139 | {"GET", "/orgs/:org"}, 140 | //{"PATCH", "/orgs/:org"}, 141 | {"GET", "/orgs/:org/members"}, 142 | {"GET", "/orgs/:org/members/:user"}, 143 | {"DELETE", "/orgs/:org/members/:user"}, 144 | {"GET", "/orgs/:org/public_members"}, 145 | {"GET", "/orgs/:org/public_members/:user"}, 146 | {"PUT", "/orgs/:org/public_members/:user"}, 147 | {"DELETE", "/orgs/:org/public_members/:user"}, 148 | {"GET", "/orgs/:org/teams"}, 149 | {"GET", "/teams/:id"}, 150 | {"POST", "/orgs/:org/teams"}, 151 | //{"PATCH", "/teams/:id"}, 152 | {"DELETE", "/teams/:id"}, 153 | {"GET", "/teams/:id/members"}, 154 | {"GET", "/teams/:id/members/:user"}, 155 | {"PUT", "/teams/:id/members/:user"}, 156 | {"DELETE", "/teams/:id/members/:user"}, 157 | {"GET", "/teams/:id/repos"}, 158 | {"GET", "/teams/:id/repos/:owner/:repo"}, 159 | {"PUT", "/teams/:id/repos/:owner/:repo"}, 160 | {"DELETE", "/teams/:id/repos/:owner/:repo"}, 161 | {"GET", "/user/teams"}, 162 | 163 | // Pull Requests 164 | {"GET", "/repos/:owner/:repo/pulls"}, 165 | {"GET", "/repos/:owner/:repo/pulls/:number"}, 166 | {"POST", "/repos/:owner/:repo/pulls"}, 167 | //{"PATCH", "/repos/:owner/:repo/pulls/:number"}, 168 | {"GET", "/repos/:owner/:repo/pulls/:number/commits"}, 169 | {"GET", "/repos/:owner/:repo/pulls/:number/files"}, 170 | {"GET", "/repos/:owner/:repo/pulls/:number/merge"}, 171 | {"PUT", "/repos/:owner/:repo/pulls/:number/merge"}, 172 | {"GET", "/repos/:owner/:repo/pulls/:number/comments"}, 173 | //{"GET", "/repos/:owner/:repo/pulls/comments"}, 174 | //{"GET", "/repos/:owner/:repo/pulls/comments/:number"}, 175 | {"PUT", "/repos/:owner/:repo/pulls/:number/comments"}, 176 | //{"PATCH", "/repos/:owner/:repo/pulls/comments/:number"}, 177 | //{"DELETE", "/repos/:owner/:repo/pulls/comments/:number"}, 178 | 179 | // Repositories 180 | {"GET", "/user/repos"}, 181 | {"GET", "/users/:user/repos"}, 182 | {"GET", "/orgs/:org/repos"}, 183 | {"GET", "/repositories"}, 184 | {"POST", "/user/repos"}, 185 | {"POST", "/orgs/:org/repos"}, 186 | {"GET", "/repos/:owner/:repo"}, 187 | //{"PATCH", "/repos/:owner/:repo"}, 188 | {"GET", "/repos/:owner/:repo/contributors"}, 189 | {"GET", "/repos/:owner/:repo/languages"}, 190 | {"GET", "/repos/:owner/:repo/teams"}, 191 | {"GET", "/repos/:owner/:repo/tags"}, 192 | {"GET", "/repos/:owner/:repo/branches"}, 193 | {"GET", "/repos/:owner/:repo/branches/:branch"}, 194 | {"DELETE", "/repos/:owner/:repo"}, 195 | {"GET", "/repos/:owner/:repo/collaborators"}, 196 | {"GET", "/repos/:owner/:repo/collaborators/:user"}, 197 | {"PUT", "/repos/:owner/:repo/collaborators/:user"}, 198 | {"DELETE", "/repos/:owner/:repo/collaborators/:user"}, 199 | {"GET", "/repos/:owner/:repo/comments"}, 200 | {"GET", "/repos/:owner/:repo/commits/:sha/comments"}, 201 | {"POST", "/repos/:owner/:repo/commits/:sha/comments"}, 202 | {"GET", "/repos/:owner/:repo/comments/:id"}, 203 | //{"PATCH", "/repos/:owner/:repo/comments/:id"}, 204 | {"DELETE", "/repos/:owner/:repo/comments/:id"}, 205 | {"GET", "/repos/:owner/:repo/commits"}, 206 | {"GET", "/repos/:owner/:repo/commits/:sha"}, 207 | {"GET", "/repos/:owner/:repo/readme"}, 208 | //{"GET", "/repos/:owner/:repo/contents/*path"}, 209 | //{"PUT", "/repos/:owner/:repo/contents/*path"}, 210 | //{"DELETE", "/repos/:owner/:repo/contents/*path"}, 211 | //{"GET", "/repos/:owner/:repo/:archive_format/:ref"}, 212 | {"GET", "/repos/:owner/:repo/keys"}, 213 | {"GET", "/repos/:owner/:repo/keys/:id"}, 214 | {"POST", "/repos/:owner/:repo/keys"}, 215 | //{"PATCH", "/repos/:owner/:repo/keys/:id"}, 216 | {"DELETE", "/repos/:owner/:repo/keys/:id"}, 217 | {"GET", "/repos/:owner/:repo/downloads"}, 218 | {"GET", "/repos/:owner/:repo/downloads/:id"}, 219 | {"DELETE", "/repos/:owner/:repo/downloads/:id"}, 220 | {"GET", "/repos/:owner/:repo/forks"}, 221 | {"POST", "/repos/:owner/:repo/forks"}, 222 | {"GET", "/repos/:owner/:repo/hooks"}, 223 | {"GET", "/repos/:owner/:repo/hooks/:id"}, 224 | {"POST", "/repos/:owner/:repo/hooks"}, 225 | //{"PATCH", "/repos/:owner/:repo/hooks/:id"}, 226 | {"POST", "/repos/:owner/:repo/hooks/:id/tests"}, 227 | {"DELETE", "/repos/:owner/:repo/hooks/:id"}, 228 | {"POST", "/repos/:owner/:repo/merges"}, 229 | {"GET", "/repos/:owner/:repo/releases"}, 230 | {"GET", "/repos/:owner/:repo/releases/:id"}, 231 | {"POST", "/repos/:owner/:repo/releases"}, 232 | //{"PATCH", "/repos/:owner/:repo/releases/:id"}, 233 | {"DELETE", "/repos/:owner/:repo/releases/:id"}, 234 | {"GET", "/repos/:owner/:repo/releases/:id/assets"}, 235 | {"GET", "/repos/:owner/:repo/stats/contributors"}, 236 | {"GET", "/repos/:owner/:repo/stats/commit_activity"}, 237 | {"GET", "/repos/:owner/:repo/stats/code_frequency"}, 238 | {"GET", "/repos/:owner/:repo/stats/participation"}, 239 | {"GET", "/repos/:owner/:repo/stats/punch_card"}, 240 | {"GET", "/repos/:owner/:repo/statuses/:ref"}, 241 | {"POST", "/repos/:owner/:repo/statuses/:ref"}, 242 | 243 | // Search 244 | {"GET", "/search/repositories"}, 245 | {"GET", "/search/code"}, 246 | {"GET", "/search/issues"}, 247 | {"GET", "/search/users"}, 248 | {"GET", "/legacy/issues/search/:owner/:repository/:state/:keyword"}, 249 | {"GET", "/legacy/repos/search/:keyword"}, 250 | {"GET", "/legacy/user/search/:keyword"}, 251 | {"GET", "/legacy/user/email/:email"}, 252 | 253 | // Users 254 | {"GET", "/users/:user"}, 255 | {"GET", "/user"}, 256 | //{"PATCH", "/user"}, 257 | {"GET", "/users"}, 258 | {"GET", "/user/emails"}, 259 | {"POST", "/user/emails"}, 260 | {"DELETE", "/user/emails"}, 261 | {"GET", "/users/:user/followers"}, 262 | {"GET", "/user/followers"}, 263 | {"GET", "/users/:user/following"}, 264 | {"GET", "/user/following"}, 265 | {"GET", "/user/following/:user"}, 266 | {"GET", "/users/:user/following/:target_user"}, 267 | {"PUT", "/user/following/:user"}, 268 | {"DELETE", "/user/following/:user"}, 269 | {"GET", "/users/:user/keys"}, 270 | {"GET", "/user/keys"}, 271 | {"GET", "/user/keys/:id"}, 272 | {"POST", "/user/keys"}, 273 | //{"PATCH", "/user/keys/:id"}, 274 | {"DELETE", "/user/keys/:id"}, 275 | } 276 | 277 | // Google+ 278 | // https://developers.google.com/+/api/latest/ 279 | // (in reality this is just a subset of a much larger API) 280 | var gplusAPI = []route{ 281 | // People 282 | {"GET", "/people/:userId"}, 283 | {"GET", "/people"}, 284 | {"GET", "/activities/:activityId/people/:collection"}, 285 | {"GET", "/people/:userId/people/:collection"}, 286 | {"GET", "/people/:userId/openIdConnect"}, 287 | 288 | // Activities 289 | {"GET", "/people/:userId/activities/:collection"}, 290 | {"GET", "/activities/:activityId"}, 291 | {"GET", "/activities"}, 292 | 293 | // Comments 294 | {"GET", "/activities/:activityId/comments"}, 295 | {"GET", "/comments/:commentId"}, 296 | 297 | // Moments 298 | {"POST", "/people/:userId/moments/:collection"}, 299 | {"GET", "/people/:userId/moments/:collection"}, 300 | {"DELETE", "/moments/:id"}, 301 | } 302 | 303 | // Parse 304 | // https://parse.com/docs/rest#summary 305 | var parseAPI = []route{ 306 | // Objects 307 | {"POST", "/1/classes/:className"}, 308 | {"GET", "/1/classes/:className/:objectId"}, 309 | {"PUT", "/1/classes/:className/:objectId"}, 310 | {"GET", "/1/classes/:className"}, 311 | {"DELETE", "/1/classes/:className/:objectId"}, 312 | 313 | // Users 314 | {"POST", "/1/users"}, 315 | {"GET", "/1/login"}, 316 | {"GET", "/1/users/:objectId"}, 317 | {"PUT", "/1/users/:objectId"}, 318 | {"GET", "/1/users"}, 319 | {"DELETE", "/1/users/:objectId"}, 320 | {"POST", "/1/requestPasswordReset"}, 321 | 322 | // Roles 323 | {"POST", "/1/roles"}, 324 | {"GET", "/1/roles/:objectId"}, 325 | {"PUT", "/1/roles/:objectId"}, 326 | {"GET", "/1/roles"}, 327 | {"DELETE", "/1/roles/:objectId"}, 328 | 329 | // Files 330 | {"POST", "/1/files/:fileName"}, 331 | 332 | // Analytics 333 | {"POST", "/1/events/:eventName"}, 334 | 335 | // Push Notifications 336 | {"POST", "/1/push"}, 337 | 338 | // Installations 339 | {"POST", "/1/installations"}, 340 | {"GET", "/1/installations/:objectId"}, 341 | {"PUT", "/1/installations/:objectId"}, 342 | {"GET", "/1/installations"}, 343 | {"DELETE", "/1/installations/:objectId"}, 344 | 345 | // Cloud Functions 346 | {"POST", "/1/functions"}, 347 | } 348 | 349 | var staticRoutes = []route{ 350 | {"GET", "/"}, 351 | {"GET", "/cmd.html"}, 352 | {"GET", "/code.html"}, 353 | {"GET", "/contrib.html"}, 354 | {"GET", "/contribute.html"}, 355 | {"GET", "/debugging_with_gdb.html"}, 356 | {"GET", "/docs.html"}, 357 | {"GET", "/effective_go.html"}, 358 | {"GET", "/files.log"}, 359 | {"GET", "/gccgo_contribute.html"}, 360 | {"GET", "/gccgo_install.html"}, 361 | {"GET", "/go-logo-black.png"}, 362 | {"GET", "/go-logo-blue.png"}, 363 | {"GET", "/go-logo-white.png"}, 364 | {"GET", "/go1.1.html"}, 365 | {"GET", "/go1.2.html"}, 366 | {"GET", "/go1.html"}, 367 | {"GET", "/go1compat.html"}, 368 | {"GET", "/go_faq.html"}, 369 | {"GET", "/go_mem.html"}, 370 | {"GET", "/go_spec.html"}, 371 | {"GET", "/help.html"}, 372 | {"GET", "/ie.css"}, 373 | {"GET", "/install-source.html"}, 374 | {"GET", "/install.html"}, 375 | {"GET", "/logo-153x55.png"}, 376 | {"GET", "/Makefile"}, 377 | {"GET", "/root.html"}, 378 | {"GET", "/share.png"}, 379 | {"GET", "/sieve.gif"}, 380 | {"GET", "/tos.html"}, 381 | {"GET", "/articles/"}, 382 | {"GET", "/articles/go_command.html"}, 383 | {"GET", "/articles/index.html"}, 384 | {"GET", "/articles/wiki/"}, 385 | {"GET", "/articles/wiki/edit.html"}, 386 | {"GET", "/articles/wiki/final-noclosure.go"}, 387 | {"GET", "/articles/wiki/final-noerror.go"}, 388 | {"GET", "/articles/wiki/final-parsetemplate.go"}, 389 | {"GET", "/articles/wiki/final-template.go"}, 390 | {"GET", "/articles/wiki/final.go"}, 391 | {"GET", "/articles/wiki/get.go"}, 392 | {"GET", "/articles/wiki/http-sample.go"}, 393 | {"GET", "/articles/wiki/index.html"}, 394 | {"GET", "/articles/wiki/Makefile"}, 395 | {"GET", "/articles/wiki/notemplate.go"}, 396 | {"GET", "/articles/wiki/part1-noerror.go"}, 397 | {"GET", "/articles/wiki/part1.go"}, 398 | {"GET", "/articles/wiki/part2.go"}, 399 | {"GET", "/articles/wiki/part3-errorhandling.go"}, 400 | {"GET", "/articles/wiki/part3.go"}, 401 | {"GET", "/articles/wiki/test.bash"}, 402 | {"GET", "/articles/wiki/test_edit.good"}, 403 | {"GET", "/articles/wiki/test_Test.txt.good"}, 404 | {"GET", "/articles/wiki/test_view.good"}, 405 | {"GET", "/articles/wiki/view.html"}, 406 | {"GET", "/codewalk/"}, 407 | {"GET", "/codewalk/codewalk.css"}, 408 | {"GET", "/codewalk/codewalk.js"}, 409 | {"GET", "/codewalk/codewalk.xml"}, 410 | {"GET", "/codewalk/functions.xml"}, 411 | {"GET", "/codewalk/markov.go"}, 412 | {"GET", "/codewalk/markov.xml"}, 413 | {"GET", "/codewalk/pig.go"}, 414 | {"GET", "/codewalk/popout.png"}, 415 | {"GET", "/codewalk/run"}, 416 | {"GET", "/codewalk/sharemem.xml"}, 417 | {"GET", "/codewalk/urlpoll.go"}, 418 | {"GET", "/devel/"}, 419 | {"GET", "/devel/release.html"}, 420 | {"GET", "/devel/weekly.html"}, 421 | {"GET", "/gopher/"}, 422 | {"GET", "/gopher/appenginegopher.jpg"}, 423 | {"GET", "/gopher/appenginegophercolor.jpg"}, 424 | {"GET", "/gopher/appenginelogo.gif"}, 425 | {"GET", "/gopher/bumper.png"}, 426 | {"GET", "/gopher/bumper192x108.png"}, 427 | {"GET", "/gopher/bumper320x180.png"}, 428 | {"GET", "/gopher/bumper480x270.png"}, 429 | {"GET", "/gopher/bumper640x360.png"}, 430 | {"GET", "/gopher/doc.png"}, 431 | {"GET", "/gopher/frontpage.png"}, 432 | {"GET", "/gopher/gopherbw.png"}, 433 | {"GET", "/gopher/gophercolor.png"}, 434 | {"GET", "/gopher/gophercolor16x16.png"}, 435 | {"GET", "/gopher/help.png"}, 436 | {"GET", "/gopher/pkg.png"}, 437 | {"GET", "/gopher/project.png"}, 438 | {"GET", "/gopher/ref.png"}, 439 | {"GET", "/gopher/run.png"}, 440 | {"GET", "/gopher/talks.png"}, 441 | {"GET", "/gopher/pencil/"}, 442 | {"GET", "/gopher/pencil/gopherhat.jpg"}, 443 | {"GET", "/gopher/pencil/gopherhelmet.jpg"}, 444 | {"GET", "/gopher/pencil/gophermega.jpg"}, 445 | {"GET", "/gopher/pencil/gopherrunning.jpg"}, 446 | {"GET", "/gopher/pencil/gopherswim.jpg"}, 447 | {"GET", "/gopher/pencil/gopherswrench.jpg"}, 448 | {"GET", "/play/"}, 449 | {"GET", "/play/fib.go"}, 450 | {"GET", "/play/hello.go"}, 451 | {"GET", "/play/life.go"}, 452 | {"GET", "/play/peano.go"}, 453 | {"GET", "/play/pi.go"}, 454 | {"GET", "/play/sieve.go"}, 455 | {"GET", "/play/solitaire.go"}, 456 | {"GET", "/play/tree.go"}, 457 | {"GET", "/progs/"}, 458 | {"GET", "/progs/cgo1.go"}, 459 | {"GET", "/progs/cgo2.go"}, 460 | {"GET", "/progs/cgo3.go"}, 461 | {"GET", "/progs/cgo4.go"}, 462 | {"GET", "/progs/defer.go"}, 463 | {"GET", "/progs/defer.out"}, 464 | {"GET", "/progs/defer2.go"}, 465 | {"GET", "/progs/defer2.out"}, 466 | {"GET", "/progs/eff_bytesize.go"}, 467 | {"GET", "/progs/eff_bytesize.out"}, 468 | {"GET", "/progs/eff_qr.go"}, 469 | {"GET", "/progs/eff_sequence.go"}, 470 | {"GET", "/progs/eff_sequence.out"}, 471 | {"GET", "/progs/eff_unused1.go"}, 472 | {"GET", "/progs/eff_unused2.go"}, 473 | {"GET", "/progs/error.go"}, 474 | {"GET", "/progs/error2.go"}, 475 | {"GET", "/progs/error3.go"}, 476 | {"GET", "/progs/error4.go"}, 477 | {"GET", "/progs/go1.go"}, 478 | {"GET", "/progs/gobs1.go"}, 479 | {"GET", "/progs/gobs2.go"}, 480 | {"GET", "/progs/image_draw.go"}, 481 | {"GET", "/progs/image_package1.go"}, 482 | {"GET", "/progs/image_package1.out"}, 483 | {"GET", "/progs/image_package2.go"}, 484 | {"GET", "/progs/image_package2.out"}, 485 | {"GET", "/progs/image_package3.go"}, 486 | {"GET", "/progs/image_package3.out"}, 487 | {"GET", "/progs/image_package4.go"}, 488 | {"GET", "/progs/image_package4.out"}, 489 | {"GET", "/progs/image_package5.go"}, 490 | {"GET", "/progs/image_package5.out"}, 491 | {"GET", "/progs/image_package6.go"}, 492 | {"GET", "/progs/image_package6.out"}, 493 | {"GET", "/progs/interface.go"}, 494 | {"GET", "/progs/interface2.go"}, 495 | {"GET", "/progs/interface2.out"}, 496 | {"GET", "/progs/json1.go"}, 497 | {"GET", "/progs/json2.go"}, 498 | {"GET", "/progs/json2.out"}, 499 | {"GET", "/progs/json3.go"}, 500 | {"GET", "/progs/json4.go"}, 501 | {"GET", "/progs/json5.go"}, 502 | {"GET", "/progs/run"}, 503 | {"GET", "/progs/slices.go"}, 504 | {"GET", "/progs/timeout1.go"}, 505 | {"GET", "/progs/timeout2.go"}, 506 | {"GET", "/progs/update.bash"}, 507 | } 508 | 509 | type route struct { 510 | method string 511 | path string 512 | } 513 | 514 | type mockResponseWriter struct{} 515 | 516 | func (m *mockResponseWriter) Header() (h http.Header) { 517 | return http.Header{} 518 | } 519 | 520 | func (m *mockResponseWriter) Write(p []byte) (n int, err error) { 521 | return len(p), nil 522 | } 523 | 524 | func (m *mockResponseWriter) WriteString(s string) (n int, err error) { 525 | return len(s), nil 526 | } 527 | 528 | func (m *mockResponseWriter) WriteHeader(int) {} 529 | 530 | func calcMem(name string, load func()) { 531 | m := new(runtime.MemStats) 532 | 533 | // before 534 | runtime.GC() 535 | runtime.ReadMemStats(m) 536 | before := m.HeapAlloc 537 | 538 | load() 539 | 540 | // after 541 | runtime.GC() 542 | runtime.ReadMemStats(m) 543 | after := m.HeapAlloc 544 | println(" "+name+":", after-before, "Bytes") 545 | } 546 | 547 | func benchRequest(b *testing.B, psm *Possum, r *http.Request) { 548 | w := new(mockResponseWriter) 549 | u := r.URL 550 | rq := u.RawQuery 551 | r.RequestURI = u.RequestURI() 552 | 553 | b.ReportAllocs() 554 | b.ResetTimer() 555 | 556 | for i := 0; i < b.N; i++ { 557 | u.RawQuery = rq 558 | psm.ServeHTTP(w, r) 559 | } 560 | } 561 | 562 | func benchRoutes(b *testing.B, psm *Possum, routes []route) { 563 | w := new(mockResponseWriter) 564 | r, _ := http.NewRequest("GET", "/", nil) 565 | u := r.URL 566 | rq := u.RawQuery 567 | 568 | b.ReportAllocs() 569 | b.ResetTimer() 570 | 571 | for i := 0; i < b.N; i++ { 572 | for _, route := range routes { 573 | r.Method = route.method 574 | r.RequestURI = route.path 575 | u.Path = route.path 576 | u.RawQuery = rq 577 | psm.ServeHTTP(w, r) 578 | } 579 | } 580 | } 581 | 582 | var ( 583 | // load functions of all routers 584 | routers = []struct { 585 | name string 586 | load func(routes []route) *Possum 587 | }{ 588 | {"Possum", loadPossum}, 589 | } 590 | 591 | // all APIs 592 | apis = []struct { 593 | name string 594 | routes []route 595 | }{ 596 | {"GitHub", githubAPI}, 597 | {"GPlus", gplusAPI}, 598 | {"Parse", parseAPI}, 599 | {"Static", staticRoutes}, 600 | } 601 | ) 602 | 603 | func TestRouters(t *testing.T) { 604 | for _, router := range routers { 605 | req, _ := http.NewRequest("GET", "/", nil) 606 | u := req.URL 607 | rq := u.RawQuery 608 | 609 | for _, api := range apis { 610 | r := router.load(api.routes) 611 | 612 | for _, route := range api.routes { 613 | w := httptest.NewRecorder() 614 | req.Method = route.method 615 | req.RequestURI = route.path 616 | u.Path = route.path 617 | u.RawQuery = rq 618 | r.ServeHTTP(w, req) 619 | t.Log(w.Code, w.Body.String()) 620 | if w.Code != 200 || w.Body.String() != route.path { 621 | t.Errorf( 622 | "%s in API %s: %d - %s; expected %s %s\n", 623 | router.name, api.name, w.Code, w.Body.String(), route.method, route.path, 624 | ) 625 | } 626 | } 627 | } 628 | } 629 | } 630 | 631 | const ( 632 | fiveColon = "/:a/:b/:c/:d/:e" 633 | fiveBrace = "/{a}/{b}/{c}/{d}/{e}" 634 | fiveRoute = "/test/test/test/test/test" 635 | twentyColon = "/:a/:b/:c/:d/:e/:f/:g/:h/:i/:j/:k/:l/:m/:n/:o/:p/:q/:r/:s/:t" 636 | twentyBrace = "/{a}/{b}/{c}/{d}/{e}/{f}/{g}/{h}/{i}/{j}/{k}/{l}/{m}/{n}/{o}/{p}/{q}/{r}/{s}/{t}" 637 | twentyRoute = "/a/b/c/d/e/f/g/h/i/j/k/l/m/n/o/p/q/r/s/t" 638 | ) 639 | 640 | var ( 641 | githubPossum *Possum 642 | gplusPossum *Possum 643 | parsePossum *Possum 644 | staticPossum *Possum 645 | ) 646 | 647 | func init() { 648 | println("#GithubAPI Routes:", len(githubAPI)) 649 | 650 | calcMem("Possum", func() { 651 | githubPossum = loadPossum(githubAPI) 652 | }) 653 | 654 | println() 655 | 656 | println("#GPlusAPI Routes:", len(gplusAPI)) 657 | 658 | calcMem("Possum", func() { 659 | gplusPossum = loadPossum(gplusAPI) 660 | }) 661 | 662 | println() 663 | 664 | println("#ParseAPI Routes:", len(parseAPI)) 665 | 666 | calcMem("Possum", func() { 667 | parsePossum = loadPossum(parseAPI) 668 | }) 669 | 670 | println() 671 | 672 | println("#Static Routes:", len(staticRoutes)) 673 | 674 | calcMem("Possum", func() { 675 | staticPossum = loadPossum(staticRoutes) 676 | }) 677 | 678 | println() 679 | } 680 | 681 | // Possum 682 | func possumHandlerWrite(w http.ResponseWriter, req *http.Request) (interface{}, int) { 683 | name := req.URL.Query().Get("name") 684 | return name, http.StatusOK 685 | } 686 | 687 | func possumHandler(w http.ResponseWriter, req *http.Request) (interface{}, int) { 688 | return req.RequestURI, http.StatusOK 689 | } 690 | 691 | func loadPossum(routes []route) *Possum { 692 | mux := New() 693 | for _, r := range routes { 694 | mux.Add(router.Simple(r.path), possumHandler, view.Simple("text/html", "utf-8")) 695 | } 696 | return mux 697 | } 698 | 699 | func loadPossumSingle(method, path string, handler HandlerFunc) *Possum { 700 | mux := New() 701 | mux.Add(router.Simple(path), handler, view.Simple("text/html", "utf-8")) 702 | return mux 703 | } 704 | 705 | func BenchmarkPossum_Param(b *testing.B) { 706 | router := loadPossumSingle("GET", "/user/:name", possumHandler) 707 | 708 | r, _ := http.NewRequest("GET", "/user/gordon", nil) 709 | benchRequest(b, router, r) 710 | } 711 | 712 | func BenchmarkPossum_Param5(b *testing.B) { 713 | router := loadPossumSingle("GET", fiveColon, possumHandler) 714 | 715 | r, _ := http.NewRequest("GET", fiveRoute, nil) 716 | benchRequest(b, router, r) 717 | } 718 | 719 | func BenchmarkPossum_Param20(b *testing.B) { 720 | router := loadPossumSingle("GET", twentyColon, possumHandler) 721 | 722 | r, _ := http.NewRequest("GET", twentyRoute, nil) 723 | benchRequest(b, router, r) 724 | } 725 | 726 | func BenchmarkPossum_ParamWrite(b *testing.B) { 727 | router := loadPossumSingle("GET", "/user/:name", possumHandlerWrite) 728 | 729 | r, _ := http.NewRequest("GET", "/user/gordon", nil) 730 | benchRequest(b, router, r) 731 | } 732 | 733 | func BenchmarkPossum_GithubStatic(b *testing.B) { 734 | req, _ := http.NewRequest("GET", "/user/repos", nil) 735 | benchRequest(b, githubPossum, req) 736 | } 737 | 738 | func BenchmarkPossum_GithubParam(b *testing.B) { 739 | req, _ := http.NewRequest("GET", "/repos/julienschmidt/httprouter/stargazers", nil) 740 | benchRequest(b, githubPossum, req) 741 | } 742 | 743 | func BenchmarkPossum_GithubAll(b *testing.B) { 744 | benchRoutes(b, githubPossum, githubAPI) 745 | } 746 | 747 | func BenchmarkPossum_GPlusStatic(b *testing.B) { 748 | req, _ := http.NewRequest("GET", "/people", nil) 749 | benchRequest(b, gplusPossum, req) 750 | } 751 | 752 | func BenchmarkPossum_GPlusParam(b *testing.B) { 753 | req, _ := http.NewRequest("GET", "/people/118051310819094153327", nil) 754 | benchRequest(b, gplusPossum, req) 755 | } 756 | 757 | func BenchmarkPossum_GPlus2Params(b *testing.B) { 758 | req, _ := http.NewRequest("GET", "/people/118051310819094153327/activities/123456789", nil) 759 | benchRequest(b, gplusPossum, req) 760 | } 761 | 762 | func BenchmarkPossum_GPlusAll(b *testing.B) { 763 | benchRoutes(b, gplusPossum, gplusAPI) 764 | } 765 | 766 | func BenchmarkPossum_ParseStatic(b *testing.B) { 767 | req, _ := http.NewRequest("GET", "/1/users", nil) 768 | benchRequest(b, parsePossum, req) 769 | } 770 | 771 | func BenchmarkPossum_ParseParam(b *testing.B) { 772 | req, _ := http.NewRequest("GET", "/1/classes/go", nil) 773 | benchRequest(b, parsePossum, req) 774 | } 775 | 776 | func BenchmarkPossum_Parse2Params(b *testing.B) { 777 | req, _ := http.NewRequest("GET", "/1/classes/go/123456789", nil) 778 | benchRequest(b, parsePossum, req) 779 | } 780 | 781 | func BenchmarkPossum_ParseAll(b *testing.B) { 782 | benchRoutes(b, parsePossum, parseAPI) 783 | } 784 | 785 | func BenchmarkPossum_StaticAll(b *testing.B) { 786 | benchRoutes(b, staticPossum, staticRoutes) 787 | } 788 | -------------------------------------------------------------------------------- /server.go: -------------------------------------------------------------------------------- 1 | package possum 2 | 3 | import ( 4 | "container/list" 5 | "net/http" 6 | "net/url" 7 | "sync" 8 | 9 | "github.com/mikespook/possum/router" 10 | "github.com/mikespook/possum/view" 11 | ) 12 | 13 | type HandlerFunc func(w http.ResponseWriter, req *http.Request) (interface{}, int) 14 | 15 | // Possum implements `http.Handler`. 16 | type Possum struct { 17 | sync.RWMutex 18 | direct map[string]*rvcPack 19 | other *list.List 20 | 21 | // PreRequest is called after initialised the request and session, before user-defined handler 22 | PreRequest http.HandlerFunc 23 | // PreResponse is called before sending response to client. 24 | PreResponse http.HandlerFunc 25 | // ErrorHandler gets a chance to write user-defined error handler 26 | ErrorHandle func(error) 27 | } 28 | 29 | // rvcPack shorts for router/view/controller pack 30 | type rvcPack struct { 31 | router router.Router 32 | view view.View 33 | f HandlerFunc 34 | } 35 | 36 | // New initailizes Possum instance. 37 | func New() *Possum { 38 | return &Possum{ 39 | direct: make(map[string]*rvcPack), 40 | other: list.New(), 41 | } 42 | } 43 | 44 | // Add a router to list 45 | func (psm *Possum) Add(rtr router.Router, f HandlerFunc, view view.View) { 46 | defer psm.Unlock() 47 | psm.Lock() 48 | pack := &rvcPack{rtr, view, f} 49 | // direct will full-match the path 50 | if baseRouter, ok := rtr.(*router.Base); ok { 51 | psm.direct[baseRouter.Path] = pack 52 | return 53 | } 54 | psm.other.PushFront(pack) 55 | } 56 | 57 | // find a router with the specific path and return it. 58 | func (psm *Possum) find(path string) (url.Values, HandlerFunc, view.View) { 59 | defer psm.RUnlock() 60 | psm.RLock() 61 | if pack, ok := psm.direct[path]; ok { 62 | return nil, pack.f, pack.view 63 | } 64 | for e := psm.other.Front(); e != nil; e = e.Next() { 65 | pack := e.Value.(*rvcPack) 66 | if params, ok := pack.router.Match(path); ok { 67 | return params, pack.f, pack.view 68 | } 69 | } 70 | return nil, nil, nil 71 | } 72 | 73 | // init request handling, and set view and controler function to context. 74 | func (psm *Possum) init(req *http.Request) (HandlerFunc, view.View) { 75 | params, f, v := psm.find(req.URL.Path) 76 | if params != nil { 77 | if (*req).URL.RawQuery == "" { 78 | (*req).URL.RawQuery = params.Encode() 79 | } else { 80 | (*req).URL.RawQuery += "&" + params.Encode() 81 | } 82 | } 83 | 84 | if err := (*req).ParseForm(); err != nil { 85 | panic(Error{http.StatusBadRequest, err.Error()}) 86 | } 87 | 88 | if v == nil || f == nil { 89 | panic(Error{http.StatusNotFound, "Not Found"}) 90 | } 91 | return f, v 92 | } 93 | 94 | func (psm *Possum) ServeHTTP(w http.ResponseWriter, req *http.Request) { 95 | defer func() { 96 | r := recover() 97 | if errPanic, ok := r.(error); ok { 98 | handleError(w, errPanic) 99 | } 100 | }() 101 | defer psm.preResponseDefer(w, req) 102 | f, v := psm.init(req) 103 | psm.preRequest(w, req) 104 | var data interface{} 105 | var status int 106 | if f != nil { 107 | data, status = f(w, req) 108 | } 109 | if link, ok := data.(string); ok { 110 | if handleRedirect(w, req, status, link) { 111 | return 112 | } 113 | } 114 | handleRender(v, w, status, data) 115 | } 116 | 117 | func (psm *Possum) preRequest(w http.ResponseWriter, req *http.Request) func() { 118 | return func() { 119 | if psm.PreRequest != nil { 120 | psm.PreRequest(w, req) 121 | } 122 | } 123 | } 124 | 125 | func (psm *Possum) preResponseDefer(w http.ResponseWriter, req *http.Request) func() { 126 | return func() { 127 | if psm.PreResponse != nil { 128 | psm.PreResponse(w, req) 129 | } 130 | } 131 | } 132 | 133 | func handleRedirect(w http.ResponseWriter, req *http.Request, status int, link string) bool { 134 | if status != http.StatusMovedPermanently && 135 | status != http.StatusFound && 136 | status != http.StatusSeeOther && 137 | status != http.StatusTemporaryRedirect { 138 | return false 139 | } 140 | http.Redirect(w, req, link, status) 141 | return true 142 | } 143 | 144 | func handleRender(v view.View, w http.ResponseWriter, statusCode int, data interface{}) { 145 | body, header, err := v.Render(data) 146 | if err != nil { 147 | panic(Error{http.StatusInternalServerError, err.Error()}) 148 | } 149 | if header != nil { 150 | for key, values := range header { 151 | for _, value := range values { 152 | w.Header().Add(key, value) 153 | } 154 | } 155 | } 156 | w.WriteHeader(statusCode) 157 | if _, err = w.Write(body); err != nil { 158 | panic(Error{http.StatusInternalServerError, err.Error()}) 159 | } 160 | } 161 | -------------------------------------------------------------------------------- /session.go: -------------------------------------------------------------------------------- 1 | package possum 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/mikespook/possum/session" 7 | ) 8 | 9 | const sessionCookieName = "possum" 10 | 11 | // SessionFacotryFunc is the default factory function of session. 12 | var SessionFacotryFunc = session.NewFactory(session.CookieStorage(sessionCookieName, nil)) 13 | 14 | // Session extracts data from the request and returns session instance. 15 | // It uses SessionFacotryFunc to initialise session if no session has been set yet. 16 | func Session(w http.ResponseWriter, req *http.Request) (sn *session.Session, deferFunc func(), err error) { 17 | sn, err = SessionFacotryFunc(w, req) 18 | if err != nil { 19 | return nil, nil, err 20 | } 21 | return sn, func() { 22 | if err := sn.Flush(); err != nil { 23 | panic(err) 24 | } 25 | }, nil 26 | } 27 | -------------------------------------------------------------------------------- /session/cookie.go: -------------------------------------------------------------------------------- 1 | package session 2 | 3 | import ( 4 | "net/http" 5 | "time" 6 | ) 7 | 8 | const ( 9 | CookieDomain = "domain" 10 | CookieExpires = "expires" 11 | CookieHttpOnly = "http-only" 12 | CookieMaxAge = "max-age" 13 | CookiePath = "path" 14 | CookieSecure = "secure" 15 | ) 16 | 17 | var DefaultCookieOptions = M{ 18 | CookieDomain: "", 19 | CookieHttpOnly: false, 20 | CookieMaxAge: 3600, 21 | CookiePath: "/", 22 | CookieSecure: false, 23 | } 24 | 25 | var CleanCookieOptions = M{ 26 | CookieExpires: time.Now(), 27 | CookieMaxAge: 0, 28 | } 29 | 30 | type cookieStorage struct { 31 | keyName string 32 | options *http.Cookie 33 | } 34 | 35 | func fillCookie(options M, cookie *http.Cookie) { 36 | if domain, ok := options[CookieDomain]; ok { 37 | cookie.Domain = domain.(string) 38 | } 39 | if maxAge, ok := options[CookieMaxAge]; ok { 40 | maxAge := maxAge.(int) 41 | cookie.MaxAge = maxAge 42 | cookie.Expires = time.Now().Add(time.Duration(maxAge) * time.Second) 43 | } 44 | if expires, ok := options[CookieExpires]; ok { 45 | cookie.Expires = expires.(time.Time) 46 | } 47 | if httpOnly, ok := options[CookieHttpOnly]; ok { 48 | cookie.HttpOnly = httpOnly.(bool) 49 | } 50 | if path, ok := options[CookiePath]; ok { 51 | cookie.Path = path.(string) 52 | } 53 | if secure, ok := options[CookieSecure]; ok { 54 | cookie.Secure = secure.(bool) 55 | } 56 | } 57 | 58 | func fillOptions(cookie *http.Cookie) M { 59 | options := make(M) 60 | options[CookieDomain] = cookie.Domain 61 | options[CookieMaxAge] = cookie.MaxAge 62 | options[CookieExpires] = time.Now().Add(time.Duration(cookie.MaxAge) * time.Second) 63 | options[CookieHttpOnly] = cookie.HttpOnly 64 | options[CookiePath] = cookie.Path 65 | options[CookieSecure] = cookie.Secure 66 | return options 67 | } 68 | 69 | func mergeOptions(origin M, extend M) M { 70 | if origin == nil { 71 | return extend 72 | } 73 | for k, v := range extend { 74 | if _, ok := origin[k]; ok { 75 | continue 76 | } 77 | origin[k] = v 78 | } 79 | return origin 80 | } 81 | 82 | func cloneCookie(cookie *http.Cookie) *http.Cookie { 83 | newOne := &http.Cookie{ 84 | Domain: cookie.Domain, 85 | MaxAge: cookie.MaxAge, 86 | Expires: cookie.Expires, 87 | HttpOnly: cookie.HttpOnly, 88 | Path: cookie.Path, 89 | Secure: cookie.Secure, 90 | } 91 | return newOne 92 | } 93 | 94 | func (storage *cookieStorage) Clean(s *Session) error { 95 | key := &http.Cookie{Name: storage.keyName} 96 | fillCookie(CleanCookieOptions, key) 97 | http.SetCookie(s.w, key) 98 | value := &http.Cookie{Name: s.id} 99 | fillCookie(CleanCookieOptions, value) 100 | http.SetCookie(s.w, value) 101 | return nil 102 | } 103 | 104 | func (storage *cookieStorage) Flush(s *Session) error { 105 | key := cloneCookie(storage.options) 106 | key.Name = storage.keyName 107 | key.Value = s.id 108 | http.SetCookie(s.w, key) 109 | 110 | v, err := encoding([]byte(s.id), s.data) 111 | value := cloneCookie(storage.options) 112 | value.Name = s.id 113 | value.Value = v 114 | http.SetCookie(s.w, value) 115 | return err 116 | } 117 | 118 | func (storage *cookieStorage) LoadTo(r *http.Request, s *Session) error { 119 | s.storage = storage 120 | cookie, err := r.Cookie(storage.keyName) 121 | if err != nil { 122 | return err 123 | } 124 | s.id = cookie.Value 125 | cookie, err = r.Cookie(cookie.Value) 126 | if err != nil { 127 | return err 128 | } 129 | err = decoding([]byte(s.id), cookie.Value, &s.data) 130 | if err != nil { 131 | s.data = make(M) 132 | } 133 | return nil 134 | } 135 | 136 | func CookieStorage(keyName string, options M) Storage { 137 | if options == nil { 138 | options = DefaultCookieOptions 139 | } 140 | cookie := &http.Cookie{} 141 | fillCookie(options, cookie) 142 | return &cookieStorage{keyName, cookie} 143 | } 144 | -------------------------------------------------------------------------------- /session/cookie_test.go: -------------------------------------------------------------------------------- 1 | package session 2 | 3 | import ( 4 | "net/http" 5 | "strings" 6 | "testing" 7 | ) 8 | 9 | const ( 10 | sessionKey = "TEST_SESSION" 11 | cookieKey = "1234567890123456" 12 | ) 13 | 14 | func init() { 15 | SetKey([]byte("0000000000000000")) 16 | } 17 | 18 | type testResponse struct { 19 | header http.Header 20 | } 21 | 22 | func newTestResponse() *testResponse { return &testResponse{make(http.Header)} } 23 | func (resp *testResponse) Header() http.Header { return resp.header } 24 | func (resp *testResponse) Write(data []byte) (n int, err error) { return } 25 | func (resp *testResponse) WriteHeader(status int) {} 26 | func splitCookie(cookie string) *http.Cookie { 27 | str := strings.Split(cookie, ";") 28 | str = strings.SplitN(str[0], "=", 2) 29 | return &http.Cookie{Name: str[0], Value: str[1]} 30 | } 31 | 32 | func TestCookieStorage(t *testing.T) { 33 | r, err := http.NewRequest("GET", "http://127.0.0.1", nil) 34 | if err != nil { 35 | t.Error(err) 36 | return 37 | } 38 | storage := CookieStorage(sessionKey, M{CookieMaxAge: 1234}) 39 | s := &Session{} 40 | s.Init() 41 | if err := storage.LoadTo(r, s); err == nil { 42 | t.Errorf("No-named cookie error should be presented.") 43 | return 44 | } 45 | s.id = cookieKey 46 | 47 | resp := newTestResponse() 48 | s.w = resp 49 | s.Set("foo", 123) 50 | storage.Flush(s) 51 | for _, setCookie := range resp.header["Set-Cookie"] { 52 | if strings.Index(setCookie, "Max-Age=1234") == -1 { 53 | t.Errorf("Options not effective: %s", setCookie) 54 | return 55 | } 56 | r.AddCookie(splitCookie(setCookie)) 57 | } 58 | 59 | if err := storage.LoadTo(r, s); err != nil { 60 | t.Error(err) 61 | return 62 | } 63 | 64 | if s.Id() != cookieKey { 65 | t.Errorf("text[%s] != origin[%s]", s.Id(), cookieKey) 66 | return 67 | } 68 | 69 | foo := s.Get("foo") 70 | if v, ok := foo.(int); !ok || v != 123 { 71 | t.Errorf("Session load issue") 72 | return 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /session/factory.go: -------------------------------------------------------------------------------- 1 | package session 2 | 3 | import ( 4 | "net/http" 5 | ) 6 | 7 | type FactoryFunc func(http.ResponseWriter, *http.Request) (*Session, error) 8 | 9 | func NewFactory(storage Storage) FactoryFunc { 10 | return func(w http.ResponseWriter, r *http.Request) (s *Session, err error) { 11 | s = &Session{ 12 | storage: storage, 13 | w: w, 14 | } 15 | if err = s.Init(); err != nil { 16 | return 17 | } 18 | if err = storage.LoadTo(r, s); err != http.ErrNoCookie { 19 | return 20 | } 21 | return s, nil 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /session/session.go: -------------------------------------------------------------------------------- 1 | package session 2 | 3 | import ( 4 | "crypto/rand" 5 | "crypto/sha256" 6 | "fmt" 7 | "net/http" 8 | "sync" 9 | ) 10 | 11 | type M map[string]interface{} 12 | 13 | type Session struct { 14 | sync.RWMutex 15 | data M 16 | id string 17 | storage Storage 18 | w http.ResponseWriter 19 | } 20 | 21 | func (s *Session) Id() string { 22 | return s.id 23 | } 24 | 25 | func (s *Session) Set(key string, value interface{}) { 26 | defer s.Unlock() 27 | s.Lock() 28 | s.data[key] = value 29 | } 30 | 31 | func (s *Session) Get(key string) (value interface{}) { 32 | defer s.RUnlock() 33 | s.RLock() 34 | return s.data[key] 35 | } 36 | 37 | func (s *Session) Del(key string) (value interface{}) { 38 | defer s.Unlock() 39 | s.Lock() 40 | value = s.data[key] 41 | delete(s.data, key) 42 | return 43 | } 44 | 45 | func (s *Session) init() { 46 | s.data = make(M) 47 | s.id = fmt.Sprintf("%x", genKey(512)) 48 | } 49 | 50 | func (s *Session) Init() error { 51 | defer s.Unlock() 52 | s.Lock() 53 | s.init() 54 | return nil 55 | } 56 | 57 | func (s *Session) Clean() error { 58 | defer func() { 59 | s.init() 60 | s.Unlock() 61 | }() 62 | s.Lock() 63 | return s.storage.Clean(s) 64 | } 65 | 66 | func (s *Session) Flush() error { 67 | defer s.Unlock() 68 | s.Lock() 69 | return s.storage.Flush(s) 70 | } 71 | 72 | func genKey(size int) []byte { 73 | b := make([]byte, size) 74 | if _, err := rand.Read(b); err != nil { 75 | return nil 76 | } 77 | h := sha256.New() 78 | h.Write(b) 79 | return h.Sum(nil) 80 | } 81 | -------------------------------------------------------------------------------- /session/storage.go: -------------------------------------------------------------------------------- 1 | package session 2 | 3 | import ( 4 | "bytes" 5 | "crypto/aes" 6 | "crypto/cipher" 7 | "crypto/rand" 8 | "encoding/base64" 9 | "encoding/gob" 10 | "errors" 11 | "net/http" 12 | ) 13 | 14 | func init() { 15 | gob.Register(M{}) 16 | } 17 | 18 | type Storage interface { 19 | Clean(*Session) error 20 | Flush(*Session) error 21 | LoadTo(*http.Request, *Session) error 22 | } 23 | 24 | const ( 25 | keySize = 16 26 | aesKeySize = 32 27 | ) 28 | 29 | var ( 30 | defaultKey = genKey(keySize)[:keySize] 31 | ) 32 | 33 | func SetKey(key []byte) { 34 | defaultKey = key[:keySize] 35 | } 36 | 37 | func GetKey() []byte { 38 | return defaultKey 39 | } 40 | 41 | var errKeyTooShort = errors.New("The key is too short") 42 | var errValueTooShort = errors.New("The value is too short") 43 | 44 | func encrypt(key, value []byte) ([]byte, error) { 45 | if len(key) < aesKeySize-keySize { 46 | return nil, errKeyTooShort 47 | } 48 | key = append(key[:aesKeySize-keySize], defaultKey...) 49 | block, err := aes.NewCipher(key) 50 | if err != nil { 51 | return nil, err 52 | } 53 | iv := make([]byte, block.BlockSize()) 54 | rand.Read(iv) 55 | stream := cipher.NewCTR(block, iv) 56 | stream.XORKeyStream(value, value) 57 | return append(iv, value...), nil 58 | } 59 | 60 | func decrypt(key, value []byte) ([]byte, error) { 61 | if len(key) < aesKeySize-keySize { 62 | return nil, errKeyTooShort 63 | } 64 | key = append(key[:aesKeySize-keySize], defaultKey...) 65 | block, err := aes.NewCipher(key) 66 | if err != nil { 67 | return nil, err 68 | } 69 | if len(value) > block.BlockSize() { 70 | iv := value[:block.BlockSize()] 71 | value = value[block.BlockSize():] 72 | stream := cipher.NewCTR(block, iv) 73 | stream.XORKeyStream(value, value) 74 | return value, nil 75 | } 76 | return nil, errValueTooShort 77 | } 78 | 79 | func decoding(key []byte, src string, dst *M) error { 80 | // 1. base64 decoding 81 | buf, err := base64.StdEncoding.DecodeString(src) 82 | if err != nil { 83 | return err 84 | } 85 | // 2. cypto decoding 86 | buf, err = decrypt(key, buf) 87 | if err != nil { 88 | return err 89 | } 90 | // 3. gob decoding 91 | g := gob.NewDecoder(bytes.NewBuffer(buf)) 92 | if err = g.Decode(&dst); err != nil { 93 | return err 94 | } 95 | return nil 96 | } 97 | 98 | func encoding(key []byte, src map[string]interface{}) (string, error) { 99 | // 1. gob encoding 100 | var buf bytes.Buffer 101 | g := gob.NewEncoder(&buf) 102 | if err := g.Encode(src); err != nil { 103 | return "", err 104 | } 105 | // 2. cypto encoding 106 | ciphertext, err := encrypt(key, buf.Bytes()) 107 | if err != nil { 108 | return "", err 109 | } 110 | // 3. base64 encoding 111 | return base64.StdEncoding.EncodeToString(ciphertext), nil 112 | } 113 | -------------------------------------------------------------------------------- /session/storage_test.go: -------------------------------------------------------------------------------- 1 | package session 2 | 3 | import ( 4 | "bytes" 5 | "testing" 6 | ) 7 | 8 | var ( 9 | legalKey = []byte("1234567890123456") 10 | tooShortKey = []byte("1234567890") 11 | tooLongKey = []byte("12345678901234567890") 12 | originText = []byte("a brown fox jumps over the lazy dog") 13 | ) 14 | 15 | func TestCrypt(t *testing.T) { 16 | // Vital, encrypt will reuse value's allocation. 17 | src := make([]byte, len(originText)) 18 | copy(src, originText) 19 | cipherText, err := encrypt(legalKey, src) 20 | if err != nil { 21 | t.Error(err) 22 | return 23 | } 24 | text, err := decrypt(legalKey, cipherText) 25 | if err != nil { 26 | t.Error(err) 27 | return 28 | } 29 | if bytes.Compare(originText, text) != 0 { 30 | t.Errorf("text[%s] != origin[%s]", text, originText) 31 | return 32 | } 33 | copy(src, originText) 34 | _, err = encrypt(tooShortKey, src) 35 | if err != errKeyTooShort { 36 | t.Errorf("Error %s needed", errKeyTooShort) 37 | return 38 | } 39 | _, err = decrypt(tooShortKey, cipherText) 40 | if err != errKeyTooShort { 41 | t.Errorf("Error %s needed", errKeyTooShort) 42 | return 43 | } 44 | 45 | copy(src, originText) 46 | cipherText, err = encrypt(tooLongKey, src) 47 | if err != nil { 48 | t.Error(err) 49 | return 50 | } 51 | text, err = decrypt(tooLongKey, cipherText) 52 | if err != nil { 53 | t.Error(err) 54 | return 55 | } 56 | if bytes.Compare(originText, text) != 0 { 57 | t.Errorf("[%s] != [%s]", originText, text) 58 | return 59 | } 60 | } 61 | 62 | func TestCoding(t *testing.T) { 63 | srcM := make(M) 64 | srcM["foo"] = 123 65 | srcM["bar"] = "abc" 66 | var dstM M 67 | cipherText, err := encoding(legalKey, srcM) 68 | if err != nil { 69 | t.Error(err) 70 | return 71 | } 72 | if err := decoding(legalKey, cipherText, &dstM); err != nil { 73 | t.Error(err) 74 | return 75 | } 76 | 77 | if v, ok := dstM["foo"]; !ok || v != 123 { 78 | t.Errorf("Decofing issue: %s", dstM["foo"]) 79 | return 80 | } 81 | 82 | cipherText, err = encoding(tooShortKey, srcM) 83 | if err != errKeyTooShort { 84 | t.Errorf("Error %s needed", errKeyTooShort) 85 | return 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /view/json.go: -------------------------------------------------------------------------------- 1 | package view 2 | 3 | import ( 4 | j "encoding/json" 5 | "fmt" 6 | "net/http" 7 | ) 8 | 9 | // Json renders response data to JSON format. 10 | type json struct { 11 | header http.Header 12 | } 13 | 14 | func (view json) Render(data interface{}) (output []byte, h http.Header, err error) { 15 | output, err = j.Marshal(data) 16 | h = view.header 17 | return 18 | } 19 | 20 | func Json(charSet string) json { 21 | if charSet == "" { 22 | charSet = CharSetUTF8 23 | } 24 | header := make(http.Header) 25 | header.Set("Content-Type", 26 | fmt.Sprintf("%s; charset=%s", ContentTypeJSON, charSet)) 27 | return json{header} 28 | } 29 | -------------------------------------------------------------------------------- /view/json_test.go: -------------------------------------------------------------------------------- 1 | package view 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | ) 7 | 8 | var jsonTestCases = map[string]interface{}{ 9 | "123": 123, 10 | "\"foobar\"": "foobar", 11 | "true": true, 12 | "[0,1,2,3]": []int{0, 1, 2, 3}, 13 | "{\"Foo\":\"bar\"}": struct{ Foo string }{"bar"}, 14 | } 15 | 16 | func TestJsonRendering(t *testing.T) { 17 | jv := Json("") 18 | for k, v := range jsonTestCases { 19 | body, header, err := jv.Render(v) 20 | if err != nil { 21 | t.Fatal(err) 22 | } 23 | if string(body) != k { 24 | t.Fatalf("%v should be rendered to %s, got %s.", v, k, body) 25 | } 26 | a := header.Get("Content-Type") 27 | b := fmt.Sprintf("%s; charset=%s", ContentTypeJSON, CharSetUTF8) 28 | if a != b { 29 | t.Errorf("Expected Content-Type is %s, got %s.", b, a) 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /view/preload.go: -------------------------------------------------------------------------------- 1 | package view 2 | 3 | import ( 4 | "io/ioutil" 5 | "mime" 6 | "net/http" 7 | "path" 8 | ) 9 | 10 | // PreloadFile returns the view which can preload static files and serve them. 11 | // The different between `StaticFile` and `PreloadFile` is that `StaticFile` 12 | // load the content of file at every request, while `PreloadFile` load 13 | // the content into memory at the initial stage. Despite that `PreloadFile` 14 | // will be using more memories and could not update the content in time until 15 | // restart the application, it should be fast than `StaticFile` in runtime. 16 | func PreloadFile(filename string, contentType string) (preloadFile, error) { 17 | body, err := ioutil.ReadFile(filename) 18 | if err != nil { 19 | return preloadFile{}, err 20 | } 21 | if contentType == "" { 22 | contentType = mime.TypeByExtension(path.Ext(filename)) 23 | } 24 | header := make(http.Header) 25 | header.Set("Content-Type", contentType) 26 | return preloadFile{body, header}, nil 27 | } 28 | 29 | type preloadFile struct { 30 | body []byte 31 | header http.Header 32 | } 33 | 34 | func (view preloadFile) Render(data interface{}) (output []byte, h http.Header, err error) { 35 | return view.body, view.header, nil 36 | } 37 | -------------------------------------------------------------------------------- /view/preload_test.go: -------------------------------------------------------------------------------- 1 | package view 2 | 3 | import "testing" 4 | 5 | func TestPreloadRendering(t *testing.T) { 6 | f := _createFile(t, _body) 7 | defer _deleteFile(f) 8 | 9 | pv, err := PreloadFile(f, ContentTypeBinary) 10 | if err != nil { 11 | t.Fatal(err) 12 | } 13 | 14 | body, header, err := pv.Render(nil) 15 | if err != nil { 16 | t.Fatal(err) 17 | } 18 | if string(body) != _body { 19 | t.Fatalf("%s should be rendered to %s, got %s.", f, _body, body) 20 | } 21 | a := header.Get("Content-Type") 22 | if a != ContentTypeBinary { 23 | t.Errorf("Expected Content-Type is %s, got %s.", ContentTypeBinary, a) 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /view/simple.go: -------------------------------------------------------------------------------- 1 | package view 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "net/http" 7 | ) 8 | 9 | // Simple reads and responses data directly. 10 | type simple struct { 11 | header http.Header 12 | } 13 | 14 | func (view simple) Render(data interface{}) (output []byte, h http.Header, err error) { 15 | if data == nil { 16 | return nil, view.header, nil 17 | } 18 | var buf bytes.Buffer 19 | if _, err = buf.WriteString(fmt.Sprintf("%s", data)); err != nil { 20 | return 21 | } 22 | return buf.Bytes(), view.header, nil 23 | } 24 | 25 | func Simple(contentType, charSet string) simple { 26 | if contentType == "" { 27 | contentType = ContentTypePlain 28 | } 29 | if charSet == "" { 30 | charSet = CharSetUTF8 31 | } 32 | header := make(http.Header) 33 | header.Set("Content-Type", 34 | fmt.Sprintf("%s; charset=%s", contentType, charSet)) 35 | return simple{header} 36 | } 37 | -------------------------------------------------------------------------------- /view/simple_test.go: -------------------------------------------------------------------------------- 1 | package view 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | ) 7 | 8 | var simpleTestCases = map[string]interface{}{ 9 | "foobar": "foobar", 10 | "

foobar

": "

foobar

", 11 | } 12 | 13 | func TestSimpleRendering(t *testing.T) { 14 | sv := Simple("", "") 15 | for k, v := range simpleTestCases { 16 | body, header, err := sv.Render(v) 17 | if err != nil { 18 | t.Fatal(err) 19 | } 20 | if string(body) != k { 21 | t.Fatalf("%v should be rendered to %s, got %s.", v, k, body) 22 | } 23 | a := header.Get("Content-Type") 24 | b := fmt.Sprintf("%s; charset=%s", ContentTypePlain, CharSetUTF8) 25 | if a != b { 26 | t.Errorf("Expected Content-Type is %s, got %s.", b, a) 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /view/static.go: -------------------------------------------------------------------------------- 1 | package view 2 | 3 | import ( 4 | "io/ioutil" 5 | "mime" 6 | "net/http" 7 | "path" 8 | ) 9 | 10 | // StaticFile returns the view which can serve static files. 11 | func StaticFile(filename string, contentType string) staticFile { 12 | if contentType == "" { 13 | contentType = mime.TypeByExtension(path.Ext(filename)) 14 | } 15 | header := make(http.Header) 16 | header.Set("Content-Type", contentType) 17 | return staticFile{filename, header} 18 | } 19 | 20 | type staticFile struct { 21 | filename string 22 | header http.Header 23 | } 24 | 25 | func (view staticFile) Render(data interface{}) (output []byte, h http.Header, err error) { 26 | output, err = ioutil.ReadFile(view.filename) 27 | h = view.header 28 | return 29 | } 30 | -------------------------------------------------------------------------------- /view/static_test.go: -------------------------------------------------------------------------------- 1 | package view 2 | 3 | import "testing" 4 | 5 | func TestStaticRendering(t *testing.T) { 6 | f := _createFile(t, _body) 7 | defer _deleteFile(f) 8 | 9 | sv := StaticFile(f, ContentTypeBinary) 10 | body, header, err := sv.Render(nil) 11 | if err != nil { 12 | t.Fatal(err) 13 | } 14 | if string(body) != _body { 15 | t.Fatalf("%s should be rendered to %s, got %s.", f, _body, body) 16 | } 17 | 18 | a := header.Get("Content-Type") 19 | if a != ContentTypeBinary { 20 | t.Errorf("Expected Content-Type is %s, got %s.", ContentTypeBinary, a) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /view/template.go: -------------------------------------------------------------------------------- 1 | package view 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "io" 7 | "net/http" 8 | "path/filepath" 9 | "sync/atomic" 10 | 11 | html "html/template" 12 | text "text/template" 13 | 14 | "gopkg.in/fsnotify.v1" 15 | ) 16 | 17 | // `tmp` is an interface to render template `name` with data, 18 | // and output to wr. 19 | type tmp interface { 20 | ExecuteTemplate(wr io.Writer, name string, data interface{}) error 21 | } 22 | 23 | var ( 24 | htmlTemp struct { 25 | *html.Template 26 | } 27 | textTemp struct { 28 | *text.Template 29 | } 30 | 31 | watcher struct { 32 | *fsnotify.Watcher 33 | closer chan bool 34 | count uint32 35 | } 36 | ) 37 | 38 | // InitHtmlTemplates initialzes a series of HTML templates 39 | // in the directory `pattern`. 40 | func InitHtmlTemplates(pattern string) (err error) { 41 | htmlTemp.Template, err = html.ParseGlob(pattern) 42 | return 43 | } 44 | 45 | // InitTextTemplates initialzes a series of plain text templates 46 | // in the directory `pattern`. 47 | func InitTextTemplates(pattern string) (err error) { 48 | textTemp.Template, err = text.ParseGlob(pattern) 49 | return nil 50 | } 51 | 52 | // Html retruns a TemplateView witch uses HTML templates internally. 53 | func Html(name, contentType, charSet string) template { 54 | if htmlTemp.Template == nil { 55 | panic("Function `InitHtmlTemplates` should be called first.") 56 | } 57 | if contentType == "" { 58 | contentType = ContentTypeHTML 59 | } 60 | if charSet == "" { 61 | charSet = CharSetUTF8 62 | } 63 | header := make(http.Header) 64 | header.Set("Content-Type", 65 | fmt.Sprintf("%s; charset=%s", contentType, charSet)) 66 | return template{&htmlTemp, name, header} 67 | } 68 | 69 | // Text retruns a TemplateView witch uses text templates internally. 70 | func Text(name, contentType, charSet string) template { 71 | if textTemp.Template == nil { 72 | panic("Function `InitTextTemplates` should be called first.") 73 | } 74 | if contentType == "" { 75 | contentType = ContentTypePlain 76 | } 77 | if charSet == "" { 78 | charSet = CharSetUTF8 79 | } 80 | header := make(http.Header) 81 | header.Set("Content-Type", 82 | fmt.Sprintf("%s; charset=%s", contentType, charSet)) 83 | return template{&textTemp, name, header} 84 | } 85 | 86 | // InitWatcher initialzes a watcher to watch templates changes in the `pattern`. 87 | // f would be InitHtmlTemplates or InitTextTemplates. 88 | // If the watcher raises an error internally, the callback function ef will be executed. 89 | // ef can be nil witch represents ignoring all internal errors. 90 | func InitWatcher(pattern string, f func(string) error, ef func(error)) (err error) { 91 | if err = f(pattern); err != nil { 92 | return 93 | } 94 | if watcher.Watcher == nil { 95 | watcher.Watcher, err = fsnotify.NewWatcher() 96 | if err != nil { 97 | return 98 | } 99 | watcher.closer = make(chan bool) 100 | } 101 | go func() { 102 | atomic.AddUint32(&watcher.count, 1) 103 | for { 104 | select { 105 | case <-watcher.Events: 106 | if err := f(pattern); err != nil { 107 | ef(err) 108 | } 109 | case err := <-watcher.Errors: 110 | if ef != nil { 111 | ef(err) 112 | } 113 | case <-watcher.closer: 114 | break 115 | } 116 | } 117 | }() 118 | 119 | var matches []string 120 | matches, err = filepath.Glob(pattern) 121 | if err != nil { 122 | return 123 | } 124 | for _, v := range matches { 125 | if err = watcher.Add(v); err != nil { 126 | return 127 | } 128 | } 129 | return 130 | } 131 | 132 | // CloseWatcher closes the wathcer. 133 | func CloseWatcher() error { 134 | for i := uint32(0); i < watcher.count; i++ { 135 | watcher.closer <- true 136 | } 137 | return watcher.Close() 138 | } 139 | 140 | // Template represents `html/template` and `text/template` view. 141 | type template struct { 142 | tmp 143 | name string 144 | header http.Header 145 | } 146 | 147 | func (view template) Render(data interface{}) (output []byte, h http.Header, err error) { 148 | var buf bytes.Buffer 149 | if err = view.ExecuteTemplate(&buf, view.name, data); err != nil { 150 | return 151 | } 152 | output = buf.Bytes() 153 | h = view.header 154 | return 155 | } 156 | -------------------------------------------------------------------------------- /view/template_test.go: -------------------------------------------------------------------------------- 1 | package view 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "strings" 7 | "testing" 8 | ) 9 | 10 | func TestTextTemplates(t *testing.T) { 11 | f := _createFile(t, _textTemplate) 12 | defer _deleteFile(f) 13 | 14 | err := InitTextTemplates(os.TempDir() + "/*.testing") 15 | if err != nil { 16 | t.Fatal(err) 17 | } 18 | 19 | v := Text("possum.testing", "", "") 20 | body, header, err := v.Render(_body) 21 | if err != nil { 22 | t.Fatal(err) 23 | } 24 | a := strings.Replace(_textTemplate, "{{.}}", _body, -1) 25 | if string(body) != a { 26 | t.Fatalf("Rendered template should be %s, got %s.", a, body) 27 | } 28 | a = header.Get("Content-Type") 29 | b := fmt.Sprintf("%s; charset=%s", ContentTypePlain, CharSetUTF8) 30 | if a != b { 31 | t.Errorf("Expected Content-Type is %s, got %s.", b, a) 32 | } 33 | } 34 | 35 | func TestHtmlTemplates(t *testing.T) { 36 | f := _createFile(t, _htmlTemplate) 37 | defer _deleteFile(f) 38 | 39 | err := InitHtmlTemplates(os.TempDir() + "/*.testing") 40 | if err != nil { 41 | t.Fatal(err) 42 | } 43 | 44 | v := Html("possum.testing", "", "") 45 | body, header, err := v.Render(_body) 46 | if err != nil { 47 | t.Fatal(err) 48 | } 49 | a := strings.Replace(_htmlTemplate, "{{.}}", _body, -1) 50 | if string(body) != a { 51 | t.Fatalf("Rendered template should be %s, got %s.", _body, a) 52 | } 53 | a = header.Get("Content-Type") 54 | b := fmt.Sprintf("%s; charset=%s", ContentTypeHTML, CharSetUTF8) 55 | if a != b { 56 | t.Errorf("Expected Content-Type is %s, got %s.", b, a) 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /view/view.go: -------------------------------------------------------------------------------- 1 | package view 2 | 3 | import "net/http" 4 | 5 | const ( 6 | ContentTypeJSON = "application/json" 7 | ContentTypeHTML = "text/html" 8 | ContentTypePlain = "text/plain" 9 | ContentTypeBinary = "application/octet-stream" 10 | 11 | CharSetUTF8 = "utf-8" 12 | ) 13 | 14 | // View is an interface to render response with a specific format. 15 | type View interface { 16 | Render(interface{}) ([]byte, http.Header, error) 17 | } 18 | -------------------------------------------------------------------------------- /view/view_test.go: -------------------------------------------------------------------------------- 1 | package view 2 | 3 | import ( 4 | "io/ioutil" 5 | "os" 6 | "path" 7 | "testing" 8 | ) 9 | 10 | const ( 11 | _body = "the quick brown fox jumped over the lazy dog" 12 | _textTemplate = "They said: {{.}}" 13 | _htmlTemplate = "They said:

{{.}}

" 14 | ) 15 | 16 | func _createFile(t *testing.T, content string) (filename string) { 17 | filename = path.Join(os.TempDir(), "possum.testing") 18 | if err := ioutil.WriteFile(filename, []byte(content), 0644); err != nil { 19 | t.Fatal(err) 20 | } 21 | return 22 | } 23 | 24 | func _deleteFile(filename string) { 25 | os.Remove(filename) 26 | } 27 | --------------------------------------------------------------------------------