Merge branch 'develop'

This commit is contained in:
Jimmy Zelinskie 2015-02-08 02:29:18 -05:00
commit 5ee355ed77
30 changed files with 474 additions and 72 deletions

15
Godeps/Godeps.json generated
View file

@ -4,27 +4,27 @@
"Deps": [
{
"ImportPath": "github.com/chihaya/bencode",
"Rev": "1df51811f3440202aa39df93596654319ea5ff57"
"Rev": "e60878f635e1a61315c413492e133dd39769b1d1"
},
{
"ImportPath": "github.com/golang/glog",
"Rev": "d1c4472bf2efd3826f2b5bdcc02d8416798d678c"
"Rev": "44145f04b68cf362d9c4df2182967c2275eaefed"
},
{
"ImportPath": "github.com/julienschmidt/httprouter",
"Rev": "46807412fe50aaceb73bb57061c2230fd26a1640"
"Rev": "00ce1c6a267162792c367acc43b1681a884e1872"
},
{
"ImportPath": "github.com/pushrax/faststats",
"Rev": "4c069408629f728eb3fe856f0966508492393a90"
"Rev": "0fc2c5e41a187240ffaa09320eea7df9f8071388"
},
{
"ImportPath": "github.com/pushrax/flatjson",
"Rev": "cce9ff039f0bec46d67ed9ab4ce846b8355c94bb"
"Rev": "86044f1c998d49053e13293029414ddb63f3a422"
},
{
"ImportPath": "github.com/stretchr/graceful",
"Rev": "0b67b304c1354aa3d1b2c4353a630ee0f589c34f"
"Rev": "8e780ba3fe3d3e7ab15fc52e3d60a996587181dc"
},
{
"ImportPath": "github.com/stretchr/pat/stop",
@ -32,8 +32,7 @@
},
{
"ImportPath": "golang.org/x/net/netutil",
"Comment": "null-204",
"Rev": "8fd8d3a0313cb59e495106ac76df5da29381fa24"
"Rev": "c84eff7014eba178f68bd4c05b86780efe0fbf35"
}
]
}

View file

@ -1,6 +1,6 @@
bencode is released under a BSD 2-Clause license, reproduced below.
Copyright (c) 2014, The Chihaya Authors
Copyright (c) 2015, The Chihaya Authors
All rights reserved.
Redistribution and use in source and binary forms, with or without

View file

@ -1,4 +1,4 @@
// Copyright 2014 The Chihaya Authors. All rights reserved.
// Copyright 2015 The Chihaya Authors. All rights reserved.
// Use of this source code is governed by the BSD 2-Clause license,
// which can be found in the LICENSE file.

View file

@ -1,4 +1,4 @@
// Copyright 2014 The Chihaya Authors. All rights reserved.
// Copyright 2015 The Chihaya Authors. All rights reserved.
// Use of this source code is governed by the BSD 2-Clause license,
// which can be found in the LICENSE file.

View file

@ -1,4 +1,4 @@
// Copyright 2014 The Chihaya Authors. All rights reserved.
// Copyright 2015 The Chihaya Authors. All rights reserved.
// Use of this source code is governed by the BSD 2-Clause license,
// which can be found in the LICENSE file.

View file

@ -1,4 +1,4 @@
// Copyright 2014 The Chihaya Authors. All rights reserved.
// Copyright 2015 The Chihaya Authors. All rights reserved.
// Use of this source code is governed by the BSD 2-Clause license,
// which can be found in the LICENSE file.

View file

@ -1,4 +1,4 @@
// Copyright 2014 The Chihaya Authors. All rights reserved.
// Copyright 2015 The Chihaya Authors. All rights reserved.
// Use of this source code is governed by the BSD 2-Clause license,
// which can be found in the LICENSE file.

View file

