├── .gitignore
├── .travis.yml
├── examples
├── hello.go
├── logger.go
├── multiserver.go
├── streaming.go
├── arcchallenge.go
├── params.go
├── cookie.go
├── secure_cookie.go
├── multipart.go
└── tls.go
├── fcgi.go
├── ttycolors.go
├── LICENSE
├── Readme.md
├── secure_cookie.go
├── helpers.go
├── scgi.go
├── web.go
├── server.go
└── web_test.go
/.gitignore:
--------------------------------------------------------------------------------
1 | *.6
2 | *.8
3 | *.o
4 | *.so
5 | *.out
6 | *.go~
7 | *.cgo?.*
8 | _cgo_*
9 | _obj
10 | _test
11 | _testmain.go
12 | *.swp
13 | _site
14 | .DS_Store
15 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: go
2 |
3 | go:
4 | - 1.3
5 | - 1.4
6 | - 1.5
7 | - 1.6
8 |
9 | install: go get
10 |
11 | script:
12 | - diff -u <(echo -n) <(gofmt -d -s .)
13 | - go test -short
14 |
--------------------------------------------------------------------------------
/examples/hello.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "github.com/hoisie/web"
5 | )
6 |
7 | func hello(val string) string { return "hello " + val + "\n" }
8 |
9 | func main() {
10 | web.Get("/(.*)", hello)
11 | web.Run("0.0.0.0:9999")
12 | }
13 |
--------------------------------------------------------------------------------
/examples/logger.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "github.com/hoisie/web"
5 | "log"
6 | "os"
7 | )
8 |
9 | func hello(val string) string { return "hello " + val + "\n" }
10 |
11 | func main() {
12 | f, err := os.Create("server.log")
13 | if err != nil {
14 | println(err.Error())
15 | return
16 | }
17 | logger := log.New(f, "", log.Ldate|log.Ltime)
18 | web.Get("/(.*)", hello)
19 | web.SetLogger(logger)
20 | web.Run("0.0.0.0:9999")
21 | }
22 |
--------------------------------------------------------------------------------
/examples/multiserver.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "github.com/hoisie/web"
5 | )
6 |
7 | func hello1(val string) string { return "hello1 " + val + "\n" }
8 |
9 | func hello2(val string) string { return "hello2 " + val + "\n" }
10 |
11 | func main() {
12 | var server1 web.Server
13 | var server2 web.Server
14 |
15 | server1.Get("/(.*)", hello1)
16 | go server1.Run("0.0.0.0:9999")
17 | server2.Get("/(.*)", hello2)
18 | go server2.Run("0.0.0.0:8999")
19 | <-make(chan int)
20 | }
21 |
--------------------------------------------------------------------------------
/examples/streaming.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "github.com/hoisie/web"
5 | "net/http"
6 | "strconv"
7 | "time"
8 | )
9 |
10 | func hello(ctx *web.Context, num string) {
11 | flusher, _ := ctx.ResponseWriter.(http.Flusher)
12 | flusher.Flush()
13 | n, _ := strconv.ParseInt(num, 10, 64)
14 | for i := int64(0); i < n; i++ {
15 | ctx.WriteString("
hello world")
16 | flusher.Flush()
17 | time.Sleep(1e9)
18 | }
19 | }
20 |
21 | func main() {
22 | web.Get("/([0-9]+)", hello)
23 | web.Run("0.0.0.0:9999")
24 | }
25 |
--------------------------------------------------------------------------------
/fcgi.go:
--------------------------------------------------------------------------------
1 | package web
2 |
3 | import (
4 | "net"
5 | "net/http/fcgi"
6 | )
7 |
8 | func (s *Server) listenAndServeFcgi(addr string) error {
9 | var l net.Listener
10 | var err error
11 |
12 | //if the path begins with a "/", assume it's a unix address
13 | if addr[0] == '/' {
14 | l, err = net.Listen("unix", addr)
15 | } else {
16 | l, err = net.Listen("tcp", addr)
17 | }
18 |
19 | //save the listener so it can be closed
20 | s.l = l
21 |
22 | if err != nil {
23 | s.Logger.Println("FCGI listen error", err.Error())
24 | return err
25 | }
26 | return fcgi.Serve(s.l, s)
27 | }
28 |
--------------------------------------------------------------------------------
/ttycolors.go:
--------------------------------------------------------------------------------
1 | package web
2 |
3 | import (
4 | "golang.org/x/crypto/ssh/terminal"
5 | "syscall"
6 | )
7 |
8 | var ttyCodes struct {
9 | green string
10 | white string
11 | reset string
12 | }
13 |
14 | func init() {
15 | ttyCodes.green = ttyBold("32")
16 | ttyCodes.white = ttyBold("37")
17 | ttyCodes.reset = ttyEscape("0")
18 | }
19 |
20 | func ttyBold(code string) string {
21 | return ttyEscape("1;" + code)
22 | }
23 |
24 | func ttyEscape(code string) string {
25 | if terminal.IsTerminal(syscall.Stdout) {
26 | return "\x1b[" + code + "m"
27 | } else {
28 | return ""
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/examples/arcchallenge.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "fmt"
5 | "github.com/hoisie/web"
6 | "math/rand"
7 | "time"
8 | )
9 |
10 | var form = `
input1: " + form.Value["input1"][0] + "
") 45 | output.WriteString("input2: " + form.Value["input2"][0] + "
") 46 | 47 | fileHeader := form.File["file"][0] 48 | filename := fileHeader.Filename 49 | file, err := fileHeader.Open() 50 | if err != nil { 51 | return err.Error() 52 | } 53 | 54 | output.WriteString("file: " + filename + " " + Md5(file) + "
") 55 | return output.String() 56 | } 57 | 58 | func main() { 59 | web.Get("/", index) 60 | web.Post("/multipart", multipart) 61 | web.Run("0.0.0.0:9999") 62 | } 63 | -------------------------------------------------------------------------------- /Readme.md: -------------------------------------------------------------------------------- 1 | [](https://travis-ci.org/hoisie/web) 2 | 3 | # web.go 4 | 5 | web.go is the simplest way to write web applications in the Go programming language. It's ideal for writing simple, performant backend web services. 6 | 7 | ## Overview 8 | 9 | web.go should be familiar to people who've developed websites with higher-level web frameworks like sinatra or web.py. It is designed to be a lightweight web framework that doesn't impose any scaffolding on the user. Some features include: 10 | 11 | * Routing to url handlers based on regular expressions 12 | * Secure cookies 13 | * Support for fastcgi and scgi 14 | * Web applications are compiled to native code. This means very fast execution and page render speed 15 | * Efficiently serving static files 16 | 17 | ## Installation 18 | 19 | Make sure you have the a working Go environment. See the [install instructions](http://golang.org/doc/install.html). web.go targets the Go `release` branch. 20 | 21 | To install web.go, simply run: 22 | 23 | go get github.com/hoisie/web 24 | 25 | To compile it from source: 26 | 27 | git clone git://github.com/hoisie/web.git 28 | cd web && go build 29 | 30 | ## Example 31 | ```go 32 | package main 33 | 34 | import ( 35 | "github.com/hoisie/web" 36 | ) 37 | 38 | func hello(val string) string { return "hello " + val } 39 | 40 | func main() { 41 | web.Get("/(.*)", hello) 42 | web.Run("0.0.0.0:9999") 43 | } 44 | ``` 45 | 46 | To run the application, put the code in a file called hello.go and run: 47 | 48 | go run hello.go 49 | 50 | You can point your browser to http://localhost:9999/world . 51 | 52 | ### Getting parameters 53 | 54 | Route handlers may contain a pointer to web.Context as their first parameter. This variable serves many purposes -- it contains information about the request, and it provides methods to control the http connection. For instance, to iterate over the web parameters, either from the URL of a GET request, or the form data of a POST request, you can access `ctx.Params`, which is a `map[string]string`: 55 | 56 | ```go 57 | package main 58 | 59 | import ( 60 | "github.com/hoisie/web" 61 | ) 62 | 63 | func hello(ctx *web.Context, val string) { 64 | for k,v := range ctx.Params { 65 | println(k, v) 66 | } 67 | } 68 | 69 | func main() { 70 | web.Get("/(.*)", hello) 71 | web.Run("0.0.0.0:9999") 72 | } 73 | ``` 74 | 75 | In this example, if you visit `http://localhost:9999/?a=1&b=2`, you'll see the following printed out in the terminal: 76 | 77 | a 1 78 | b 2 79 | 80 | ## Documentation 81 | 82 | API docs are hosted at https://hoisie.github.io/web/ 83 | 84 | If you use web.go, I'd greatly appreciate a quick message about what you're building with it. This will help me get a sense of usage patterns, and helps me focus development efforts on features that people will actually use. 85 | 86 | ## About 87 | 88 | web.go was written by Michael Hoisie 89 | 90 | 91 | -------------------------------------------------------------------------------- /examples/tls.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "crypto/tls" 5 | "github.com/hoisie/web" 6 | ) 7 | 8 | // an arbitrary self-signed certificate, generated with 9 | // `openssl req -x509 -nodes -days 365 -newkey rsa:1024 -keyout cert.pem -out cert.pem` 10 | 11 | var pkey = `-----BEGIN RSA PRIVATE KEY----- 12 | MIICXAIBAAKBgQDVBUw40q0zpF/zWzwBf0GFkXmnkw+YCNTiV8l7mso1DCv/VTYM 13 | cqtvy0g2KNBV7SFLC+NHuxJkNOAtJ8Fxx1EpeIw5A3KeCRNb4lo6ecAkuDLiPYGO 14 | qgAqjj8QmhmZA68qTIuWGYM1FTtUK3wO4wrHnqHEjs3cWNghmby6AgLHVQIDAQAB 15 | AoGAcy5GJINlu4KpjwBJ1dVlLD+YtA9EY0SDN0+YVglARKasM4dzjg+CuxQDm6U9 16 | 4PgzBE0NO3/fVedxP3k7k7XeH73PosaxjWpfMawXR3wSLFKJBwxux/8gNdzeGRHN 17 | X1sYsJ70WiZLFOAPQ9jctF1ejUP6fpLHsti6ZHQj/R1xqBECQQDrHxmpMoviQL6n 18 | 4CBR4HvlIRtd4Qr21IGEXtbjIcC5sgbkfne6qhqdv9/zxsoiPTi0859cr704Mf3y 19 | cA8LZ8c3AkEA5+/KjSoqgzPaUnvPZ0p9TNx6odxMsd5h1AMIVIbZPT6t2vffCaZ7 20 | R0ffim/KeWfoav8u9Cyz8eJpBG6OHROT0wJBAML54GLCCuROAozePI8JVFS3NqWM 21 | OHZl1R27NAHYfKTBMBwNkCYYZ8gHVKUoZXktQbg1CyNmjMhsFIYWTTONFNMCQFsL 22 | eBld2f5S1nrWex3y0ajgS4tKLRkNUJ2m6xgzLwepmRmBf54MKgxbHFb9dx+dOFD4 23 | Bvh2q9RhqhPBSiwDyV0CQBxN3GPbaa8V7eeXBpBYO5Evy4VxSWJTpgmMDtMH+RUp 24 | 9eAJ8rUyhZ2OaElg1opGCRemX98s/o2R5JtzZvOx7so= 25 | -----END RSA PRIVATE KEY----- 26 | ` 27 | 28 | var cert = `-----BEGIN CERTIFICATE----- 29 | MIIDXDCCAsWgAwIBAgIJAJqbbWPZgt0sMA0GCSqGSIb3DQEBBQUAMH0xCzAJBgNV 30 | BAYTAlVTMQswCQYDVQQIEwJDQTEWMBQGA1UEBxMNU2FuIEZyYW5jaXNjbzEPMA0G 31 | A1UEChMGV2ViLmdvMRcwFQYDVQQDEw5NaWNoYWVsIEhvaXNpZTEfMB0GCSqGSIb3 32 | DQEJARYQaG9pc2llQGdtYWlsLmNvbTAeFw0xMzA0MDgxNjIzMDVaFw0xNDA0MDgx 33 | NjIzMDVaMH0xCzAJBgNVBAYTAlVTMQswCQYDVQQIEwJDQTEWMBQGA1UEBxMNU2Fu 34 | IEZyYW5jaXNjbzEPMA0GA1UEChMGV2ViLmdvMRcwFQYDVQQDEw5NaWNoYWVsIEhv 35 | aXNpZTEfMB0GCSqGSIb3DQEJARYQaG9pc2llQGdtYWlsLmNvbTCBnzANBgkqhkiG 36 | 9w0BAQEFAAOBjQAwgYkCgYEA1QVMONKtM6Rf81s8AX9BhZF5p5MPmAjU4lfJe5rK 37 | NQwr/1U2DHKrb8tINijQVe0hSwvjR7sSZDTgLSfBccdRKXiMOQNyngkTW+JaOnnA 38 | JLgy4j2BjqoAKo4/EJoZmQOvKkyLlhmDNRU7VCt8DuMKx56hxI7N3FjYIZm8ugIC 39 | x1UCAwEAAaOB4zCB4DAdBgNVHQ4EFgQURizcvrgUl8yhIEQvJT/1b5CzV8MwgbAG 40 | A1UdIwSBqDCBpYAURizcvrgUl8yhIEQvJT/1b5CzV8OhgYGkfzB9MQswCQYDVQQG 41 | EwJVUzELMAkGA1UECBMCQ0ExFjAUBgNVBAcTDVNhbiBGcmFuY2lzY28xDzANBgNV 42 | BAoTBldlYi5nbzEXMBUGA1UEAxMOTWljaGFlbCBIb2lzaWUxHzAdBgkqhkiG9w0B 43 | CQEWEGhvaXNpZUBnbWFpbC5jb22CCQCam21j2YLdLDAMBgNVHRMEBTADAQH/MA0G 44 | CSqGSIb3DQEBBQUAA4GBAGBPoVCReGMO1FrsIeVrPV/N6pSK7H3PLdxm7gmmvnO9 45 | K/LK0OKIT7UL3eus+eh0gt0/Tv/ksq4nSIzXBLPKyPggLmpC6Agf3ydNTpdLQ23J 46 | gWrxykqyLToIiAuL+pvC3Jv8IOPIiVFsY032rOqcwSGdVUyhTsG28+7KnR6744tM 47 | -----END CERTIFICATE----- 48 | ` 49 | 50 | func hello(val string) string { return "hello " + val + "\n" } 51 | 52 | func main() { 53 | config := tls.Config{ 54 | Time: nil, 55 | } 56 | 57 | config.Certificates = make([]tls.Certificate, 1) 58 | var err error 59 | config.Certificates[0], err = tls.X509KeyPair([]byte(cert), []byte(pkey)) 60 | if err != nil { 61 | println(err.Error()) 62 | return 63 | } 64 | 65 | // you must access the server with an HTTPS address, i.e https://localhost:9999/world 66 | web.Get("/(.*)", hello) 67 | web.RunTLS("0.0.0.0:9999", &config) 68 | } 69 | -------------------------------------------------------------------------------- /secure_cookie.go: -------------------------------------------------------------------------------- 1 | package web 2 | 3 | import ( 4 | "bytes" 5 | "crypto/aes" 6 | "crypto/cipher" 7 | "crypto/hmac" 8 | "crypto/rand" 9 | "crypto/sha512" 10 | "encoding/base64" 11 | "errors" 12 | "golang.org/x/crypto/pbkdf2" 13 | "io" 14 | "strings" 15 | ) 16 | 17 | const ( 18 | pbkdf2Iterations = 64000 19 | keySize = 32 20 | ) 21 | 22 | var ( 23 | ErrMissingCookieSecret = errors.New("Secret Key for secure cookies has not been set. Assign one to web.Config.CookieSecret.") 24 | ErrInvalidKey = errors.New("The keys for secure cookies have not been initialized. Ensure that a Run* method is being called") 25 | ) 26 | 27 | func (ctx *Context) SetSecureCookie(name string, val string, age int64) error { 28 | server := ctx.Server 29 | if len(server.Config.CookieSecret) == 0 { 30 | return ErrMissingCookieSecret 31 | } 32 | if len(server.encKey) == 0 || len(server.signKey) == 0 { 33 | return ErrInvalidKey 34 | } 35 | ciphertext, err := encrypt([]byte(val), server.encKey) 36 | if err != nil { 37 | return err 38 | } 39 | sig := sign(ciphertext, server.signKey) 40 | data := base64.StdEncoding.EncodeToString(ciphertext) + "|" + base64.StdEncoding.EncodeToString(sig) 41 | ctx.SetCookie(NewCookie(name, data, age)) 42 | return nil 43 | } 44 | 45 | func (ctx *Context) GetSecureCookie(name string) (string, bool) { 46 | for _, cookie := range ctx.Request.Cookies() { 47 | if cookie.Name != name { 48 | continue 49 | } 50 | parts := strings.SplitN(cookie.Value, "|", 2) 51 | if len(parts) != 2 { 52 | return "", false 53 | } 54 | ciphertext, err := base64.StdEncoding.DecodeString(parts[0]) 55 | if err != nil { 56 | return "", false 57 | } 58 | sig, err := base64.StdEncoding.DecodeString(parts[1]) 59 | if err != nil { 60 | return "", false 61 | } 62 | expectedSig := sign([]byte(ciphertext), ctx.Server.signKey) 63 | if !bytes.Equal(expectedSig, sig) { 64 | return "", false 65 | } 66 | plaintext, err := decrypt(ciphertext, ctx.Server.encKey) 67 | if err != nil { 68 | return "", false 69 | } 70 | return string(plaintext), true 71 | } 72 | return "", false 73 | } 74 | 75 | func genKey(password string, salt string) []byte { 76 | return pbkdf2.Key([]byte(password), []byte(salt), pbkdf2Iterations, keySize, sha512.New) 77 | } 78 | 79 | func encrypt(plaintext []byte, key []byte) ([]byte, error) { 80 | aesCipher, err := aes.NewCipher(key) 81 | if err != nil { 82 | return nil, err 83 | } 84 | ciphertext := make([]byte, aes.BlockSize+len(plaintext)) 85 | iv := ciphertext[:aes.BlockSize] 86 | if _, err := io.ReadFull(rand.Reader, iv); err != nil { 87 | return nil, err 88 | } 89 | stream := cipher.NewCTR(aesCipher, iv) 90 | stream.XORKeyStream(ciphertext[aes.BlockSize:], plaintext) 91 | return ciphertext, nil 92 | } 93 | 94 | func decrypt(ciphertext []byte, key []byte) ([]byte, error) { 95 | if len(ciphertext) <= aes.BlockSize { 96 | return nil, errors.New("Invalid cipher text") 97 | } 98 | aesCipher, err := aes.NewCipher(key) 99 | if err != nil { 100 | return nil, err 101 | } 102 | plaintext := make([]byte, len(ciphertext)-aes.BlockSize) 103 | stream := cipher.NewCTR(aesCipher, ciphertext[:aes.BlockSize]) 104 | stream.XORKeyStream(plaintext, ciphertext[aes.BlockSize:]) 105 | return plaintext, nil 106 | } 107 | 108 | func sign(data []byte, key []byte) []byte { 109 | mac := hmac.New(sha512.New, key) 110 | mac.Write(data) 111 | return mac.Sum(nil) 112 | } 113 | -------------------------------------------------------------------------------- /helpers.go: -------------------------------------------------------------------------------- 1 | package web 2 | 3 | import ( 4 | "bytes" 5 | "encoding/base64" 6 | "errors" 7 | "net/http" 8 | "net/url" 9 | "os" 10 | "regexp" 11 | "strings" 12 | "time" 13 | ) 14 | 15 | // internal utility methods 16 | func webTime(t time.Time) string { 17 | ftime := t.Format(time.RFC1123) 18 | if strings.HasSuffix(ftime, "UTC") { 19 | ftime = ftime[0:len(ftime)-3] + "GMT" 20 | } 21 | return ftime 22 | } 23 | 24 | func dirExists(dir string) bool { 25 | d, e := os.Stat(dir) 26 | switch { 27 | case e != nil: 28 | return false 29 | case !d.IsDir(): 30 | return false 31 | } 32 | 33 | return true 34 | } 35 | 36 | func fileExists(dir string) bool { 37 | info, err := os.Stat(dir) 38 | if err != nil { 39 | return false 40 | } 41 | 42 | return !info.IsDir() 43 | } 44 | 45 | // Urlencode is a helper method that converts a map into URL-encoded form data. 46 | // It is a useful when constructing HTTP POST requests. 47 | func Urlencode(data map[string]string) string { 48 | var buf bytes.Buffer 49 | for k, v := range data { 50 | buf.WriteString(url.QueryEscape(k)) 51 | buf.WriteByte('=') 52 | buf.WriteString(url.QueryEscape(v)) 53 | buf.WriteByte('&') 54 | } 55 | s := buf.String() 56 | return s[0 : len(s)-1] 57 | } 58 | 59 | var slugRegex = regexp.MustCompile(`(?i:[^a-z0-9\-_])`) 60 | 61 | // Slug is a helper function that returns the URL slug for string s. 62 | // It's used to return clean, URL-friendly strings that can be 63 | // used in routing. 64 | func Slug(s string, sep string) string { 65 | if s == "" { 66 | return "" 67 | } 68 | slug := slugRegex.ReplaceAllString(s, sep) 69 | if slug == "" { 70 | return "" 71 | } 72 | quoted := regexp.QuoteMeta(sep) 73 | sepRegex := regexp.MustCompile("(" + quoted + "){2,}") 74 | slug = sepRegex.ReplaceAllString(slug, sep) 75 | sepEnds := regexp.MustCompile("^" + quoted + "|" + quoted + "$") 76 | slug = sepEnds.ReplaceAllString(slug, "") 77 | return strings.ToLower(slug) 78 | } 79 | 80 | // NewCookie is a helper method that returns a new http.Cookie object. 81 | // Duration is specified in seconds. If the duration is zero, the cookie is permanent. 82 | // This can be used in conjunction with ctx.SetCookie. 83 | func NewCookie(name string, value string, age int64) *http.Cookie { 84 | var utctime time.Time 85 | if age == 0 { 86 | // 2^31 - 1 seconds (roughly 2038) 87 | utctime = time.Unix(2147483647, 0) 88 | } else { 89 | utctime = time.Unix(time.Now().Unix()+age, 0) 90 | } 91 | return &http.Cookie{Name: name, Value: value, Expires: utctime} 92 | } 93 | 94 | // GetBasicAuth returns the decoded user and password from the context's 95 | // 'Authorization' header. 96 | func (ctx *Context) GetBasicAuth() (string, string, error) { 97 | if len(ctx.Request.Header["Authorization"]) == 0 { 98 | return "", "", errors.New("No Authorization header provided") 99 | } 100 | authHeader := ctx.Request.Header["Authorization"][0] 101 | authString := strings.Split(string(authHeader), " ") 102 | if authString[0] != "Basic" { 103 | return "", "", errors.New("Not Basic Authentication") 104 | } 105 | decodedAuth, err := base64.StdEncoding.DecodeString(authString[1]) 106 | if err != nil { 107 | return "", "", err 108 | } 109 | authSlice := strings.Split(string(decodedAuth), ":") 110 | if len(authSlice) != 2 { 111 | return "", "", errors.New("Error delimiting authString into username/password. Malformed input: " + authString[1]) 112 | } 113 | return authSlice[0], authSlice[1], nil 114 | } 115 | -------------------------------------------------------------------------------- /scgi.go: -------------------------------------------------------------------------------- 1 | package web 2 | 3 | import ( 4 | "bufio" 5 | "bytes" 6 | "errors" 7 | "fmt" 8 | "io" 9 | "net" 10 | "net/http" 11 | "net/http/cgi" 12 | "strconv" 13 | "strings" 14 | ) 15 | 16 | type scgiBody struct { 17 | reader io.Reader 18 | conn io.ReadWriteCloser 19 | closed bool 20 | } 21 | 22 | func (b *scgiBody) Read(p []byte) (n int, err error) { 23 | if b.closed { 24 | return 0, errors.New("SCGI read after close") 25 | } 26 | return b.reader.Read(p) 27 | } 28 | 29 | func (b *scgiBody) Close() error { 30 | b.closed = true 31 | return b.conn.Close() 32 | } 33 | 34 | type scgiConn struct { 35 | fd io.ReadWriteCloser 36 | req *http.Request 37 | headers http.Header 38 | wroteHeaders bool 39 | } 40 | 41 | func (conn *scgiConn) WriteHeader(status int) { 42 | if !conn.wroteHeaders { 43 | conn.wroteHeaders = true 44 | 45 | var buf bytes.Buffer 46 | text := http.StatusText(status) 47 | 48 | fmt.Fprintf(&buf, "HTTP/1.1 %d %s\r\n", status, text) 49 | 50 | for k, v := range conn.headers { 51 | for _, i := range v { 52 | buf.WriteString(k + ": " + i + "\r\n") 53 | } 54 | } 55 | 56 | buf.WriteString("\r\n") 57 | conn.fd.Write(buf.Bytes()) 58 | } 59 | } 60 | 61 | func (conn *scgiConn) Header() http.Header { 62 | return conn.headers 63 | } 64 | 65 | func (conn *scgiConn) Write(data []byte) (n int, err error) { 66 | if !conn.wroteHeaders { 67 | conn.WriteHeader(200) 68 | } 69 | 70 | if conn.req.Method == "HEAD" { 71 | return 0, errors.New("Body Not Allowed") 72 | } 73 | 74 | return conn.fd.Write(data) 75 | } 76 | 77 | func (conn *scgiConn) Close() { conn.fd.Close() } 78 | 79 | func (conn *scgiConn) finishRequest() error { 80 | var buf bytes.Buffer 81 | if !conn.wroteHeaders { 82 | conn.wroteHeaders = true 83 | 84 | for k, v := range conn.headers { 85 | for _, i := range v { 86 | buf.WriteString(k + ": " + i + "\r\n") 87 | } 88 | } 89 | 90 | buf.WriteString("\r\n") 91 | conn.fd.Write(buf.Bytes()) 92 | } 93 | return nil 94 | } 95 | 96 | func (s *Server) readScgiRequest(fd io.ReadWriteCloser) (*http.Request, error) { 97 | reader := bufio.NewReader(fd) 98 | line, err := reader.ReadString(':') 99 | if err != nil { 100 | return nil, err 101 | } 102 | length, err := strconv.Atoi(line[0 : len(line)-1]) 103 | if err != nil { 104 | return nil, err 105 | } 106 | if length > 16384 { 107 | return nil, errors.New("Max header size is 16k") 108 | } 109 | headerData := make([]byte, length) 110 | _, err = reader.Read(headerData) 111 | if err != nil { 112 | return nil, err 113 | } 114 | b, err := reader.ReadByte() 115 | if err != nil { 116 | return nil, err 117 | } 118 | // discard the trailing comma 119 | if b != ',' { 120 | return nil, errors.New("SCGI protocol error: missing comma") 121 | } 122 | headerList := bytes.Split(headerData, []byte{0}) 123 | headers := map[string]string{} 124 | for i := 0; i < len(headerList)-1; i += 2 { 125 | headers[string(headerList[i])] = string(headerList[i+1]) 126 | } 127 | httpReq, err := cgi.RequestFromMap(headers) 128 | if err != nil { 129 | return nil, err 130 | } 131 | if httpReq.ContentLength > 0 { 132 | httpReq.Body = &scgiBody{ 133 | reader: io.LimitReader(reader, httpReq.ContentLength), 134 | conn: fd, 135 | } 136 | } else { 137 | httpReq.Body = &scgiBody{reader: reader, conn: fd} 138 | } 139 | return httpReq, nil 140 | } 141 | 142 | func (s *Server) handleScgiRequest(fd io.ReadWriteCloser) { 143 | defer fd.Close() 144 | req, err := s.readScgiRequest(fd) 145 | if err != nil { 146 | s.Logger.Println("Error reading SCGI request: %q", err.Error()) 147 | return 148 | } 149 | sc := scgiConn{fd, req, make(map[string][]string), false} 150 | s.routeHandler(req, &sc) 151 | sc.finishRequest() 152 | } 153 | 154 | func (s *Server) listenAndServeScgi(addr string) error { 155 | 156 | var l net.Listener 157 | var err error 158 | 159 | //if the path begins with a "/", assume it's a unix address 160 | if strings.HasPrefix(addr, "/") { 161 | l, err = net.Listen("unix", addr) 162 | } else { 163 | l, err = net.Listen("tcp", addr) 164 | } 165 | 166 | //save the listener so it can be closed 167 | s.l = l 168 | 169 | if err != nil { 170 | s.Logger.Println("SCGI listen error", err.Error()) 171 | return err 172 | } 173 | 174 | for { 175 | fd, err := l.Accept() 176 | if err != nil { 177 | s.Logger.Println("SCGI accept error", err.Error()) 178 | return err 179 | } 180 | go s.handleScgiRequest(fd) 181 | } 182 | return nil 183 | } 184 | -------------------------------------------------------------------------------- /web.go: -------------------------------------------------------------------------------- 1 | // Package web is a lightweight web framework for Go. It's ideal for 2 | // writing simple, performant backend web services. 3 | package web 4 | 5 | import ( 6 | "crypto/tls" 7 | "golang.org/x/net/websocket" 8 | "log" 9 | "mime" 10 | "net/http" 11 | "os" 12 | "path" 13 | "reflect" 14 | "strings" 15 | ) 16 | 17 | // A Context object is created for every incoming HTTP request, and is 18 | // passed to handlers as an optional first argument. It provides information 19 | // about the request, including the http.Request object, the GET and POST params, 20 | // and acts as a Writer for the response. 21 | type Context struct { 22 | Request *http.Request 23 | Params map[string]string 24 | Server *Server 25 | http.ResponseWriter 26 | } 27 | 28 | // WriteString writes string data into the response object. 29 | func (ctx *Context) WriteString(content string) { 30 | ctx.ResponseWriter.Write([]byte(content)) 31 | } 32 | 33 | // Abort is a helper method that sends an HTTP header and an optional 34 | // body. It is useful for returning 4xx or 5xx errors. 35 | // Once it has been called, any return value from the handler will 36 | // not be written to the response. 37 | func (ctx *Context) Abort(status int, body string) { 38 | ctx.SetHeader("Content-Type", "text/html; charset=utf-8", true) 39 | ctx.ResponseWriter.WriteHeader(status) 40 | ctx.ResponseWriter.Write([]byte(body)) 41 | } 42 | 43 | // Redirect is a helper method for 3xx redirects. 44 | func (ctx *Context) Redirect(status int, url_ string) { 45 | ctx.ResponseWriter.Header().Set("Location", url_) 46 | ctx.ResponseWriter.WriteHeader(status) 47 | ctx.ResponseWriter.Write([]byte("Redirecting to: " + url_)) 48 | } 49 | 50 | //BadRequest writes a 400 HTTP response 51 | func (ctx *Context) BadRequest() { 52 | ctx.ResponseWriter.WriteHeader(400) 53 | } 54 | 55 | // Notmodified writes a 304 HTTP response 56 | func (ctx *Context) NotModified() { 57 | ctx.ResponseWriter.WriteHeader(304) 58 | } 59 | 60 | //Unauthorized writes a 401 HTTP response 61 | func (ctx *Context) Unauthorized() { 62 | ctx.ResponseWriter.WriteHeader(401) 63 | } 64 | 65 | //Forbidden writes a 403 HTTP response 66 | func (ctx *Context) Forbidden() { 67 | ctx.ResponseWriter.WriteHeader(403) 68 | } 69 | 70 | // NotFound writes a 404 HTTP response 71 | func (ctx *Context) NotFound(message string) { 72 | ctx.ResponseWriter.WriteHeader(404) 73 | ctx.ResponseWriter.Write([]byte(message)) 74 | } 75 | 76 | // ContentType sets the Content-Type header for an HTTP response. 77 | // For example, ctx.ContentType("json") sets the content-type to "application/json" 78 | // If the supplied value contains a slash (/) it is set as the Content-Type 79 | // verbatim. The return value is the content type as it was 80 | // set, or an empty string if none was found. 81 | func (ctx *Context) ContentType(val string) string { 82 | var ctype string 83 | if strings.ContainsRune(val, '/') { 84 | ctype = val 85 | } else { 86 | if !strings.HasPrefix(val, ".") { 87 | val = "." + val 88 | } 89 | ctype = mime.TypeByExtension(val) 90 | } 91 | if ctype != "" { 92 | ctx.Header().Set("Content-Type", ctype) 93 | } 94 | return ctype 95 | } 96 | 97 | // SetHeader sets a response header. If `unique` is true, the current value 98 | // of that header will be overwritten . If false, it will be appended. 99 | func (ctx *Context) SetHeader(hdr string, val string, unique bool) { 100 | if unique { 101 | ctx.Header().Set(hdr, val) 102 | } else { 103 | ctx.Header().Add(hdr, val) 104 | } 105 | } 106 | 107 | // SetCookie adds a cookie header to the response. 108 | func (ctx *Context) SetCookie(cookie *http.Cookie) { 109 | ctx.SetHeader("Set-Cookie", cookie.String(), false) 110 | } 111 | 112 | // small optimization: cache the context type instead of repeteadly calling reflect.Typeof 113 | var contextType reflect.Type 114 | 115 | var defaultStaticDirs []string 116 | 117 | func init() { 118 | contextType = reflect.TypeOf(Context{}) 119 | //find the location of the exe file 120 | wd, _ := os.Getwd() 121 | arg0 := path.Clean(os.Args[0]) 122 | var exeFile string 123 | if strings.HasPrefix(arg0, "/") { 124 | exeFile = arg0 125 | } else { 126 | //TODO for robustness, search each directory in $PATH 127 | exeFile = path.Join(wd, arg0) 128 | } 129 | parent, _ := path.Split(exeFile) 130 | defaultStaticDirs = append(defaultStaticDirs, path.Join(parent, "static")) 131 | defaultStaticDirs = append(defaultStaticDirs, path.Join(wd, "static")) 132 | return 133 | } 134 | 135 | // Process invokes the main server's routing system. 136 | func Process(c http.ResponseWriter, req *http.Request) { 137 | mainServer.Process(c, req) 138 | } 139 | 140 | // Run starts the web application and serves HTTP requests for the main server. 141 | func Run(addr string) { 142 | mainServer.Run(addr) 143 | } 144 | 145 | // RunTLS starts the web application and serves HTTPS requests for the main server. 146 | func RunTLS(addr string, config *tls.Config) { 147 | mainServer.RunTLS(addr, config) 148 | } 149 | 150 | // RunScgi starts the web application and serves SCGI requests for the main server. 151 | func RunScgi(addr string) { 152 | mainServer.RunScgi(addr) 153 | } 154 | 155 | // RunFcgi starts the web application and serves FastCGI requests for the main server. 156 | func RunFcgi(addr string) { 157 | mainServer.RunFcgi(addr) 158 | } 159 | 160 | // Close stops the main server. 161 | func Close() { 162 | mainServer.Close() 163 | } 164 | 165 | // Get adds a handler for the 'GET' http method in the main server. 166 | func Get(route string, handler interface{}) { 167 | mainServer.Get(route, handler) 168 | } 169 | 170 | // Post adds a handler for the 'POST' http method in the main server. 171 | func Post(route string, handler interface{}) { 172 | mainServer.addRoute(route, "POST", handler) 173 | } 174 | 175 | // Put adds a handler for the 'PUT' http method in the main server. 176 | func Put(route string, handler interface{}) { 177 | mainServer.addRoute(route, "PUT", handler) 178 | } 179 | 180 | // Delete adds a handler for the 'DELETE' http method in the main server. 181 | func Delete(route string, handler interface{}) { 182 | mainServer.addRoute(route, "DELETE", handler) 183 | } 184 | 185 | // Match adds a handler for an arbitrary http method in the main server. 186 | func Match(method string, route string, handler interface{}) { 187 | mainServer.addRoute(route, method, handler) 188 | } 189 | 190 | // Add a custom http.Handler. Will have no effect when running as FCGI or SCGI. 191 | func Handle(route string, method string, httpHandler http.Handler) { 192 | mainServer.Handle(route, method, httpHandler) 193 | } 194 | 195 | //Adds a handler for websockets. Only for webserver mode. Will have no effect when running as FCGI or SCGI. 196 | func Websocket(route string, httpHandler websocket.Handler) { 197 | mainServer.Websocket(route, httpHandler) 198 | } 199 | 200 | // SetLogger sets the logger for the main server. 201 | func SetLogger(logger *log.Logger) { 202 | mainServer.Logger = logger 203 | } 204 | 205 | // Config is the configuration of the main server. 206 | var Config = &ServerConfig{ 207 | RecoverPanic: true, 208 | ColorOutput: true, 209 | } 210 | 211 | var mainServer = NewServer() 212 | -------------------------------------------------------------------------------- /server.go: -------------------------------------------------------------------------------- 1 | package web 2 | 3 | import ( 4 | "bytes" 5 | "crypto/tls" 6 | "fmt" 7 | "golang.org/x/net/websocket" 8 | "log" 9 | "net" 10 | "net/http" 11 | "net/http/pprof" 12 | "os" 13 | "path" 14 | "reflect" 15 | "regexp" 16 | "runtime" 17 | "strconv" 18 | "strings" 19 | "time" 20 | ) 21 | 22 | // ServerConfig is configuration for server objects. 23 | type ServerConfig struct { 24 | StaticDir string 25 | Addr string 26 | Port int 27 | CookieSecret string 28 | RecoverPanic bool 29 | Profiler bool 30 | ColorOutput bool 31 | } 32 | 33 | // Server represents a web.go server. 34 | type Server struct { 35 | Config *ServerConfig 36 | routes []route 37 | Logger *log.Logger 38 | Env map[string]interface{} 39 | //save the listener so it can be closed 40 | l net.Listener 41 | encKey []byte 42 | signKey []byte 43 | } 44 | 45 | func NewServer() *Server { 46 | return &Server{ 47 | Config: Config, 48 | Logger: log.New(os.Stdout, "", log.Ldate|log.Ltime), 49 | Env: map[string]interface{}{}, 50 | } 51 | } 52 | 53 | func (s *Server) initServer() { 54 | if s.Config == nil { 55 | s.Config = &ServerConfig{} 56 | } 57 | 58 | if s.Logger == nil { 59 | s.Logger = log.New(os.Stdout, "", log.Ldate|log.Ltime) 60 | } 61 | 62 | if len(s.Config.CookieSecret) > 0 { 63 | s.Logger.Println("Generating cookie encryption keys") 64 | s.encKey = genKey(s.Config.CookieSecret, "encryption key salt") 65 | s.signKey = genKey(s.Config.CookieSecret, "signature key salt") 66 | } 67 | } 68 | 69 | type route struct { 70 | r string 71 | cr *regexp.Regexp 72 | method string 73 | handler reflect.Value 74 | httpHandler http.Handler 75 | } 76 | 77 | func (s *Server) addRoute(r string, method string, handler interface{}) { 78 | cr, err := regexp.Compile(r) 79 | if err != nil { 80 | s.Logger.Printf("Error in route regex %q\n", r) 81 | return 82 | } 83 | 84 | switch handler.(type) { 85 | case http.Handler: 86 | s.routes = append(s.routes, route{r: r, cr: cr, method: method, httpHandler: handler.(http.Handler)}) 87 | case reflect.Value: 88 | fv := handler.(reflect.Value) 89 | s.routes = append(s.routes, route{r: r, cr: cr, method: method, handler: fv}) 90 | default: 91 | fv := reflect.ValueOf(handler) 92 | s.routes = append(s.routes, route{r: r, cr: cr, method: method, handler: fv}) 93 | } 94 | } 95 | 96 | // ServeHTTP is the interface method for Go's http server package 97 | func (s *Server) ServeHTTP(c http.ResponseWriter, req *http.Request) { 98 | s.Process(c, req) 99 | } 100 | 101 | // Process invokes the routing system for server s 102 | func (s *Server) Process(c http.ResponseWriter, req *http.Request) { 103 | route := s.routeHandler(req, c) 104 | if route != nil { 105 | route.httpHandler.ServeHTTP(c, req) 106 | } 107 | } 108 | 109 | // Get adds a handler for the 'GET' http method for server s. 110 | func (s *Server) Get(route string, handler interface{}) { 111 | s.addRoute(route, "GET", handler) 112 | } 113 | 114 | // Post adds a handler for the 'POST' http method for server s. 115 | func (s *Server) Post(route string, handler interface{}) { 116 | s.addRoute(route, "POST", handler) 117 | } 118 | 119 | // Put adds a handler for the 'PUT' http method for server s. 120 | func (s *Server) Put(route string, handler interface{}) { 121 | s.addRoute(route, "PUT", handler) 122 | } 123 | 124 | // Delete adds a handler for the 'DELETE' http method for server s. 125 | func (s *Server) Delete(route string, handler interface{}) { 126 | s.addRoute(route, "DELETE", handler) 127 | } 128 | 129 | // Match adds a handler for an arbitrary http method for server s. 130 | func (s *Server) Match(method string, route string, handler interface{}) { 131 | s.addRoute(route, method, handler) 132 | } 133 | 134 | // Add a custom http.Handler. Will have no effect when running as FCGI or SCGI. 135 | func (s *Server) Handle(route string, method string, httpHandler http.Handler) { 136 | s.addRoute(route, method, httpHandler) 137 | } 138 | 139 | //Adds a handler for websockets. Only for webserver mode. Will have no effect when running as FCGI or SCGI. 140 | func (s *Server) Websocket(route string, httpHandler websocket.Handler) { 141 | s.addRoute(route, "GET", httpHandler) 142 | } 143 | 144 | // Run starts the web application and serves HTTP requests for s 145 | func (s *Server) Run(addr string) { 146 | s.initServer() 147 | 148 | mux := http.NewServeMux() 149 | if s.Config.Profiler { 150 | mux.Handle("/debug/pprof/cmdline", http.HandlerFunc(pprof.Cmdline)) 151 | mux.Handle("/debug/pprof/profile", http.HandlerFunc(pprof.Profile)) 152 | mux.Handle("/debug/pprof/heap", pprof.Handler("heap")) 153 | mux.Handle("/debug/pprof/symbol", http.HandlerFunc(pprof.Symbol)) 154 | } 155 | mux.Handle("/", s) 156 | 157 | l, err := net.Listen("tcp", addr) 158 | if err != nil { 159 | log.Fatal("ListenAndServe:", err) 160 | } 161 | 162 | s.Logger.Printf("web.go serving %s\n", l.Addr()) 163 | 164 | s.l = l 165 | err = http.Serve(s.l, mux) 166 | s.l.Close() 167 | } 168 | 169 | // RunFcgi starts the web application and serves FastCGI requests for s. 170 | func (s *Server) RunFcgi(addr string) { 171 | s.initServer() 172 | s.Logger.Printf("web.go serving fcgi %s\n", addr) 173 | s.listenAndServeFcgi(addr) 174 | } 175 | 176 | // RunScgi starts the web application and serves SCGI requests for s. 177 | func (s *Server) RunScgi(addr string) { 178 | s.initServer() 179 | s.Logger.Printf("web.go serving scgi %s\n", addr) 180 | s.listenAndServeScgi(addr) 181 | } 182 | 183 | // RunTLS starts the web application and serves HTTPS requests for s. 184 | func (s *Server) RunTLS(addr string, config *tls.Config) error { 185 | s.initServer() 186 | mux := http.NewServeMux() 187 | mux.Handle("/", s) 188 | 189 | l, err := tls.Listen("tcp", addr, config) 190 | if err != nil { 191 | log.Fatal("Listen:", err) 192 | return err 193 | } 194 | s.Logger.Printf("web.go serving %s\n", l.Addr()) 195 | 196 | s.l = l 197 | return http.Serve(s.l, mux) 198 | } 199 | 200 | // Close stops server s. 201 | func (s *Server) Close() { 202 | if s.l != nil { 203 | s.l.Close() 204 | } 205 | } 206 | 207 | // safelyCall invokes `function` in recover block 208 | func (s *Server) safelyCall(function reflect.Value, args []reflect.Value) (resp []reflect.Value, e interface{}) { 209 | defer func() { 210 | if err := recover(); err != nil { 211 | if !s.Config.RecoverPanic { 212 | // go back to panic 213 | panic(err) 214 | } else { 215 | e = err 216 | resp = nil 217 | s.Logger.Println("Handler crashed with error", err) 218 | for i := 1; ; i += 1 { 219 | _, file, line, ok := runtime.Caller(i) 220 | if !ok { 221 | break 222 | } 223 | s.Logger.Println(file, line) 224 | } 225 | } 226 | } 227 | }() 228 | return function.Call(args), nil 229 | } 230 | 231 | // requiresContext determines whether 'handlerType' contains 232 | // an argument to 'web.Ctx' as its first argument 233 | func requiresContext(handlerType reflect.Type) bool { 234 | //if the method doesn't take arguments, no 235 | if handlerType.NumIn() == 0 { 236 | return false 237 | } 238 | 239 | //if the first argument is not a pointer, no 240 | a0 := handlerType.In(0) 241 | if a0.Kind() != reflect.Ptr { 242 | return false 243 | } 244 | //if the first argument is a context, yes 245 | if a0.Elem() == contextType { 246 | return true 247 | } 248 | 249 | return false 250 | } 251 | 252 | // tryServingFile attempts to serve a static file, and returns 253 | // whether or not the operation is successful. 254 | // It checks the following directories for the file, in order: 255 | // 1) Config.StaticDir 256 | // 2) The 'static' directory in the parent directory of the executable. 257 | // 3) The 'static' directory in the current working directory 258 | func (s *Server) tryServingFile(name string, req *http.Request, w http.ResponseWriter) bool { 259 | //try to serve a static file 260 | if s.Config.StaticDir != "" { 261 | staticFile := path.Join(s.Config.StaticDir, name) 262 | if fileExists(staticFile) { 263 | http.ServeFile(w, req, staticFile) 264 | return true 265 | } 266 | } else { 267 | for _, staticDir := range defaultStaticDirs { 268 | staticFile := path.Join(staticDir, name) 269 | if fileExists(staticFile) { 270 | http.ServeFile(w, req, staticFile) 271 | return true 272 | } 273 | } 274 | } 275 | return false 276 | } 277 | 278 | func (s *Server) logRequest(ctx Context, sTime time.Time) { 279 | //log the request 280 | req := ctx.Request 281 | requestPath := req.URL.Path 282 | 283 | duration := time.Now().Sub(sTime) 284 | var client string 285 | 286 | // We suppose RemoteAddr is of the form Ip:Port as specified in the Request 287 | // documentation at http://golang.org/pkg/net/http/#Request 288 | pos := strings.LastIndex(req.RemoteAddr, ":") 289 | if pos > 0 { 290 | client = req.RemoteAddr[0:pos] 291 | } else { 292 | client = req.RemoteAddr 293 | } 294 | 295 | var logEntry bytes.Buffer 296 | logEntry.WriteString(client) 297 | logEntry.WriteString(" - " + s.ttyGreen(req.Method+" "+requestPath)) 298 | logEntry.WriteString(" - " + duration.String()) 299 | if len(ctx.Params) > 0 { 300 | logEntry.WriteString(" - " + s.ttyWhite(fmt.Sprintf("Params: %v\n", ctx.Params))) 301 | } 302 | ctx.Server.Logger.Print(logEntry.String()) 303 | } 304 | 305 | func (s *Server) ttyGreen(msg string) string { 306 | return s.ttyColor(msg, ttyCodes.green) 307 | } 308 | 309 | func (s *Server) ttyWhite(msg string) string { 310 | return s.ttyColor(msg, ttyCodes.white) 311 | } 312 | 313 | func (s *Server) ttyColor(msg string, colorCode string) string { 314 | if s.Config.ColorOutput { 315 | return colorCode + msg + ttyCodes.reset 316 | } else { 317 | return msg 318 | } 319 | } 320 | 321 | // the main route handler in web.go 322 | // Tries to handle the given request. 323 | // Finds the route matching the request, and execute the callback associated 324 | // with it. In case of custom http handlers, this function returns an "unused" 325 | // route. The caller is then responsible for calling the httpHandler associated 326 | // with the returned route. 327 | func (s *Server) routeHandler(req *http.Request, w http.ResponseWriter) (unused *route) { 328 | requestPath := req.URL.Path 329 | ctx := Context{req, map[string]string{}, s, w} 330 | 331 | //set some default headers 332 | ctx.SetHeader("Server", "web.go", true) 333 | tm := time.Now().UTC() 334 | 335 | //ignore errors from ParseForm because it's usually harmless. 336 | req.ParseForm() 337 | if len(req.Form) > 0 { 338 | for k, v := range req.Form { 339 | ctx.Params[k] = v[0] 340 | } 341 | } 342 | 343 | defer s.logRequest(ctx, tm) 344 | 345 | ctx.SetHeader("Date", webTime(tm), true) 346 | 347 | if req.Method == "GET" || req.Method == "HEAD" { 348 | if s.tryServingFile(requestPath, req, w) { 349 | return 350 | } 351 | } 352 | 353 | for i := 0; i < len(s.routes); i++ { 354 | route := s.routes[i] 355 | cr := route.cr 356 | //if the methods don't match, skip this handler (except HEAD can be used in place of GET) 357 | if req.Method != route.method && !(req.Method == "HEAD" && route.method == "GET") { 358 | continue 359 | } 360 | 361 | if !cr.MatchString(requestPath) { 362 | continue 363 | } 364 | match := cr.FindStringSubmatch(requestPath) 365 | 366 | if len(match[0]) != len(requestPath) { 367 | continue 368 | } 369 | 370 | if route.httpHandler != nil { 371 | unused = &route 372 | // We can not handle custom http handlers here, give back to the caller. 373 | return 374 | } 375 | 376 | // set the default content-type 377 | ctx.SetHeader("Content-Type", "text/html; charset=utf-8", true) 378 | 379 | var args []reflect.Value 380 | handlerType := route.handler.Type() 381 | if requiresContext(handlerType) { 382 | args = append(args, reflect.ValueOf(&ctx)) 383 | } 384 | for _, arg := range match[1:] { 385 | args = append(args, reflect.ValueOf(arg)) 386 | } 387 | 388 | ret, err := s.safelyCall(route.handler, args) 389 | if err != nil { 390 | //there was an error or panic while calling the handler 391 | ctx.Abort(500, "Server Error") 392 | } 393 | if len(ret) == 0 { 394 | return 395 | } 396 | 397 | sval := ret[0] 398 | 399 | var content []byte 400 | 401 | if sval.Kind() == reflect.String { 402 | content = []byte(sval.String()) 403 | } else if sval.Kind() == reflect.Slice && sval.Type().Elem().Kind() == reflect.Uint8 { 404 | content = sval.Interface().([]byte) 405 | } 406 | ctx.SetHeader("Content-Length", strconv.Itoa(len(content)), true) 407 | _, err = ctx.ResponseWriter.Write(content) 408 | if err != nil { 409 | ctx.Server.Logger.Println("Error during write: ", err) 410 | } 411 | return 412 | } 413 | 414 | // try serving index.html or index.htm 415 | if req.Method == "GET" || req.Method == "HEAD" { 416 | if s.tryServingFile(path.Join(requestPath, "index.html"), req, w) { 417 | return 418 | } else if s.tryServingFile(path.Join(requestPath, "index.htm"), req, w) { 419 | return 420 | } 421 | } 422 | ctx.Abort(404, "Page not found") 423 | return 424 | } 425 | 426 | // SetLogger sets the logger for server s 427 | func (s *Server) SetLogger(logger *log.Logger) { 428 | s.Logger = logger 429 | } 430 | -------------------------------------------------------------------------------- /web_test.go: -------------------------------------------------------------------------------- 1 | package web 2 | 3 | import ( 4 | "bytes" 5 | "encoding/base64" 6 | "encoding/json" 7 | "errors" 8 | "fmt" 9 | "io" 10 | "io/ioutil" 11 | "log" 12 | "net/http" 13 | "net/url" 14 | "runtime" 15 | "strconv" 16 | "strings" 17 | "testing" 18 | ) 19 | 20 | func init() { 21 | runtime.GOMAXPROCS(runtime.NumCPU()) 22 | } 23 | 24 | // ioBuffer is a helper that implements io.ReadWriteCloser, 25 | // which is helpful in imitating a net.Conn 26 | type ioBuffer struct { 27 | input *bytes.Buffer 28 | output *bytes.Buffer 29 | closed bool 30 | } 31 | 32 | func (buf *ioBuffer) Write(p []uint8) (n int, err error) { 33 | if buf.closed { 34 | return 0, errors.New("Write after Close on ioBuffer") 35 | } 36 | return buf.output.Write(p) 37 | } 38 | 39 | func (buf *ioBuffer) Read(p []byte) (n int, err error) { 40 | if buf.closed { 41 | return 0, errors.New("Read after Close on ioBuffer") 42 | } 43 | return buf.input.Read(p) 44 | } 45 | 46 | //noop 47 | func (buf *ioBuffer) Close() error { 48 | buf.closed = true 49 | return nil 50 | } 51 | 52 | type testResponse struct { 53 | statusCode int 54 | status string 55 | body string 56 | headers map[string][]string 57 | cookies map[string]string 58 | } 59 | 60 | func buildTestResponse(buf *bytes.Buffer) *testResponse { 61 | 62 | response := testResponse{headers: make(map[string][]string), cookies: make(map[string]string)} 63 | s := buf.String() 64 | contents := strings.SplitN(s, "\r\n\r\n", 2) 65 | 66 | header := contents[0] 67 | 68 | if len(contents) > 1 { 69 | response.body = contents[1] 70 | } 71 | 72 | headers := strings.Split(header, "\r\n") 73 | 74 | statusParts := strings.SplitN(headers[0], " ", 3) 75 | response.statusCode, _ = strconv.Atoi(statusParts[1]) 76 | 77 | for _, h := range headers[1:] { 78 | split := strings.SplitN(h, ":", 2) 79 | name := strings.TrimSpace(split[0]) 80 | value := strings.TrimSpace(split[1]) 81 | if _, ok := response.headers[name]; !ok { 82 | response.headers[name] = []string{} 83 | } 84 | 85 | newheaders := make([]string, len(response.headers[name])+1) 86 | copy(newheaders, response.headers[name]) 87 | newheaders[len(newheaders)-1] = value 88 | response.headers[name] = newheaders 89 | 90 | //if the header is a cookie, set it 91 | if name == "Set-Cookie" { 92 | i := strings.Index(value, ";") 93 | cookie := value[0:i] 94 | cookieParts := strings.SplitN(cookie, "=", 2) 95 | response.cookies[strings.TrimSpace(cookieParts[0])] = strings.TrimSpace(cookieParts[1]) 96 | } 97 | } 98 | 99 | return &response 100 | } 101 | 102 | func getTestResponse(method string, path string, body string, headers map[string][]string, cookies []*http.Cookie) *testResponse { 103 | req := buildTestRequest(method, path, body, headers, cookies) 104 | var buf bytes.Buffer 105 | 106 | tcpb := ioBuffer{input: nil, output: &buf} 107 | c := scgiConn{wroteHeaders: false, req: req, headers: make(map[string][]string), fd: &tcpb} 108 | mainServer.Process(&c, req) 109 | return buildTestResponse(&buf) 110 | } 111 | 112 | func testGet(path string, headers map[string]string) *testResponse { 113 | var header http.Header 114 | for k, v := range headers { 115 | header.Set(k, v) 116 | } 117 | return getTestResponse("GET", path, "", header, nil) 118 | } 119 | 120 | type Test struct { 121 | method string 122 | path string 123 | headers map[string][]string 124 | body string 125 | expectedStatus int 126 | expectedBody string 127 | } 128 | 129 | //initialize the routes 130 | func init() { 131 | mainServer.SetLogger(log.New(ioutil.Discard, "", 0)) 132 | Get("/", func() string { return "index" }) 133 | Get("/panic", func() { panic(0) }) 134 | Get("/echo/(.*)", func(s string) string { return s }) 135 | Get("/multiecho/(.*)/(.*)/(.*)/(.*)", func(a, b, c, d string) string { return a + b + c + d }) 136 | Post("/post/echo/(.*)", func(s string) string { return s }) 137 | Post("/post/echoparam/(.*)", func(ctx *Context, name string) string { return ctx.Params[name] }) 138 | 139 | Get("/error/code/(.*)", func(ctx *Context, code string) string { 140 | n, _ := strconv.Atoi(code) 141 | message := http.StatusText(n) 142 | ctx.Abort(n, message) 143 | return "" 144 | }) 145 | 146 | Get("/error/notfound/(.*)", func(ctx *Context, message string) { ctx.NotFound(message) }) 147 | 148 | Get("/error/badrequest", func(ctx *Context) { ctx.BadRequest() }) 149 | Post("/error/badrequest", func(ctx *Context) { ctx.BadRequest() }) 150 | 151 | Get("/error/unauthorized", func(ctx *Context) { ctx.Unauthorized() }) 152 | Post("/error/unauthorized", func(ctx *Context) { ctx.Unauthorized() }) 153 | 154 | Get("/error/forbidden", func(ctx *Context) { ctx.Forbidden() }) 155 | Post("/error/forbidden", func(ctx *Context) { ctx.Forbidden() }) 156 | 157 | Post("/posterror/code/(.*)/(.*)", func(ctx *Context, code string, message string) string { 158 | n, _ := strconv.Atoi(code) 159 | ctx.Abort(n, message) 160 | return "" 161 | }) 162 | 163 | Get("/writetest", func(ctx *Context) { ctx.WriteString("hello") }) 164 | 165 | Post("/securecookie/set/(.+)/(.+)", func(ctx *Context, name string, val string) string { 166 | ctx.SetSecureCookie(name, val, 60) 167 | return "" 168 | }) 169 | 170 | Get("/securecookie/get/(.+)", func(ctx *Context, name string) string { 171 | val, ok := ctx.GetSecureCookie(name) 172 | if !ok { 173 | return "" 174 | } 175 | return val 176 | }) 177 | Get("/getparam", func(ctx *Context) string { return ctx.Params["a"] }) 178 | Get("/fullparams", func(ctx *Context) string { 179 | return strings.Join(ctx.Request.Form["a"], ",") 180 | }) 181 | 182 | Get("/json", func(ctx *Context) string { 183 | ctx.ContentType("json") 184 | data, _ := json.Marshal(ctx.Params) 185 | return string(data) 186 | }) 187 | 188 | Get("/jsonbytes", func(ctx *Context) []byte { 189 | ctx.ContentType("json") 190 | data, _ := json.Marshal(ctx.Params) 191 | return data 192 | }) 193 | 194 | Post("/parsejson", func(ctx *Context) string { 195 | var tmp = struct { 196 | A string 197 | B string 198 | }{} 199 | json.NewDecoder(ctx.Request.Body).Decode(&tmp) 200 | return tmp.A + " " + tmp.B 201 | }) 202 | 203 | Match("OPTIONS", "/options", func(ctx *Context) { 204 | ctx.SetHeader("Access-Control-Allow-Methods", "POST, GET, OPTIONS", true) 205 | ctx.SetHeader("Access-Control-Max-Age", "1000", true) 206 | ctx.WriteHeader(200) 207 | }) 208 | 209 | Get("/dupeheader", func(ctx *Context) string { 210 | ctx.SetHeader("Server", "myserver", true) 211 | return "" 212 | }) 213 | 214 | Get("/authorization", func(ctx *Context) string { 215 | user, pass, err := ctx.GetBasicAuth() 216 | if err != nil { 217 | return "fail" 218 | } 219 | return user + pass 220 | }) 221 | } 222 | 223 | var tests = []Test{ 224 | {"GET", "/", nil, "", 200, "index"}, 225 | {"GET", "/echo/hello", nil, "", 200, "hello"}, 226 | {"GET", "/echo/hello", nil, "", 200, "hello"}, 227 | {"GET", "/multiecho/a/b/c/d", nil, "", 200, "abcd"}, 228 | {"POST", "/post/echo/hello", nil, "", 200, "hello"}, 229 | {"POST", "/post/echo/hello", nil, "", 200, "hello"}, 230 | {"POST", "/post/echoparam/a", map[string][]string{"Content-Type": {"application/x-www-form-urlencoded"}}, "a=hello", 200, "hello"}, 231 | {"POST", "/post/echoparam/c?c=hello", nil, "", 200, "hello"}, 232 | {"POST", "/post/echoparam/a", map[string][]string{"Content-Type": {"application/x-www-form-urlencoded"}}, "a=hello\x00", 200, "hello\x00"}, 233 | //long url 234 | {"GET", "/echo/" + strings.Repeat("0123456789", 100), nil, "", 200, strings.Repeat("0123456789", 100)}, 235 | {"GET", "/writetest", nil, "", 200, "hello"}, 236 | {"GET", "/error/badrequest", nil, "", 400, ""}, 237 | {"POST", "/error/badrequest", nil, "", 400, ""}, 238 | {"GET", "/error/unauthorized", nil, "", 401, ""}, 239 | {"POST", "/error/unauthorized", nil, "", 401, ""}, 240 | {"GET", "/error/forbidden", nil, "", 403, ""}, 241 | {"POST", "/error/forbidden", nil, "", 403, ""}, 242 | {"GET", "/error/notfound/notfound", nil, "", 404, "notfound"}, 243 | {"GET", "/doesnotexist", nil, "", 404, "Page not found"}, 244 | {"POST", "/doesnotexist", nil, "", 404, "Page not found"}, 245 | {"GET", "/error/code/500", nil, "", 500, http.StatusText(500)}, 246 | {"POST", "/posterror/code/410/failedrequest", nil, "", 410, "failedrequest"}, 247 | {"GET", "/getparam?a=abcd", nil, "", 200, "abcd"}, 248 | {"GET", "/getparam?b=abcd", nil, "", 200, ""}, 249 | {"GET", "/fullparams?a=1&a=2&a=3", nil, "", 200, "1,2,3"}, 250 | {"GET", "/panic", nil, "", 500, "Server Error"}, 251 | {"GET", "/json?a=1&b=2", nil, "", 200, `{"a":"1","b":"2"}`}, 252 | {"GET", "/jsonbytes?a=1&b=2", nil, "", 200, `{"a":"1","b":"2"}`}, 253 | {"POST", "/parsejson", map[string][]string{"Content-Type": {"application/json"}}, `{"a":"hello", "b":"world"}`, 200, "hello world"}, 254 | //{"GET", "/testenv", "", 200, "hello world"}, 255 | {"GET", "/authorization", map[string][]string{"Authorization": {BuildBasicAuthCredentials("foo", "bar")}}, "", 200, "foobar"}, 256 | {"GET", "/authorization", nil, "", 200, "fail"}, 257 | } 258 | 259 | func buildTestRequest(method string, path string, body string, headers map[string][]string, cookies []*http.Cookie) *http.Request { 260 | host := "127.0.0.1" 261 | port := "80" 262 | rawurl := "http://" + host + ":" + port + path 263 | url_, _ := url.Parse(rawurl) 264 | proto := "HTTP/1.1" 265 | 266 | if headers == nil { 267 | headers = map[string][]string{} 268 | } 269 | 270 | headers["User-Agent"] = []string{"web.go test"} 271 | if method == "POST" { 272 | headers["Content-Length"] = []string{fmt.Sprintf("%d", len(body))} 273 | if headers["Content-Type"] == nil { 274 | headers["Content-Type"] = []string{"text/plain"} 275 | } 276 | } 277 | 278 | req := http.Request{Method: method, 279 | URL: url_, 280 | Proto: proto, 281 | Host: host, 282 | Header: http.Header(headers), 283 | Body: ioutil.NopCloser(bytes.NewBufferString(body)), 284 | } 285 | 286 | for _, cookie := range cookies { 287 | req.AddCookie(cookie) 288 | } 289 | return &req 290 | } 291 | 292 | func TestRouting(t *testing.T) { 293 | for _, test := range tests { 294 | resp := getTestResponse(test.method, test.path, test.body, test.headers, nil) 295 | 296 | if resp.statusCode != test.expectedStatus { 297 | t.Fatalf("%v(%v) expected status %d got %d", test.method, test.path, test.expectedStatus, resp.statusCode) 298 | } 299 | if resp.body != test.expectedBody { 300 | t.Fatalf("%v(%v) expected %q got %q", test.method, test.path, test.expectedBody, resp.body) 301 | } 302 | if cl, ok := resp.headers["Content-Length"]; ok { 303 | clExp, _ := strconv.Atoi(cl[0]) 304 | clAct := len(resp.body) 305 | if clExp != clAct { 306 | t.Fatalf("Content-length doesn't match. expected %d got %d", clExp, clAct) 307 | } 308 | } 309 | } 310 | } 311 | 312 | func TestHead(t *testing.T) { 313 | for _, test := range tests { 314 | 315 | if test.method != "GET" { 316 | continue 317 | } 318 | getresp := getTestResponse("GET", test.path, test.body, test.headers, nil) 319 | headresp := getTestResponse("HEAD", test.path, test.body, test.headers, nil) 320 | 321 | if getresp.statusCode != headresp.statusCode { 322 | t.Fatalf("head and get status differ. expected %d got %d", getresp.statusCode, headresp.statusCode) 323 | } 324 | if len(headresp.body) != 0 { 325 | t.Fatalf("head request arrived with a body") 326 | } 327 | 328 | var cl []string 329 | var getcl, headcl int 330 | var hascl1, hascl2 bool 331 | 332 | if cl, hascl1 = getresp.headers["Content-Length"]; hascl1 { 333 | getcl, _ = strconv.Atoi(cl[0]) 334 | } 335 | 336 | if cl, hascl2 = headresp.headers["Content-Length"]; hascl2 { 337 | headcl, _ = strconv.Atoi(cl[0]) 338 | } 339 | 340 | if hascl1 != hascl2 { 341 | t.Fatalf("head and get: one has content-length, one doesn't") 342 | } 343 | 344 | if hascl1 == true && getcl != headcl { 345 | t.Fatalf("head and get content-length differ") 346 | } 347 | } 348 | } 349 | 350 | func buildTestScgiRequest(method string, path string, body string, headers map[string][]string) *bytes.Buffer { 351 | var headerBuf bytes.Buffer 352 | scgiHeaders := make(map[string]string) 353 | 354 | headerBuf.WriteString("CONTENT_LENGTH") 355 | headerBuf.WriteByte(0) 356 | headerBuf.WriteString(fmt.Sprintf("%d", len(body))) 357 | headerBuf.WriteByte(0) 358 | 359 | scgiHeaders["REQUEST_METHOD"] = method 360 | scgiHeaders["HTTP_HOST"] = "127.0.0.1" 361 | scgiHeaders["REQUEST_URI"] = path 362 | scgiHeaders["SERVER_PORT"] = "80" 363 | scgiHeaders["SERVER_PROTOCOL"] = "HTTP/1.1" 364 | scgiHeaders["USER_AGENT"] = "web.go test framework" 365 | 366 | for k, v := range headers { 367 | //Skip content-length 368 | if k == "Content-Length" { 369 | continue 370 | } 371 | key := "HTTP_" + strings.ToUpper(strings.Replace(k, "-", "_", -1)) 372 | scgiHeaders[key] = v[0] 373 | } 374 | for k, v := range scgiHeaders { 375 | headerBuf.WriteString(k) 376 | headerBuf.WriteByte(0) 377 | headerBuf.WriteString(v) 378 | headerBuf.WriteByte(0) 379 | } 380 | headerData := headerBuf.Bytes() 381 | 382 | var buf bytes.Buffer 383 | //extra 1 is for the comma at the end 384 | dlen := len(headerData) 385 | fmt.Fprintf(&buf, "%d:", dlen) 386 | buf.Write(headerData) 387 | buf.WriteByte(',') 388 | buf.WriteString(body) 389 | return &buf 390 | } 391 | 392 | func TestScgi(t *testing.T) { 393 | for _, test := range tests { 394 | req := buildTestScgiRequest(test.method, test.path, test.body, test.headers) 395 | var output bytes.Buffer 396 | nb := ioBuffer{input: req, output: &output} 397 | mainServer.handleScgiRequest(&nb) 398 | resp := buildTestResponse(&output) 399 | 400 | if resp.statusCode != test.expectedStatus { 401 | t.Fatalf("expected status %d got %d", test.expectedStatus, resp.statusCode) 402 | } 403 | 404 | if resp.body != test.expectedBody { 405 | t.Fatalf("Scgi expected %q got %q", test.expectedBody, resp.body) 406 | } 407 | } 408 | } 409 | 410 | func TestScgiHead(t *testing.T) { 411 | for _, test := range tests { 412 | 413 | if test.method != "GET" { 414 | continue 415 | } 416 | 417 | req := buildTestScgiRequest("GET", test.path, test.body, make(map[string][]string)) 418 | var output bytes.Buffer 419 | nb := ioBuffer{input: req, output: &output} 420 | mainServer.handleScgiRequest(&nb) 421 | getresp := buildTestResponse(&output) 422 | 423 | req = buildTestScgiRequest("HEAD", test.path, test.body, make(map[string][]string)) 424 | var output2 bytes.Buffer 425 | nb = ioBuffer{input: req, output: &output2} 426 | mainServer.handleScgiRequest(&nb) 427 | headresp := buildTestResponse(&output2) 428 | 429 | if getresp.statusCode != headresp.statusCode { 430 | t.Fatalf("head and get status differ. expected %d got %d", getresp.statusCode, headresp.statusCode) 431 | } 432 | if len(headresp.body) != 0 { 433 | t.Fatalf("head request arrived with a body") 434 | } 435 | 436 | var cl []string 437 | var getcl, headcl int 438 | var hascl1, hascl2 bool 439 | 440 | if cl, hascl1 = getresp.headers["Content-Length"]; hascl1 { 441 | getcl, _ = strconv.Atoi(cl[0]) 442 | } 443 | 444 | if cl, hascl2 = headresp.headers["Content-Length"]; hascl2 { 445 | headcl, _ = strconv.Atoi(cl[0]) 446 | } 447 | 448 | if hascl1 != hascl2 { 449 | t.Fatalf("head and get: one has content-length, one doesn't") 450 | } 451 | 452 | if hascl1 == true && getcl != headcl { 453 | t.Fatalf("head and get content-length differ") 454 | } 455 | } 456 | } 457 | 458 | func TestReadScgiRequest(t *testing.T) { 459 | headers := map[string][]string{"User-Agent": {"web.go"}} 460 | req := buildTestScgiRequest("POST", "/hello", "Hello world!", headers) 461 | var s Server 462 | httpReq, err := s.readScgiRequest(&ioBuffer{input: req, output: nil}) 463 | if err != nil { 464 | t.Fatalf("Error while reading SCGI request: ", err.Error()) 465 | } 466 | if httpReq.ContentLength != 12 { 467 | t.Fatalf("Content length mismatch, expected %d, got %d ", 12, httpReq.ContentLength) 468 | } 469 | var body bytes.Buffer 470 | io.Copy(&body, httpReq.Body) 471 | if body.String() != "Hello world!" { 472 | t.Fatalf("Body mismatch, expected %q, got %q ", "Hello world!", body.String()) 473 | } 474 | } 475 | 476 | func makeCookie(vals map[string]string) []*http.Cookie { 477 | var cookies []*http.Cookie 478 | for k, v := range vals { 479 | c := &http.Cookie{ 480 | Name: k, 481 | Value: v, 482 | } 483 | cookies = append(cookies, c) 484 | } 485 | return cookies 486 | } 487 | 488 | func TestSecureCookie(t *testing.T) { 489 | mainServer.Config.CookieSecret = "7C19QRmwf3mHZ9CPAaPQ0hsWeufKd" 490 | mainServer.initServer() 491 | resp1 := getTestResponse("POST", "/securecookie/set/a/1", "", nil, nil) 492 | sval, ok := resp1.cookies["a"] 493 | if !ok { 494 | t.Fatalf("Failed to get cookie ") 495 | } 496 | cookies := makeCookie(map[string]string{"a": sval}) 497 | 498 | resp2 := getTestResponse("GET", "/securecookie/get/a", "", nil, cookies) 499 | 500 | if resp2.body != "1" { 501 | t.Fatalf("SecureCookie test failed") 502 | } 503 | } 504 | 505 | func TestEmptySecureCookie(t *testing.T) { 506 | mainServer.Config.CookieSecret = "7C19QRmwf3mHZ9CPAaPQ0hsWeufKd" 507 | cookies := makeCookie(map[string]string{"empty": ""}) 508 | 509 | resp2 := getTestResponse("GET", "/securecookie/get/empty", "", nil, cookies) 510 | 511 | if resp2.body != "" { 512 | t.Fatalf("Expected an empty secure cookie") 513 | } 514 | } 515 | 516 | func TestEarlyClose(t *testing.T) { 517 | var server1 Server 518 | server1.Close() 519 | } 520 | 521 | func TestOptions(t *testing.T) { 522 | resp := getTestResponse("OPTIONS", "/options", "", nil, nil) 523 | if resp.headers["Access-Control-Allow-Methods"][0] != "POST, GET, OPTIONS" { 524 | t.Fatalf("TestOptions - Access-Control-Allow-Methods failed") 525 | } 526 | if resp.headers["Access-Control-Max-Age"][0] != "1000" { 527 | t.Fatalf("TestOptions - Access-Control-Max-Age failed") 528 | } 529 | } 530 | 531 | func TestSlug(t *testing.T) { 532 | tests := [][]string{ 533 | {"", ""}, 534 | {"a", "a"}, 535 | {"a/b", "a-b"}, 536 | {"a b", "a-b"}, 537 | {"a////b", "a-b"}, 538 | {" a////b ", "a-b"}, 539 | {" Manowar / Friends ", "manowar-friends"}, 540 | } 541 | 542 | for _, test := range tests { 543 | v := Slug(test[0], "-") 544 | if v != test[1] { 545 | t.Fatalf("TestSlug(%v) failed, expected %v, got %v", test[0], test[1], v) 546 | } 547 | } 548 | } 549 | 550 | // tests that we don't duplicate headers 551 | func TestDuplicateHeader(t *testing.T) { 552 | resp := testGet("/dupeheader", nil) 553 | if len(resp.headers["Server"]) > 1 { 554 | t.Fatalf("Expected only one header, got %#v", resp.headers["Server"]) 555 | } 556 | if resp.headers["Server"][0] != "myserver" { 557 | t.Fatalf("Incorrect header, exp 'myserver', got %q", resp.headers["Server"][0]) 558 | } 559 | } 560 | 561 | // test that output contains ASCII color codes by default 562 | func TestColorOutputDefault(t *testing.T) { 563 | s := NewServer() 564 | var logOutput bytes.Buffer 565 | logger := log.New(&logOutput, "", 0) 566 | s.Logger = logger 567 | s.Get("/test", func() string { 568 | return "test" 569 | }) 570 | req := buildTestRequest("GET", "/test", "", nil, nil) 571 | var buf bytes.Buffer 572 | iob := ioBuffer{input: nil, output: &buf} 573 | c := scgiConn{wroteHeaders: false, req: req, headers: make(map[string][]string), fd: &iob} 574 | s.Process(&c, req) 575 | if !strings.Contains(logOutput.String(), "\x1b") { 576 | t.Fatalf("The default log output does not seem to be colored") 577 | } 578 | } 579 | 580 | // test that output contains ASCII color codes by default 581 | func TestNoColorOutput(t *testing.T) { 582 | s := NewServer() 583 | s.Config.ColorOutput = false 584 | var logOutput bytes.Buffer 585 | logger := log.New(&logOutput, "", 0) 586 | s.Logger = logger 587 | s.Get("/test", func() string { 588 | return "test" 589 | }) 590 | req := buildTestRequest("GET", "/test", "", nil, nil) 591 | var buf bytes.Buffer 592 | iob := ioBuffer{input: nil, output: &buf} 593 | c := scgiConn{wroteHeaders: false, req: req, headers: make(map[string][]string), fd: &iob} 594 | s.Process(&c, req) 595 | if strings.Contains(logOutput.String(), "\x1b") { 596 | t.Fatalf("The log contains color escape codes") 597 | } 598 | } 599 | 600 | // a malformed SCGI request should be discarded and not cause a panic 601 | func TestMaformedScgiRequest(t *testing.T) { 602 | var headerBuf bytes.Buffer 603 | 604 | headerBuf.WriteString("CONTENT_LENGTH") 605 | headerBuf.WriteByte(0) 606 | headerBuf.WriteString("0") 607 | headerBuf.WriteByte(0) 608 | headerData := headerBuf.Bytes() 609 | 610 | var buf bytes.Buffer 611 | fmt.Fprintf(&buf, "%d:", len(headerData)) 612 | buf.Write(headerData) 613 | buf.WriteByte(',') 614 | 615 | var output bytes.Buffer 616 | nb := ioBuffer{input: &buf, output: &output} 617 | mainServer.handleScgiRequest(&nb) 618 | if !nb.closed { 619 | t.Fatalf("The connection should have been closed") 620 | } 621 | } 622 | 623 | type TestHandler struct{} 624 | 625 | func (t *TestHandler) ServeHTTP(c http.ResponseWriter, req *http.Request) { 626 | } 627 | 628 | // When a custom HTTP handler is used, the Content-Type header should not be set to a default. 629 | // Go's FileHandler does not replace the Content-Type header if it is already set. 630 | func TestCustomHandlerContentType(t *testing.T) { 631 | s := NewServer() 632 | s.SetLogger(log.New(ioutil.Discard, "", 0)) 633 | s.Handle("/testHandler", "GET", &TestHandler{}) 634 | req := buildTestRequest("GET", "/testHandler", "", nil, nil) 635 | c := scgiConn{wroteHeaders: false, req: req, headers: make(map[string][]string), fd: nil} 636 | s.Process(&c, req) 637 | if c.headers["Content-Type"] != nil { 638 | t.Fatalf("A default Content-Type should not be present when using a custom HTTP handler") 639 | } 640 | } 641 | 642 | func BuildBasicAuthCredentials(user string, pass string) string { 643 | s := user + ":" + pass 644 | return "Basic " + base64.StdEncoding.EncodeToString([]byte(s)) 645 | } 646 | 647 | func BenchmarkProcessGet(b *testing.B) { 648 | s := NewServer() 649 | s.SetLogger(log.New(ioutil.Discard, "", 0)) 650 | s.Get("/echo/(.*)", func(s string) string { 651 | return s 652 | }) 653 | req := buildTestRequest("GET", "/echo/hi", "", nil, nil) 654 | var buf bytes.Buffer 655 | iob := ioBuffer{input: nil, output: &buf} 656 | c := scgiConn{wroteHeaders: false, req: req, headers: make(map[string][]string), fd: &iob} 657 | b.ReportAllocs() 658 | b.ResetTimer() 659 | for i := 0; i < b.N; i++ { 660 | s.Process(&c, req) 661 | } 662 | } 663 | 664 | func BenchmarkProcessPost(b *testing.B) { 665 | s := NewServer() 666 | s.SetLogger(log.New(ioutil.Discard, "", 0)) 667 | s.Post("/echo/(.*)", func(s string) string { 668 | return s 669 | }) 670 | req := buildTestRequest("POST", "/echo/hi", "", nil, nil) 671 | var buf bytes.Buffer 672 | iob := ioBuffer{input: nil, output: &buf} 673 | c := scgiConn{wroteHeaders: false, req: req, headers: make(map[string][]string), fd: &iob} 674 | b.ReportAllocs() 675 | b.ResetTimer() 676 | for i := 0; i < b.N; i++ { 677 | s.Process(&c, req) 678 | } 679 | } 680 | --------------------------------------------------------------------------------