Revert "Remove demo-server and receipts code" (#2791)

This commit is contained in:
Ben Kalman
2016-10-31 17:10:17 -07:00
committed by GitHub
parent ba5e309c84
commit bbfc27d7fe
7 changed files with 884 additions and 0 deletions
+61
View File
@@ -0,0 +1,61 @@
// Copyright 2016 Attic Labs, Inc. All rights reserved.
// Licensed under the Apache License, version 2.0:
// http://www.apache.org/licenses/LICENSE-2.0
package main
import (
"fmt"
"os"
"github.com/attic-labs/noms/go/chunks"
"github.com/attic-labs/noms/go/util/receipts"
flag "github.com/juju/gnuflag"
)
var (
portFlag = flag.Int("port", 8000, "port to listen on")
ldbDir = flag.String("ldb-dir", "", "directory for ldb database")
authKeyFlag = flag.String("authkey", "", "token to use for authenticating write operations")
receiptKeyFlag = flag.String("receiptkey", "", "Receipt key to use for generating and verifying receipts (generate with tools/crypto/receiptkey)")
)
func main() {
chunks.RegisterLevelDBFlags(flag.CommandLine)
dynFlags := chunks.DynamoFlags("")
flag.Usage = func() {
fmt.Println("Usage: demo-server --authkey <authkey> [options]")
flag.PrintDefaults()
}
flag.Parse(true)
if *authKeyFlag == "" {
flag.Usage()
os.Exit(1)
}
var receiptKey receipts.Key
if *receiptKeyFlag != "" {
var err error
receiptKey, err = receipts.DecodeKey(*receiptKeyFlag)
if err != nil {
fmt.Printf("Invalid receipt key: %s\n", err.Error())
os.Exit(1)
}
}
var factory chunks.Factory
if factory = dynFlags.CreateFactory(); factory != nil {
fmt.Printf("Using dynamo ...\n")
} else if *ldbDir != "" {
factory = chunks.NewLevelDBStoreFactoryUseFlags(*ldbDir)
fmt.Printf("Using leveldb ...\n")
} else {
factory = chunks.NewMemoryStoreFactory()
fmt.Printf("Using mem ...\n")
}
defer factory.Shutter()
startWebServer(factory, *authKeyFlag, receiptKey)
}
+208
View File
@@ -0,0 +1,208 @@
// Copyright 2016 Attic Labs, Inc. All rights reserved.
// Licensed under the Apache License, version 2.0:
// http://www.apache.org/licenses/LICENSE-2.0
package main
import (
"fmt"
"log"
"net"
"net/http"
"os"
"path"
"regexp"
"runtime/debug"
"strings"
"github.com/attic-labs/noms/go/chunks"
"github.com/attic-labs/noms/go/constants"
"github.com/attic-labs/noms/go/d"
"github.com/attic-labs/noms/go/datas"
"github.com/attic-labs/noms/go/util/receipts"
"github.com/julienschmidt/httprouter"
)
const (
dbParam = "dbName"
privatePrefix = "/p/"
nomsBaseHtml = "<html><head></head><body><p>Hi. This is a Noms HTTP server.</p><p>To learn more, visit <a href=\"https://github.com/attic-labs/noms\">our GitHub project</a>.</p></body></html>"
)
var (
authRegexp = regexp.MustCompile("^Bearer\\s+(\\S*)$")
router *httprouter.Router
authKey = ""
receiptKey receipts.Key
)
func setupWebServer(factory chunks.Factory) *httprouter.Router {
router := &httprouter.Router{
HandleMethodNotAllowed: true,
NotFound: http.HandlerFunc(notFound),
PanicHandler: panicHandler,
RedirectFixedPath: true,
}
// Note: We use the beginning of the url path as the database name. Consequently, these routes
// don't match. For each request, h.NotFound() ends up getting called. That function separtes
// the database name from the endpoint and then looks up the route and invokes its handler.
// e.g. http://localhost:8000/dan/root/ doesn't match any of these routes. h.NotFound(), will
// pull out "dan" and lookup up the "/root/" route, and then invoke it.
router.GET(constants.RootPath, corsHandle(storeHandle(factory, datas.HandleRootGet)))
router.POST(constants.RootPath, corsHandle(authorizeHandle(storeHandle(factory, datas.HandleRootPost))))
router.OPTIONS(constants.RootPath, corsHandle(noopHandle))
router.POST(constants.GetRefsPath, corsHandle(storeHandle(factory, datas.HandleGetRefs)))
router.OPTIONS(constants.GetRefsPath, corsHandle(noopHandle))
router.POST(constants.HasRefsPath, corsHandle(storeHandle(factory, datas.HandleHasRefs)))
router.OPTIONS(constants.HasRefsPath, corsHandle(noopHandle))
router.POST(constants.WriteValuePath, corsHandle(authorizeHandle(storeHandle(factory, datas.HandleWriteValue))))
router.OPTIONS(constants.WriteValuePath, corsHandle(noopHandle))
router.GET(constants.BasePath, handleBaseGet)
return router
}
func startWebServer(factory chunks.Factory, authKeyParam string, receiptKeyParam receipts.Key) {
d.Chk.NotEmpty(authKeyParam, "No auth key was provided to startWebServer")
// Allow receiptKey to be empty, we'll just always fail verification if
// anybody tries to access a private database.
authKey = authKeyParam
receiptKey = receiptKeyParam
router = setupWebServer(factory)
fmt.Printf("Listening on http://localhost:%d/...\n", *portFlag)
l, err := net.Listen("tcp", fmt.Sprintf(":%d", *portFlag))
d.Chk.NoError(err)
srv := &http.Server{
Handler: http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
router.ServeHTTP(w, req)
}),
}
log.Fatal(srv.Serve(l))
}
// Attach handlers that provide the Database API
func storeHandle(factory chunks.Factory, hndlr datas.Handler) httprouter.Handle {
return func(w http.ResponseWriter, req *http.Request, params httprouter.Params) {
dbName := params.ByName(dbParam)
if isPrivate(dbName) {
// Private database access is granted with the master auth key, or a receipt.
token := getAuthToken(req)
if token != authKey && !checkReceipt(dbName, token) {
setUnauthorized(w)
return
}
}
cs := factory.CreateStore(dbName)
defer cs.Close()
hndlr(w, req, params, cs)
}
}
func authorizeHandle(f httprouter.Handle) httprouter.Handle {
return func(w http.ResponseWriter, r *http.Request, params httprouter.Params) {
// If it's a private database, delegate authentication to storeHandle.
isPriv := isPrivate(params.ByName(dbParam))
if !isPriv && getAuthToken(r) != authKey {
setUnauthorized(w)
return
}
f(w, r, params)
}
}
func getAuthToken(r *http.Request) (token string) {
if authHeader := r.Header.Get("Authorization"); authHeader != "" {
if res := authRegexp.FindStringSubmatch(authHeader); res != nil {
token = res[1]
}
} else {
token = r.URL.Query().Get("access_token")
}
return
}
func isPrivate(dbName string) bool {
return strings.HasPrefix(dbName, privatePrefix)
}
func checkReceipt(dbName, token string) bool {
if receiptKey == (receipts.Key{}) {
return false
}
data := receipts.Data{
Database: dbName,
}
ok, err := receipts.Verify(receiptKey, token, &data)
if err != nil {
fmt.Printf("Error decoding receipt for %s: %s\n", dbName, err.Error())
} else if !ok {
fmt.Printf("Receipt verification failed for %s issued at %s\n", dbName, data.IssueDate.String())
}
return ok
}
func setUnauthorized(w http.ResponseWriter) {
w.Header().Set("WWW-Authenticate", "Bearer realm=\"Restricted\", error=\"invalid token\"")
http.Error(w, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)
}
func noopHandle(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
}
func corsHandle(f httprouter.Handle) httprouter.Handle {
// TODO: Implement full pre-flighting?
// See: http://www.html5rocks.com/static/images/cors_server_flowchart.png
return func(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
// Can't use * when clients are using cookies.
w.Header().Add("Access-Control-Allow-Origin", r.Header.Get("Origin"))
w.Header().Add("Access-Control-Allow-Methods", "GET, POST")
w.Header().Add("Access-Control-Allow-Headers", datas.NomsVersionHeader)
w.Header().Add("Access-Control-Expose-Headers", datas.NomsVersionHeader)
w.Header().Add(datas.NomsVersionHeader, constants.NomsVersion)
f(w, r, ps)
}
}
func panicHandler(w http.ResponseWriter, r *http.Request, recover interface{}) {
fmt.Fprintf(os.Stderr, "error for request: %s\n", r.URL)
fmt.Fprintf(os.Stderr, "server error: %s\n", recover)
debug.PrintStack()
http.Error(w, "Internal server error", http.StatusInternalServerError)
}
func notFound(w http.ResponseWriter, r *http.Request) {
u := r.URL
p := u.Path
route := "/" + path.Base(p) + "/"
databaseId := path.Dir(strings.TrimRight(p, "/"))
hndl, params, _ := router.Lookup(r.Method, route)
if hndl == nil {
http.NotFound(w, r)
return
}
newParams := append(httprouter.Params{}, httprouter.Param{Key: dbParam, Value: databaseId})
newParams = append(newParams, params...)
hndl(w, r, newParams)
}
func handleBaseGet(w http.ResponseWriter, req *http.Request, params httprouter.Params) {
d.PanicIfTrue(req.Method != "GET", "Expected get method.")
w.Header().Add("content-type", "text/html")
fmt.Fprintf(w, nomsBaseHtml)
}
+261
View File
@@ -0,0 +1,261 @@
// Copyright 2016 Attic Labs, Inc. All rights reserved.
// Licensed under the Apache License, version 2.0:
// http://www.apache.org/licenses/LICENSE-2.0
package main
import (
"bytes"
"crypto/rand"
"encoding/binary"
"fmt"
"io"
"io/ioutil"
"net/http"
"net/http/httptest"
"net/url"
"strings"
"testing"
"time"
"github.com/attic-labs/noms/go/chunks"
"github.com/attic-labs/noms/go/constants"
"github.com/attic-labs/noms/go/datas"
"github.com/attic-labs/noms/go/hash"
"github.com/attic-labs/noms/go/types"
"github.com/attic-labs/noms/go/util/receipts"
"github.com/attic-labs/testify/assert"
)
func TestRoot(t *testing.T) {
assert := assert.New(t)
factory := chunks.NewMemoryStoreFactory()
defer factory.Shutter()
router = setupWebServer(factory)
defer func() { router = nil }()
dbName := "/test/db"
w := httptest.NewRecorder()
r, _ := newRequest("GET", dbName+constants.RootPath, nil)
router.ServeHTTP(w, r)
assert.Equal("00000000000000000000000000000000", w.Body.String())
w = httptest.NewRecorder()
r, _ = newRequest("OPTIONS", dbName+constants.RootPath, nil)
r.Header.Add("Origin", "http://www.noms.io")
router.ServeHTTP(w, r)
assert.Equal(w.HeaderMap["Access-Control-Allow-Origin"][0], "http://www.noms.io")
}
func buildGetRefsRequestBody(hashes map[hash.Hash]struct{}) io.Reader {
values := &url.Values{}
for h := range hashes {
values.Add("ref", h.String())
}
return strings.NewReader(values.Encode())
}
func TestWriteValue(t *testing.T) {
assert := assert.New(t)
// Auth with master key:
authKey = "goodAuthKey"
wrongKey := "wrongAuthKey"
testWriteValue(t, "/test/db", authKey, true, true)
testWriteValue(t, "/test/db", wrongKey, true, false)
testWriteValue(t, "/p/test/db", authKey, true, true)
testWriteValue(t, "/p/test/db", wrongKey, false, false)
// Auth with receipt encrypted with empty (invalid) key:
receipt, err := receipts.Generate(receiptKey, receipts.Data{
Database: "/p/test/db",
IssueDate: time.Now(),
})
assert.NoError(err)
testWriteValue(t, "/p/test/db", receipt, false, false)
testWriteValue(t, "/p/test/db2", receipt, false, false)
// Auth with good receipt:
rand.Read(receiptKey[:])
receipt, err = receipts.Generate(receiptKey, receipts.Data{
Database: "/p/test/db",
IssueDate: time.Now(),
})
assert.NoError(err)
testWriteValue(t, "/p/test/db", receipt, true, true)
testWriteValue(t, "/p/test/db2", receipt, false, false)
// Auth with wrong receipt (different receipt key):
var wrongReceiptKey receipts.Key
rand.Read(wrongReceiptKey[:])
receipt, err = receipts.Generate(wrongReceiptKey, receipts.Data{
Database: "/p/test/db",
IssueDate: time.Now(),
})
assert.NoError(err)
testWriteValue(t, "/p/test/db", receipt, false, false)
testWriteValue(t, "/p/test/db2", receipt, false, false)
// Receipts cannot grant write access to non-private databases:
receipt, err = receipts.Generate(receiptKey, receipts.Data{
Database: "/test/db",
IssueDate: time.Now(),
})
assert.NoError(err)
testWriteValue(t, "/test/db", receipt, true, false)
testWriteValue(t, "/test/db2", receipt, true, false)
}
func testWriteValue(t *testing.T, dbName, testAuthKey string, expectRead, expectWrite bool) {
assert := assert.New(t)
factory := chunks.NewMemoryStoreFactory()
defer factory.Shutter()
router = setupWebServer(factory)
defer func() { router = nil }()
testString := "Now, what?"
var (
w *httptest.ResponseRecorder
r *http.Request
err error
lastRoot *bytes.Buffer
)
// GET /root/
runTestGetRoot := func(key string) {
path := dbName + constants.RootPath + prefixIfNotEmpty("?access_token=", key)
r, err = newRequest("GET", path, nil)
assert.NoError(err)
w = httptest.NewRecorder()
router.ServeHTTP(w, r)
lastRoot = w.Body
}
runTestGetRoot(testAuthKey)
if expectRead {
assert.Equal(http.StatusOK, w.Code)
} else {
assert.Equal(http.StatusUnauthorized, w.Code)
runTestGetRoot(authKey) // this should always succeed
}
// POST /writeValue/ preamble
craftCommit := func(v types.Value) types.Struct {
return datas.NewCommit(v, types.NewSet(), types.NewStruct("Meta", types.StructData{}))
}
tval := craftCommit(types.Bool(true))
wval := craftCommit(types.String(testString))
chunk1 := types.EncodeValue(tval, nil)
chunk2 := types.EncodeValue(wval, nil)
refMap := types.NewMap(
types.String("ds1"), types.NewRef(tval),
types.String("ds2"), types.NewRef(wval))
chunk3 := types.EncodeValue(refMap, nil)
body := &bytes.Buffer{}
// we would use this func, but it's private so use next line instead: serializeHints(body, map[ref.Ref]struct{}{hint: struct{}{}})
err = binary.Write(body, binary.BigEndian, uint32(0))
assert.NoError(err)
chunks.Serialize(chunk1, body)
chunks.Serialize(chunk2, body)
chunks.Serialize(chunk3, body)
// POST /writeValue/
runTestPostWriteValue := func(key string) {
path := dbName + constants.WriteValuePath + prefixIfNotEmpty("?access_token=", key)
w = httptest.NewRecorder()
r, err = newRequest("POST", path, ioutil.NopCloser(body))
assert.NoError(err)
router.ServeHTTP(w, r)
}
runTestPostWriteValue(testAuthKey)
if expectWrite {
assert.Equal(http.StatusCreated, w.Code)
} else {
assert.Equal(http.StatusUnauthorized, w.Code)
runTestPostWriteValue(authKey) // this should always succeed
}
// POST /root/
runTestPostRoot := func(key string) {
args := fmt.Sprintf("?last=%s&current=%s", lastRoot, types.NewRef(refMap).TargetHash())
path := dbName + constants.RootPath + args + prefixIfNotEmpty("&access_token=", key)
w = httptest.NewRecorder()
r, _ = newRequest("POST", path, ioutil.NopCloser(body))
router.ServeHTTP(w, r)
}
runTestPostRoot(testAuthKey)
if expectWrite {
assert.Equal(http.StatusOK, w.Code, string(w.Body.Bytes()))
} else {
assert.Equal(http.StatusUnauthorized, w.Code)
runTestPostRoot(authKey) // this should always succeed
}
// POST /getRefs/
whash := wval.Hash()
hints := map[hash.Hash]struct{}{whash: {}}
rdr := buildGetRefsRequestBody(hints)
runTestPostGetRefs := func(key string) {
path := dbName + constants.GetRefsPath + prefixIfNotEmpty("?access_token=", key)
w = httptest.NewRecorder()
r, _ = newRequest("POST", path, rdr)
r.Header.Add("Content-Type", "application/x-www-form-urlencoded")
router.ServeHTTP(w, r)
}
runTestPostGetRefs(testAuthKey)
if expectRead {
assert.Equal(http.StatusOK, w.Code, string(w.Body.Bytes()))
} else {
assert.Equal(http.StatusUnauthorized, w.Code)
runTestPostGetRefs(authKey) // this should always succeed
}
ms := chunks.NewMemoryStore()
chunks.Deserialize(w.Body, ms, nil)
v := types.DecodeValue(ms.Get(whash), datas.NewDatabase(ms))
assert.Equal(testString, string(v.(types.Struct).Get(datas.ValueField).(types.String)))
}
func newRequest(method, url string, body io.Reader) (req *http.Request, err error) {
req, err = http.NewRequest(method, url, body)
if err != nil {
return
}
req.Header.Set(datas.NomsVersionHeader, constants.NomsVersion)
return
}
func prefixIfNotEmpty(prefix, s string) string {
if s != "" {
return prefix + s
}
return ""
}