@ -77,6 +77,7 @@ import (
"flag"
"fmt"
"io"
stdLog "log"
"os"
"path/filepath"
"runtime"
@ -93,6 +94,9 @@ import (
// the corresponding constants in C++.
type severity int32 // sync/atomic int32
// These constants identify the log levels in order of increasing severity.
// A message written to a high-severity log file is also written to each
// lower-severity log file.
const (
infoLog severity = iota
warningLog
@ -311,7 +315,7 @@ func (m *moduleSpec) Set(value string) error {
// isLiteral reports whether the pattern is a literal string, that is, has no metacharacters
// that require filepath.Match to be called to match the pattern.
func isLiteral(pattern string) bool {
return !strings.ContainsAny(pattern, `*?[]\`)
return !strings.ContainsAny(pattern, `\*?[]`)
}
// traceLocation represents the setting of the -log_backtrace_at flag.
@ -466,7 +470,7 @@ func (l *loggingT) setVState(verbosity Level, filter []modulePat, setFilter bool
// Turn verbosity off so V will not fire while we are in transition.
logging.verbosity.set(0)
// Ditto for filter length.
logging.filterLength = 0
atomic.StoreInt32(&logging.filterLength, 0)
// Set the new filters and wipe the pc->Level map if the filter has changed.
if setFilter {
@ -513,7 +517,8 @@ var timeNow = time.Now // Stubbed out for testing.
/*
header formats a log header as defined by the C++ implementation.
It returns a buffer containing the formatted header.
It returns a buffer containing the formatted header and the user's file and line number.
The depth specifies how many stack frames above lives the source line to be identified in the log message.
Log lines have this form:
Lmmdd hh:mm:ss.uuuuuu threadid file:line] msg...
@ -527,10 +532,8 @@ where the fields are defined as follows:
line The line number
msg The user-supplied message
*/
func (l *loggingT) header(s severity) *buffer {
// Lmmdd hh:mm:ss.uuuuuu threadid file:line]
now := timeNow()
_, file, line, ok := runtime.Caller(3) // It's always the same number of frames to the user's call.
func (l *loggingT) header(s severity, depth int) (*buffer, string, int) {
_, file, line, ok := runtime.Caller(3 + depth)
if !ok {
file = "???"
line = 1
@ -540,6 +543,12 @@ func (l *loggingT) header(s severity) *buffer {
file = file[slash+1:]
}
}
return l.formatHeader(s, file, line), file, line
}
// formatHeader formats a log header using the provided file name and line number.
func (l *loggingT) formatHeader(s severity, file string, line int) *buffer {
now := timeNow()
if line < 0 {
line = 0 // not a real line number, but acceptable to someDigits
}
@ -552,6 +561,7 @@ func (l *loggingT) header(s severity) *buffer {
// It's worth about 3X. Fprintf is hard.
_, month, day := now.Date()
hour, minute, second := now.Clock()
// Lmmdd hh:mm:ss.uuuuuu threadid file:line]
buf.tmp[0] = severityChar[s]
buf.twoDigits(1, int(month))
buf.twoDigits(3, day)
@ -562,11 +572,11 @@ func (l *loggingT) header(s severity) *buffer {
buf.tmp[11] = ':'
buf.twoDigits(12, second)
buf.tmp[14] = '.'
buf.nDigits(6, 15, now.Nanosecond()/1000)
buf.nDigits(6, 15, now.Nanosecond()/1000, '0')
buf.tmp[21] = ' '
buf.nDigits(5, 22, pid) // TODO: should be TID
buf.tmp[27] = ' '
buf.Write(buf.tmp[:28])
buf.nDigits(7, 22, pid, ' ') // TODO: should be TID
buf.tmp[29] = ' '
buf.Write(buf.tmp[:30])
buf.WriteString(file)
buf.tmp[0] = ':'
n := buf.someDigits(1, line)
@ -587,12 +597,18 @@ func (buf *buffer) twoDigits(i, d int) {
buf.tmp[i] = digits[d%10]
}
// nDigits formats a zero-prefixed n-digit integer at buf.tmp[i].
func (buf *buffer) nDigits(n, i, d int) {
for j := n - 1; j >= 0; j-- {
// nDigits formats an n-digit integer at buf.tmp[i],
// padding with pad on the left.
// It assumes d >= 0.
func (buf *buffer) nDigits(n, i, d int, pad byte) {
j := n - 1
for ; j >= 0 && d > 0; j-- {
buf.tmp[i+j] = digits[d%10]
d /= 10
}
for ; j >= 0; j-- {
buf.tmp[i+j] = pad
}
}
// someDigits formats a zero-prefixed variable-width integer at buf.tmp[i].
@ -612,35 +628,50 @@ func (buf *buffer) someDigits(i, d int) int {
}
func (l *loggingT) println(s severity, args ...interface{}) {
buf := l.header(s)
buf, file, line := l.header(s, 0)
fmt.Fprintln(buf, args...)
l.output(s, buf)
l.output(s, buf, file, line, false)
}
func (l *loggingT) print(s severity, args ...interface{}) {
buf := l.header(s)
l.printDepth(s, 1, args...)
}
func (l *loggingT) printDepth(s severity, depth int, args ...interface{}) {
buf, file, line := l.header(s, depth)
fmt.Fprint(buf, args...)
if buf.Bytes()[buf.Len()-1] != '\n' {
buf.WriteByte('\n')
}
l.output(s, buf)
l.output(s, buf, file, line, false)
}
func (l *loggingT) printf(s severity, format string, args ...interface{}) {
buf := l.header(s)
buf, file, line := l.header(s, 0)
fmt.Fprintf(buf, format, args...)
if buf.Bytes()[buf.Len()-1] != '\n' {
buf.WriteByte('\n')
}
l.output(s, buf)
l.output(s, buf, file, line, false)
}
// printWithFileLine behaves like print but uses the provided file and line number. If
// alsoLogToStderr is true, the log message always appears on standard error; it
// will also appear in the log file unless --logtostderr is set.
func (l *loggingT) printWithFileLine(s severity, file string, line int, alsoToStderr bool, args ...interface{}) {
buf := l.formatHeader(s, file, line)
fmt.Fprint(buf, args...)
if buf.Bytes()[buf.Len()-1] != '\n' {
buf.WriteByte('\n')
}
l.output(s, buf, file, line, alsoToStderr)
}
// output writes the data to the log files and releases the buffer.
func (l *loggingT) output(s severity, buf *buffer) {
func (l *loggingT) output(s severity, buf *buffer, file string, line int, alsoToStderr bool) {
l.mu.Lock()
if l.traceLocation.isSet() {
_, file, line, ok := runtime.Caller(3) // It's always the same number of frames to the user's call (same as header).
if ok && l.traceLocation.match(file, line) {
if l.traceLocation.match(file, line) {
buf.Write(stacks(false))
}
}
@ -648,7 +679,7 @@ func (l *loggingT) output(s severity, buf *buffer) {
if l.toStderr {
os.Stderr.Write(data)
} else {
if l.alsoToStderr || s >= l.stderrThreshold.get() {
if alsoToStderr || l.alsoToStderr || s >= l.stderrThreshold.get() {
os.Stderr.Write(data)
}
if l.file[s] == nil {
@ -672,7 +703,16 @@ func (l *loggingT) output(s severity, buf *buffer) {
}
}
if s == fatalLog {
// Make sure we see the trace for the current goroutine on standard error.
// If we got here via Exit rather than Fatal, print no stacks.
if atomic.LoadUint32(&fatalNoStacks) > 0 {
l.mu.Unlock()
timeoutFlush(10 * time.Second)
os.Exit(1)
}
// Dump all goroutine stacks before exiting.
// First, make sure we see the trace for the current goroutine on standard error.
// If -logtostderr has been specified, the loop below will do that anyway
// as the first stack in the full dump.
if !l.toStderr {
os.Stderr.Write(stacks(false))
}
@ -861,6 +901,54 @@ func (l *loggingT) flushAll() {
}
}
// CopyStandardLogTo arranges for messages written to the Go "log" package's
// default logs to also appear in the Google logs for the named and lower
// severities. Subsequent changes to the standard log's default output location
// or format may break this behavior.
//
// Valid names are "INFO", "WARNING", "ERROR", and "FATAL". If the name is not
// recognized, CopyStandardLogTo panics.
func CopyStandardLogTo(name string) {
sev, ok := severityByName(name)
if !ok {
panic(fmt.Sprintf("log.CopyStandardLogTo(%q): unrecognized severity name", name))
}
// Set a log format that captures the user's file and line:
// d.go:23: message
stdLog.SetFlags(stdLog.Lshortfile)
stdLog.SetOutput(logBridge(sev))
}
// logBridge provides the Write method that enables CopyStandardLogTo to connect
// Go's standard logs to the logs provided by this package.
type logBridge severity
// Write parses the standard logging line and passes its components to the
// logger for severity(lb).
func (lb logBridge) Write(b []byte) (n int, err error) {
var (
file = "???"
line = 1
text string
)
// Split "d.go:23: message" into "d.go", "23", and "message".
if parts := bytes.SplitN(b, []byte{':'}, 3); len(parts) != 3 || len(parts[0]) < 1 || len(parts[2]) < 1 {
text = fmt.Sprintf("bad log format: %s", b)
} else {
file = string(parts[0])
text = string(parts[2][1:]) // skip leading space
line, err = strconv.Atoi(string(parts[1]))
if err != nil {
text = fmt.Sprintf("bad line number: %s", b)
line = 1
}
}
// printWithFileLine with alsoToStderr=true, so standard log messages
// always appear on standard error.
logging.printWithFileLine(severity(lb), file, line, true, text)
return len(b), nil
}
// setV computes and remembers the V level for a given PC
// when vmodule is enabled.
// File pattern matching takes the basename of the file, stripped
@ -964,6 +1052,12 @@ func Info(args ...interface{}) {
logging.print(infoLog, args...)
}
// InfoDepth acts as Info but uses depth to determine which call frame to log.
// InfoDepth(0, "msg") is the same as Info("msg").
func InfoDepth(depth int, args ...interface{}) {
logging.printDepth(infoLog, depth, args...)
}
// Infoln logs to the INFO log.
// Arguments are handled in the manner of fmt.Println; a newline is appended if missing.
func Infoln(args ...interface{}) {
@ -982,6 +1076,12 @@ func Warning(args ...interface{}) {
logging.print(warningLog, args...)
}
// WarningDepth acts as Warning but uses depth to determine which call frame to log.
// WarningDepth(0, "msg") is the same as Warning("msg").
func WarningDepth(depth int, args ...interface{}) {
logging.printDepth(warningLog, depth, args...)
}
// Warningln logs to the WARNING and INFO logs.
// Arguments are handled in the manner of fmt.Println; a newline is appended if missing.
func Warningln(args ...interface{}) {
@ -1000,6 +1100,12 @@ func Error(args ...interface{}) {
logging.print(errorLog, args...)
}
// ErrorDepth acts as Error but uses depth to determine which call frame to log.
// ErrorDepth(0, "msg") is the same as Error("msg").
func ErrorDepth(depth int, args ...interface{}) {
logging.printDepth(errorLog, depth, args...)
}
// Errorln logs to the ERROR, WARNING, and INFO logs.
// Arguments are handled in the manner of fmt.Println; a newline is appended if missing.
func Errorln(args ...interface{}) {
@ -1019,6 +1125,12 @@ func Fatal(args ...interface{}) {
logging.print(fatalLog, args...)
}
// FatalDepth acts as Fatal but uses depth to determine which call frame to log.
// FatalDepth(0, "msg") is the same as Fatal("msg").
func FatalDepth(depth int, args ...interface{}) {
logging.printDepth(fatalLog, depth, args...)
}
// Fatalln logs to the FATAL, ERROR, WARNING, and INFO logs,
// including a stack trace of all running goroutines, then calls os.Exit(255).
// Arguments are handled in the manner of fmt.Println; a newline is appended if missing.
@ -1032,3 +1144,34 @@ func Fatalln(args ...interface{}) {
func Fatalf(format string, args ...interface{}) {
logging.printf(fatalLog, format, args...)
}
// fatalNoStacks is non-zero if we are to exit without dumping goroutine stacks.
// It allows Exit and relatives to use the Fatal logs.
var fatalNoStacks uint32
// Exit logs to the FATAL, ERROR, WARNING, and INFO logs, then calls os.Exit(1).
// Arguments are handled in the manner of fmt.Print; a newline is appended if missing.
func Exit(args ...interface{}) {
atomic.StoreUint32(&fatalNoStacks, 1)
logging.print(fatalLog, args...)
}
// ExitDepth acts as Exit but uses depth to determine which call frame to log.
// ExitDepth(0, "msg") is the same as Exit("msg").
func ExitDepth(depth int, args ...interface{}) {
atomic.StoreUint32(&fatalNoStacks, 1)
logging.printDepth(fatalLog, depth, args...)
}
// Exitln logs to the FATAL, ERROR, WARNING, and INFO logs, then calls os.Exit(1).
func Exitln(args ...interface{}) {
atomic.StoreUint32(&fatalNoStacks, 1)
logging.println(fatalLog, args...)
}
// Exitf logs to the FATAL, ERROR, WARNING, and INFO logs, then calls os.Exit(1).
// Arguments are handled in the manner of fmt.Printf; a newline is appended if missing.
func Exitf(format string, args ...interface{}) {
atomic.StoreUint32(&fatalNoStacks, 1)
logging.printf(fatalLog, format, args...)
}

View file

@ -19,8 +19,10 @@ package glog
import (
"bytes"
"fmt"
stdLog "log"
"path/filepath"
"runtime"
"strconv"
"strings"
"testing"
"time"
@ -96,20 +98,99 @@ func TestInfo(t *testing.T) {
}
}
func TestInfoDepth(t *testing.T) {
setFlags()
defer logging.swap(logging.newBuffers())
f := func() { InfoDepth(1, "depth-test1") }
// The next three lines must stay together
_, _, wantLine, _ := runtime.Caller(0)
InfoDepth(0, "depth-test0")
f()
msgs := strings.Split(strings.TrimSuffix(contents(infoLog), "\n"), "\n")
if len(msgs) != 2 {
t.Fatalf("Got %d lines, expected 2", len(msgs))
}
for i, m := range msgs {
if !strings.HasPrefix(m, "I") {
t.Errorf("InfoDepth[%d] has wrong character: %q", i, m)
}
w := fmt.Sprintf("depth-test%d", i)
if !strings.Contains(m, w) {
t.Errorf("InfoDepth[%d] missing %q: %q", i, w, m)
}
// pull out the line number (between : and ])
msg := m[strings.LastIndex(m, ":")+1:]
x := strings.Index(msg, "]")
if x < 0 {
t.Errorf("InfoDepth[%d]: missing ']': %q", i, m)
continue
}
line, err := strconv.Atoi(msg[:x])
if err != nil {
t.Errorf("InfoDepth[%d]: bad line number: %q", i, m)
continue
}
wantLine++
if wantLine != line {
t.Errorf("InfoDepth[%d]: got line %d, want %d", i, line, wantLine)
}
}
}
func init() {
CopyStandardLogTo("INFO")
}
// Test that CopyStandardLogTo panics on bad input.
func TestCopyStandardLogToPanic(t *testing.T) {
defer func() {
if s, ok := recover().(string); !ok || !strings.Contains(s, "LOG") {
t.Errorf(`CopyStandardLogTo("LOG") should have panicked: %v`, s)
}
}()
CopyStandardLogTo("LOG")
}
// Test that using the standard log package logs to INFO.
func TestStandardLog(t *testing.T) {
setFlags()
defer logging.swap(logging.newBuffers())
stdLog.Print("test")
if !contains(infoLog, "I", t) {
t.Errorf("Info has wrong character: %q", contents(infoLog))
}
if !contains(infoLog, "test", t) {
t.Error("Info failed")
}
}
// Test that the header has the correct format.
func TestHeader(t *testing.T) {
setFlags()
defer logging.swap(logging.newBuffers())
defer func(previous func() time.Time) { timeNow = previous }(timeNow)
timeNow = func() time.Time {
return time.Date(2006, 1, 2, 15, 4, 5, .678901e9, time.Local)
return time.Date(2006, 1, 2, 15, 4, 5, .067890e9, time.Local)
}
pid = 1234
Info("test")
var line, pid int
n, err := fmt.Sscanf(contents(infoLog), "I0102 15:04:05.678901 %d glog_test.go:%d] test\n", &pid, &line)
if n != 2 || err != nil {
var line int
format := "I0102 15:04:05.067890 1234 glog_test.go:%d] test\n"
n, err := fmt.Sscanf(contents(infoLog), format, &line)
if n != 1 || err != nil {
t.Errorf("log format error: %d elements, error %s:\n%s", n, err, contents(infoLog))
}
// Scanf treats multiple spaces as equivalent to a single space,
// so check for correct space-padding also.
want := fmt.Sprintf(format, line)
if contents(infoLog) != want {
t.Errorf("log format error: got:\n\t%q\nwant:\t%q", contents(infoLog), want)
}
}
// Test that an Error log goes to Warning and Info.
@ -328,6 +409,7 @@ func TestLogBacktraceAt(t *testing.T) {
func BenchmarkHeader(b *testing.B) {
for i := 0; i < b.N; i++ {
logging.putBuffer(logging.header(infoLog))
buf, _, _ := logging.header(infoLog, 0)
logging.putBuffer(buf)
}
}

View file

@ -3,4 +3,5 @@ go:
- 1.1
- 1.2
- 1.3
- 1.4
- tip

View file

@ -189,8 +189,9 @@ for example the [Gorilla handlers](http://www.gorillatoolkit.org/pkg/handlers).
Or you could [just write your own](http://justinas.org/writing-http-middleware-in-go/),
it's very easy!
Alternatively, you could try [a framework building upon HttpRouter](#web-frameworks-building-upon-httprouter).
Alternatively, you could try [a framework building upon HttpRouter](#web-frameworks--co-based-on-httprouter).
### Multi-domain / Sub-domains
Here is a quick example: Does your server serve multiple domains / hosts?
You want to use sub-domains?
Define a router per host!
@ -228,7 +229,85 @@ func main() {
}
```
## Web Frameworks building upon HttpRouter
### Basic Authentication
Another quick example: Basic Authentification (RFC 2617) for handles:
```go
package main
import (
"bytes"
"encoding/base64"
"fmt"
"github.com/julienschmidt/httprouter"
"net/http"
"log"
"strings"
)
func BasicAuth(h httprouter.Handle, user, pass []byte) httprouter.Handle {
return func(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
const basicAuthPrefix string = "Basic "
// Get the Basic Authentication credentials
auth := r.Header.Get("Authorization")
if strings.HasPrefix(auth, basicAuthPrefix) {
// Check credentials
payload, err := base64.StdEncoding.DecodeString(auth[len(basicAuthPrefix):])
if err == nil {
pair := bytes.SplitN(payload, []byte(":"), 2)
if len(pair) == 2 && bytes.Equal(pair[0], user) && bytes.Equal(pair[1], pass) {
// Delegate request to the given handle
h(w, r, ps)
return
}
}
}
// Request Basic Authentication otherwise
w.Header().Set("WWW-Authenticate", "Basic realm=Restricted")
http.Error(w, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)
}
}
func Index(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
fmt.Fprint(w, "Not protected!\n")
}
func Protected(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
fmt.Fprint(w, "Protected!\n")
}
func main() {
user := []byte("gordon")
pass := []byte("secret!")
router := httprouter.New()
router.GET("/", Index)
router.GET("/protected/", BasicAuth(Protected, user, pass))
log.Fatal(http.ListenAndServe(":8080", router))
}
```
## Chaining with the NotFound handler
**NOTE: It might be required to set [Router.HandleMethodNotAllowed](http://godoc.org/github.com/julienschmidt/httprouter#Router.HandleMethodNotAllowed) to `false` to avoid problems.**
You can use another [http.HandlerFunc](http://golang.org/pkg/net/http/#HandlerFunc), for example another router, to handle requests which could not be matched by this router by using the [Router.NotFound](http://godoc.org/github.com/julienschmidt/httprouter#Router.NotFound) handler. This allows chaining.
### Static files
The `NotFound` handler can for example be used to serve static files from the root path `/` (like an index.html file along with other assets):
```go
// Serve static files from the ./public directory
router.NotFound = http.FileServer(http.Dir("public")).ServeHTTP
```
But this approach sidesteps the strict core rules of this router to avoid routing problems. A cleaner approach is to use a distinct sub-path for serving files, like `/static/*filepath` or `/files/*filepath`.
## Web Frameworks & Co based on HttpRouter
If the HttpRouter is a bit too minimalistic for you, you might try one of the following more high-level 3rd-party web frameworks building upon the HttpRouter package:
* [Gin](https://github.com/gin-gonic/gin): Features a martini-like API with much better performance
* [Hikaru](https://github.com/najeira/hikaru): Supports standalone and Google AppEngine
* [Hitch](https://github.com/nbio/hitch): Hitch ties httprouter, [httpcontext](https://github.com/nbio/httpcontext), and middleware up in a bow
* [Neko](https://github.com/rocwong/neko): A lightweight web application framework for Golang

View file

@ -123,13 +123,21 @@ type Router struct {
// handle is registered for it.
// First superfluous path elements like ../ or // are removed.
// Afterwards the router does a case-insensitive lookup of the cleaned path.
// If a handle can be found for this route, the router makes a redirection
// If a handle can be found for this route, the router makes a redirection
// to the corrected path with status code 301 for GET requests and 307 for
// all other request methods.
// For example /FOO and /..//Foo could be redirected to /foo.
// RedirectTrailingSlash is independent of this option.
RedirectFixedPath bool
// If enabled, the router checks if another method is allowed for the
// current route, if the current request can not be routed.
// If this is the case, the request is answered with 'Method Not Allowed'
// and HTTP status code 405.
// If no other Method is allowed, the request is delegated to the NotFound
// handler.
HandleMethodNotAllowed bool
// Configurable http.HandlerFunc which is called when no matching route is
// found. If it is not set, http.NotFound is used.
NotFound http.HandlerFunc
@ -149,8 +157,9 @@ var _ http.Handler = New()
// Path auto-correction, including trailing slashes, is enabled by default.
func New() *Router {
return &Router{
RedirectTrailingSlash: true,
RedirectFixedPath: true,
RedirectTrailingSlash: true,
RedirectFixedPath: true,
HandleMethodNotAllowed: true,
}
}
@ -159,6 +168,11 @@ func (r *Router) GET(path string, handle Handle) {
r.Handle("GET", path, handle)
}
// HEAD is a shortcut for router.Handle("HEAD", path, handle)
func (r *Router) HEAD(path string, handle Handle) {
r.Handle("HEAD", path, handle)
}
// POST is a shortcut for router.Handle("POST", path, handle)
func (r *Router) POST(path string, handle Handle) {
r.Handle("POST", path, handle)
@ -256,6 +270,9 @@ func (r *Router) recv(w http.ResponseWriter, req *http.Request) {
// Lookup allows the manual lookup of a method + path combo.
// This is e.g. useful to build a framework around this router.
// If the path was found, it returns the handle function and the path parameter
// values. Otherwise the third return value indicates whether a redirection to
// the same path with an extra / without the trailing slash should be performed.
func (r *Router) Lookup(method, path string) (Handle, Params, bool) {
if root := r.trees[method]; root != nil {
return root.getValue(path)
@ -284,7 +301,7 @@ func (r *Router) ServeHTTP(w http.ResponseWriter, req *http.Request) {
}
if tsr && r.RedirectTrailingSlash {
if path[len(path)-1] == '/' {
if len(path) > 1 && path[len(path)-1] == '/' {
req.URL.Path = path[:len(path)-1]
} else {
req.URL.Path = path + "/"
@ -308,6 +325,25 @@ func (r *Router) ServeHTTP(w http.ResponseWriter, req *http.Request) {
}
}
// Handle 405
if r.HandleMethodNotAllowed {
for method := range r.trees {
// Skip the requested method - we already tried this one
if method == req.Method {
continue
}
handle, _, _ := r.trees[method].getValue(req.URL.Path)
if handle != nil {
http.Error(w,
http.StatusText(http.StatusMethodNotAllowed),
http.StatusMethodNotAllowed,
)
return
}
}
}
// Handle 404
if r.NotFound != nil {
r.NotFound(w, req)

View file

@ -76,7 +76,7 @@ func (h handlerStruct) ServeHTTP(w http.ResponseWriter, r *http.Request) {
}
func TestRouterAPI(t *testing.T) {
var get, post, put, patch, delete, handler, handlerFunc bool
var get, head, post, put, patch, delete, handler, handlerFunc bool
httpHandler := handlerStruct{&handler}
@ -84,6 +84,9 @@ func TestRouterAPI(t *testing.T) {
router.GET("/GET", func(w http.ResponseWriter, r *http.Request, _ Params) {
get = true
})
router.HEAD("/GET", func(w http.ResponseWriter, r *http.Request, _ Params) {
head = true
})
router.POST("/POST", func(w http.ResponseWriter, r *http.Request, _ Params) {
post = true
})
@ -109,6 +112,12 @@ func TestRouterAPI(t *testing.T) {
t.Error("routing GET failed")
}
r, _ = http.NewRequest("HEAD", "/GET", nil)
router.ServeHTTP(w, r)
if !head {
t.Error("routing HEAD failed")
}
r, _ = http.NewRequest("POST", "/POST", nil)
router.ServeHTTP(w, r)
if !post {
@ -156,12 +165,28 @@ func TestRouterRoot(t *testing.T) {
}
}
func TestRouterNotAllowed(t *testing.T) {
handlerFunc := func(_ http.ResponseWriter, _ *http.Request, _ Params) {}
router := New()
router.POST("/path", handlerFunc)
// Test not allowed
r, _ := http.NewRequest("GET", "/path", nil)
w := httptest.NewRecorder()
router.ServeHTTP(w, r)
if !(w.Code == http.StatusMethodNotAllowed) {
t.Errorf("NotAllowed handling route %s failed: Code=%d, Header=%v", w.Code, w.Header())
}
}
func TestRouterNotFound(t *testing.T) {
handlerFunc := func(_ http.ResponseWriter, _ *http.Request, _ Params) {}
router := New()
router.GET("/path", handlerFunc)
router.GET("/dir/", handlerFunc)
router.GET("/", handlerFunc)
testRoutes := []struct {
route string
@ -170,6 +195,7 @@ func TestRouterNotFound(t *testing.T) {
}{
{"/path/", 301, "map[Location:[/path]]"}, // TSR -/
{"/dir", 301, "map[Location:[/dir/]]"}, // TSR +/
{"", 301, "map[Location:[/]]"}, // TSR +/
{"/PATH", 301, "map[Location:[/path]]"}, // Fixed Case
{"/DIR/", 301, "map[Location:[/dir/]]"}, // Fixed Case
{"/PATH/", 301, "map[Location:[/path]]"}, // Fixed Case -/

View file

@ -379,7 +379,7 @@ walk: // Outer loop for walking the tree
return
default:
panic("Unknown node type")
panic("Invalid node type")
}
}
} else if path == n.path {
@ -490,7 +490,7 @@ func (n *node) findCaseInsensitivePath(path string, fixTrailingSlash bool) (ciPa
return append(ciPath, path...), true
default:
panic("Unknown node type")
panic("Invalid node type")
}
}
} else {

View file

@ -557,3 +557,28 @@ func TestTreeFindCaseInsensitivePath(t *testing.T) {
}
}
}
func TestTreeInvalidNodeType(t *testing.T) {
tree := &node{}
tree.addRoute("/", fakeHandler("/"))
tree.addRoute("/:page", fakeHandler("/:page"))
// set invalid node type
tree.children[0].nType = 42
// normal lookup
recv := catchPanic(func() {
tree.getValue("/test")
})
if rs, ok := recv.(string); !ok || rs != "Invalid node type" {
t.Fatalf(`Expected panic "Invalid node type", got "%v"`, recv)
}
// case-insensitive lookup
recv = catchPanic(func() {
tree.findCaseInsensitivePath("/test", true)
})
if rs, ok := recv.(string); !ok || rs != "Invalid node type" {
t.Fatalf(`Expected panic "Invalid node type", got "%v"`, recv)
}
}

View file

@ -1,6 +1,6 @@
faststats is released under a BSD 2-Clause license, reproduced below.
Copyright (c) 2014, The faststats Authors
Copyright (c) 2015, The faststats Authors
All rights reserved.
Redistribution and use in source and binary forms, with or without modification,

View file

@ -1,4 +1,4 @@
// Copyright 2014 The faststats Authors. All rights reserved.
// Copyright 2015 The faststats Authors. All rights reserved.
// Use of this source code is governed by the BSD 2-Clause license,
// which can be found in the LICENSE file.

View file

@ -1,4 +1,4 @@
// Copyright 2014 The faststats Authors. All rights reserved.
// Copyright 2015 The faststats Authors. All rights reserved.
// Use of this source code is governed by the BSD 2-Clause license,
// which can be found in the LICENSE file.

View file

@ -1,4 +1,4 @@
// Copyright 2014 The faststats Authors. All rights reserved.
// Copyright 2015 The faststats Authors. All rights reserved.
// Use of this source code is governed by the BSD 2-Clause license,
// which can be found in the LICENSE file.

View file

@ -1,4 +1,4 @@
// Copyright 2014 The faststats Authors. All rights reserved.
// Copyright 2015 The faststats Authors. All rights reserved.
// Use of this source code is governed by the BSD 2-Clause license,
// which can be found in the LICENSE file.

View file

@ -1,4 +1,4 @@
// Copyright 2014 The faststats Authors. All rights reserved.
// Copyright 2015 The faststats Authors. All rights reserved.
// Use of this source code is governed by the BSD 2-Clause license,
// which can be found in the LICENSE file.

View file

@ -1,6 +1,6 @@
Chihaya is released under a BSD 2-Clause license, reproduced below.
flatjson is released under a BSD 2-Clause license, reproduced below.
Copyright (c) 2014, The Chihaya Authors
Copyright (c) 2015, The flatjson Authors
All rights reserved.
Redistribution and use in source and binary forms, with or without modification,

View file

@ -7,7 +7,7 @@ Example use case:
```json
{
"Connections" {
"Connections": {
"Open": 2,
"Accepted": 4
},

View file

@ -1,4 +1,4 @@
// Copyright 2014 The flatjson Authors. All rights reserved.
// Copyright 2015 The flatjson Authors. All rights reserved.
// Use of this source code is governed by the BSD 2-Clause license,
// which can be found in the LICENSE file.

View file

@ -14,14 +14,11 @@ type Child struct {
}
func TestBasicFlatten(t *testing.T) {
val := &struct {
A int
B string
}{10, "str"}
val := &Child{10, "str"}
expected := flatjson.Map{
"A": 10.0, // JSON numbers are all float64.
"B": "str",
"CC": 10.0, // JSON numbers are all float64.
"CD": "str",
}
testFlattening(t, val, expected)

View file

@ -12,7 +12,7 @@ import (
"time"
"github.com/stretchr/pat/stop"
"code.google.com/p/go.net/netutil"
"golang.org/x/net/netutil"
)
// Server wraps an http.Server with graceful connection handling.
@ -115,8 +115,13 @@ func (srv *Server) ListenAndServe() error {
// timeout is the duration to wait until killing active requests and stopping the server.
// If timeout is 0, the server never times out. It waits for all active requests to finish.
func ListenAndServeTLS(server *http.Server, certFile, keyFile string, timeout time.Duration) error {
// Create the listener ourselves so we can control its lifetime
srv := &Server{Timeout: timeout, Server: server}
return srv.ListenAndServeTLS(certFile, keyFile)
}
// ListenAndServeTLS is equivalent to http.Server.ListenAndServeTLS with graceful shutdown enabled.
func (srv *Server) ListenAndServeTLS(certFile, keyFile string) error {
// Create the listener ourselves so we can control its lifetime
addr := srv.Addr
if addr == "" {
addr = ":https"

View file

@ -4,7 +4,7 @@
// Package netutil provides network utility functions, complementing the more
// common ones in the net package.
package netutil
package netutil // import "golang.org/x/net/netutil"
import (
"net"

View file

@ -11,6 +11,7 @@
"real_ip_header": "",
"respect_af": false,
"client_whitelist_enabled": false,
"client_whitelist": ["OP1011"],
"http_listen_addr": ":6881",
"http_request_timeout": "10s",
"http_read_timeout": "10s",

View file

@ -83,6 +83,7 @@ func newRouter(s *Server) *httprouter.Router {
}
if s.config.ClientWhitelistEnabled {
r.GET("/clients/:clientID", makeHandler(s.getClient))
r.PUT("/clients/:clientID", makeHandler(s.putClient))
r.DELETE("/clients/:clientID", makeHandler(s.delClient))
}

View file

@ -178,6 +178,13 @@ func (s *Server) delUser(w http.ResponseWriter, r *http.Request, p httprouter.Pa
return http.StatusOK, nil
}
func (s *Server) getClient(w http.ResponseWriter, r *http.Request, p httprouter.Params) (int, error) {
if err := s.tracker.ClientApproved(p.ByName("clientID")); err != nil {
return http.StatusNotFound, err
}
return http.StatusOK, nil
}
func (s *Server) putClient(w http.ResponseWriter, r *http.Request, p httprouter.Params) (int, error) {
s.tracker.PutClient(p.ByName("clientID"))
return http.StatusOK, nil