├── test └── index.html ├── .gitingore ├── bench.sh ├── pass.go ├── PATCH.go ├── OPTIONS.go ├── PUT.go ├── root.go ├── DELETE.go ├── PATCH_test.go ├── PUT_test.go ├── OPTIONS_test.go ├── README.md ├── logger.go ├── .travis.yml ├── root_test.go ├── etag.go ├── config_test.go ├── etag_test.go ├── CORS.go ├── GET.go ├── LICENSE ├── POST.go ├── test_cert.pem ├── rdf_test.go ├── rdf.go ├── DELETE_test.go ├── helix └── daemon.go ├── init_test.go ├── test_key.pem ├── config.go ├── auth_test.go ├── POST_test.go ├── server_test.go ├── CORS_test.go ├── account.go ├── auth.go ├── server.go ├── conneg_test.go ├── conneg.go ├── GET_test.go ├── tokens.go ├── account_test.go └── tokens_test.go /test/index.html: -------------------------------------------------------------------------------- 1 | Hello static! -------------------------------------------------------------------------------- /.gitingore: -------------------------------------------------------------------------------- 1 | bench.sh 2 | run.sh 3 | -------------------------------------------------------------------------------- /bench.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | go test -bench=. 4 | -------------------------------------------------------------------------------- /pass.go: -------------------------------------------------------------------------------- 1 | package helix 2 | 3 | // import ( 4 | // "errors" 5 | // "github.com/boltdb/bolt" 6 | // "gopkg.in/hlandau/passlib.v1" 7 | // ) 8 | -------------------------------------------------------------------------------- /PATCH.go: -------------------------------------------------------------------------------- 1 | package helix 2 | 3 | import ( 4 | "github.com/gocraft/web" 5 | ) 6 | 7 | func (c *Context) PatchHandler(w web.ResponseWriter, req *web.Request) { 8 | 9 | } 10 | -------------------------------------------------------------------------------- /OPTIONS.go: -------------------------------------------------------------------------------- 1 | package helix 2 | 3 | import ( 4 | "github.com/gocraft/web" 5 | ) 6 | 7 | func (c *Context) OptionsHandler(w web.ResponseWriter, r *web.Request, methods []string) { 8 | 9 | } 10 | -------------------------------------------------------------------------------- /PUT.go: -------------------------------------------------------------------------------- 1 | package helix 2 | 3 | import ( 4 | "github.com/gocraft/web" 5 | ) 6 | 7 | // PutHandler is used to create/overwrite non-RDF resources 8 | func (c *Context) PutHandler(w web.ResponseWriter, req *web.Request) { 9 | 10 | } 11 | -------------------------------------------------------------------------------- /root.go: -------------------------------------------------------------------------------- 1 | package helix 2 | 3 | import ( 4 | "fmt" 5 | "github.com/gocraft/web" 6 | ) 7 | 8 | func (c *Context) RootHandler(w web.ResponseWriter, req *web.Request) { 9 | logger.Info().Msg("In root") 10 | fmt.Fprint(w, "Hello world from root") 11 | } 12 | -------------------------------------------------------------------------------- /DELETE.go: -------------------------------------------------------------------------------- 1 | package helix 2 | 3 | import ( 4 | "github.com/gocraft/web" 5 | ) 6 | 7 | func (c *Context) DeleteHandler(w web.ResponseWriter, req *web.Request) { 8 | URI := absoluteURI(req.Request) 9 | err := c.delGraph(URI) 10 | if err != nil { 11 | w.WriteHeader(404) 12 | w.Write([]byte(err.Error())) 13 | return 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /PATCH_test.go: -------------------------------------------------------------------------------- 1 | package helix 2 | 3 | import ( 4 | "net/http" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func Test_PATCH(t *testing.T) { 11 | req, err := http.NewRequest("PATCH", testServer.URL+"/foo", nil) 12 | assert.NoError(t, err) 13 | resp, err := testClient.Do(req) 14 | assert.NoError(t, err) 15 | assert.Equal(t, 200, resp.StatusCode) 16 | } 17 | -------------------------------------------------------------------------------- /PUT_test.go: -------------------------------------------------------------------------------- 1 | package helix 2 | 3 | import ( 4 | "net/http" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func Test_PUT(t *testing.T) { 11 | req, err := http.NewRequest("PUT", testServer.URL+"/foo", nil) 12 | assert.NoError(t, err) 13 | res, err := testClient.Do(req) 14 | assert.NoError(t, err) 15 | assert.Equal(t, 200, res.StatusCode) 16 | 17 | } 18 | -------------------------------------------------------------------------------- /OPTIONS_test.go: -------------------------------------------------------------------------------- 1 | package helix 2 | 3 | import ( 4 | "net/http" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func Test_OPTIONS(t *testing.T) { 11 | req, err := http.NewRequest("OPTIONS", testServer.URL+"/foo", nil) 12 | assert.NoError(t, err) 13 | res, err := testClient.Do(req) 14 | assert.NoError(t, err) 15 | assert.Equal(t, 200, res.StatusCode) 16 | } 17 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # helix 2 | 3 | [![Build Status](https://api.travis-ci.org/deiu/helix.svg?branch=master)](https://travis-ci.org/deiu/helix) 4 | [![Coverage Status](https://coveralls.io/repos/github/deiu/helix/badge.svg?branch=master)](https://coveralls.io/github/deiu/helix?branch=master) 5 | 6 | 7 | Go Link Data server. This is a parallel implementation of the [Solid](https://github.com/solid/solid) stack, which aims to simplify things a lot. 8 | -------------------------------------------------------------------------------- /logger.go: -------------------------------------------------------------------------------- 1 | package helix 2 | 3 | import ( 4 | "os" 5 | 6 | "github.com/gocraft/web" 7 | "github.com/rs/zerolog" 8 | ) 9 | 10 | func (c *Context) RequestLogger(w web.ResponseWriter, req *web.Request, next web.NextMiddlewareFunc) { 11 | logger = zerolog.New(os.Stderr).With(). 12 | Timestamp(). 13 | Str("Method", req.Method). 14 | Str("Path", req.Request.URL.String()). 15 | Str("User", c.User). 16 | Logger() 17 | 18 | next(w, req) 19 | } 20 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | sudo: false 3 | go: 1.8.1 4 | 5 | before_install: 6 | - go get github.com/mattn/goveralls 7 | script: 8 | - go test -v -covermode=count -coverprofile=coverage.out 9 | - $HOME/gopath/bin/goveralls -coverprofile=coverage.out -service=travis-ci 10 | 11 | notifications: 12 | webhooks: 13 | on_success: change # options: [always|never|change] default: always 14 | on_failure: always # options: [always|never|change] default: always 15 | on_start: false # default: false 16 | 17 | -------------------------------------------------------------------------------- /root_test.go: -------------------------------------------------------------------------------- 1 | package helix 2 | 3 | import ( 4 | "net/http" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func Test_Root(t *testing.T) { 11 | request, err := http.NewRequest("GET", testServer.URL, nil) 12 | assert.NoError(t, err) 13 | response, err := testClient.Do(request) 14 | assert.NoError(t, err) 15 | assert.Equal(t, 200, response.StatusCode) 16 | 17 | request, err = http.NewRequest("GET", testServer.URL+"/", nil) 18 | assert.NoError(t, err) 19 | response, err = testClient.Do(request) 20 | assert.NoError(t, err) 21 | assert.Equal(t, 200, response.StatusCode) 22 | } 23 | -------------------------------------------------------------------------------- /etag.go: -------------------------------------------------------------------------------- 1 | package helix 2 | 3 | import ( 4 | "crypto/sha1" 5 | "fmt" 6 | "strings" 7 | ) 8 | 9 | func newETag(data []byte) string { 10 | h := sha1.New() 11 | h.Write(data) 12 | return fmt.Sprintf("%x", h.Sum(nil)) 13 | } 14 | 15 | func ETagMatch(header string, etag string) bool { 16 | if len(etag) == 0 { 17 | return true 18 | } 19 | if len(header) == 0 { 20 | return true 21 | } 22 | val := strings.Split(header, ",") 23 | for _, v := range val { 24 | v = strings.TrimSpace(v) 25 | if v == "*" || v == etag { 26 | return true 27 | } 28 | } 29 | return false 30 | } 31 | 32 | func ETagNoneMatch(header string, etag string) bool { 33 | if len(etag) == 0 { 34 | return true 35 | } 36 | if len(header) == 0 { 37 | return true 38 | } 39 | val := strings.Split(header, ",") 40 | for _, v := range val { 41 | v = strings.TrimSpace(v) 42 | if v != "*" && v != etag { 43 | return true 44 | } 45 | } 46 | return false 47 | } 48 | -------------------------------------------------------------------------------- /config_test.go: -------------------------------------------------------------------------------- 1 | package helix 2 | 3 | import ( 4 | "encoding/json" 5 | "io/ioutil" 6 | "os" 7 | "testing" 8 | 9 | "github.com/stretchr/testify/assert" 10 | ) 11 | 12 | func Test_Conf_NewConfig(t *testing.T) { 13 | conf := NewConfig() 14 | assert.Equal(t, "8443", conf.Port) 15 | assert.Equal(t, "test_cert.pem", conf.Cert) 16 | assert.Equal(t, "test_key.pem", conf.Key) 17 | } 18 | 19 | func Test_Conf_LoadJSONFile(t *testing.T) { 20 | file := "test_conf.json" 21 | conf := NewConfig() 22 | // fail to load inexisting file 23 | err := conf.LoadJSONFile(file) 24 | assert.Error(t, err) 25 | // change some config value 26 | conf.Port = "8888" 27 | data, err := json.Marshal(conf) 28 | assert.NoError(t, err) 29 | // write file 30 | err = ioutil.WriteFile(file, data, 0644) 31 | assert.NoError(t, err) 32 | // read file 33 | conf = NewConfig() 34 | err = conf.LoadJSONFile(file) 35 | assert.NoError(t, err) 36 | assert.Equal(t, "8888", conf.Port) 37 | // Cleanup 38 | err = os.Remove(file) 39 | assert.NoError(t, err) 40 | } 41 | -------------------------------------------------------------------------------- /etag_test.go: -------------------------------------------------------------------------------- 1 | package helix 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func Test_RDF_NewETag(t *testing.T) { 10 | data := []byte("test") 11 | assert.Equal(t, "a94a8fe5ccb19ba61c4c0873d391e987982fbbd3", newETag(data)) 12 | } 13 | 14 | func Test_ETag_Match(t *testing.T) { 15 | goodHeader := "12345" 16 | badHeader := "123" 17 | empty := "" 18 | star := "*" 19 | etag := "12345" 20 | 21 | assert.False(t, ETagMatch(badHeader, etag)) 22 | assert.True(t, ETagMatch("", "")) 23 | assert.True(t, ETagMatch(empty, etag)) 24 | assert.True(t, ETagMatch(goodHeader, etag)) 25 | assert.True(t, ETagMatch(star, etag)) 26 | } 27 | 28 | func Test_ETag_NoneMatch(t *testing.T) { 29 | goodHeader := "12345" 30 | badHeader := "123" 31 | empty := "" 32 | star := "*" 33 | etag := "12345" 34 | 35 | assert.True(t, ETagNoneMatch(badHeader, etag)) 36 | assert.True(t, ETagNoneMatch("", "")) 37 | assert.True(t, ETagNoneMatch(empty, etag)) 38 | assert.False(t, ETagNoneMatch(goodHeader, etag)) 39 | assert.False(t, ETagNoneMatch(star, etag)) 40 | } 41 | -------------------------------------------------------------------------------- /CORS.go: -------------------------------------------------------------------------------- 1 | package helix 2 | 3 | import ( 4 | "github.com/gocraft/web" 5 | "strings" 6 | ) 7 | 8 | func (c *Context) CORS(w web.ResponseWriter, r *web.Request, next web.NextMiddlewareFunc) { 9 | origin := r.Request.Header.Get("Origin") 10 | if len(origin) > 0 { 11 | w.Header().Set("Access-Control-Allow-Origin", origin) 12 | } 13 | if len(origin) < 1 { 14 | w.Header().Set("Access-Control-Allow-Origin", "*") 15 | } 16 | 17 | crh := r.Request.Header.Get("Access-Control-Request-Headers") // CORS preflight only 18 | if len(crh) > 0 { 19 | w.Header().Set("Access-Control-Allow-Headers", crh) 20 | } 21 | crm := r.Request.Header.Get("Access-Control-Request-Method") // CORS preflight only 22 | if len(crm) > 0 { 23 | w.Header().Set("Access-Control-Allow-Methods", crm) 24 | } else { 25 | w.Header().Set("Access-Control-Allow-Methods", strings.Join(methodsAll, ", ")) 26 | } 27 | 28 | w.Header().Set("Accept-Post", strings.Join(rdfMimes, ", ")) 29 | 30 | if c.Config.HSTS { 31 | w.Header().Set("Strict-Transport-Security", "max-age=63072000") 32 | } 33 | 34 | next(w, r) 35 | } 36 | -------------------------------------------------------------------------------- /GET.go: -------------------------------------------------------------------------------- 1 | package helix 2 | 3 | import ( 4 | "github.com/gocraft/web" 5 | ) 6 | 7 | func (c *Context) GetHandler(w web.ResponseWriter, req *web.Request) { 8 | var err error 9 | ctype := "" 10 | acceptList, _ := conneg(req.Request) 11 | if len(acceptList) > 0 && acceptList[0].SubType != "*" { 12 | ctype, err = acceptList.Negotiate(rdfMimes...) 13 | if err != nil { 14 | w.WriteHeader(406) 15 | w.Write([]byte("HTTP 406 - Accept type not acceptable: " + err.Error())) 16 | return 17 | } 18 | logger.Info().Str("Accept", ctype).Msg("") 19 | } 20 | 21 | w.Header().Set("Content-Type", ctype) 22 | 23 | if canSerialize(ctype) { 24 | c.getRDF(w, req, ctype) 25 | return 26 | } 27 | w.WriteHeader(404) 28 | } 29 | 30 | func (c *Context) getRDF(w web.ResponseWriter, req *web.Request, mime string) { 31 | URI := absoluteURI(req.Request) 32 | graph, err := c.getGraph(URI) 33 | if err != nil { 34 | w.WriteHeader(404) 35 | w.Write([]byte(err.Error())) 36 | return 37 | } 38 | w.Header().Add("ETag", graph.Etag) 39 | 40 | if req.Method == "HEAD" { 41 | return 42 | } 43 | 44 | graph.Graph.Serialize(w, mime) 45 | } 46 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Andrei 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /POST.go: -------------------------------------------------------------------------------- 1 | package helix 2 | 3 | import ( 4 | rdf "github.com/deiu/rdf2go" 5 | "github.com/gocraft/web" 6 | ) 7 | 8 | func (c *Context) PostHandler(w web.ResponseWriter, req *web.Request) { 9 | ctype := req.Header.Get("Content-Type") 10 | logger.Info().Str("Content-Type", ctype).Msg("") 11 | if canParse(ctype) { 12 | c.postRDF(w, req) 13 | return 14 | } 15 | w.WriteHeader(400) 16 | } 17 | 18 | func (c *Context) postRDF(w web.ResponseWriter, req *web.Request) { 19 | URI := absoluteURI(req.Request) 20 | g := rdf.NewGraph(URI) 21 | g.Parse(req.Body, req.Header.Get("Content-Type")) 22 | if g.Len() == 0 { 23 | w.WriteHeader(400) 24 | w.Write([]byte("Empty request body")) 25 | return 26 | } 27 | _, err := c.getGraph(URI) 28 | if err == nil { 29 | w.WriteHeader(409) 30 | w.Write([]byte("Cannot create new graph if it aready exists")) 31 | return 32 | } 33 | 34 | // add graph 35 | // TODO: move this into a go routine 36 | graph := NewGraph() 37 | graph.Graph = g 38 | graph.Etag = newETag([]byte(g.String())) 39 | c.addGraph(URI, graph) 40 | 41 | // add ETag 42 | w.Header().Add("ETag", graph.Etag) 43 | 44 | w.WriteHeader(201) 45 | } 46 | -------------------------------------------------------------------------------- /test_cert.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIC+jCCAeKgAwIBAgIRAPWouokm1hSN0743tB9pVGYwDQYJKoZIhvcNAQELBQAw 3 | EjEQMA4GA1UEChMHQWNtZSBDbzAeFw0xNjEwMDcyMTEwMzBaFw0xNzEwMDcyMTEw 4 | MzBaMBIxEDAOBgNVBAoTB0FjbWUgQ28wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAw 5 | ggEKAoIBAQCjRq6DCkfLr7Fgoi+3MUfuqT2QYYBXM7i/32VmSlaNFQ7mme3EiumL 6 | 2xpZEr1iMybnN58clhCdKy700jNBLPvMh42Ab++na9VMUq9Y8ZQ4cYDqQmUZTddt 7 | /wrcyxEPIRWMAf8HmRBBU3jQw5ATxV92F47beyu67jSq87R+KW9DBGStd/7+KSGo 8 | SV5k+zQewbFjAa2G8lJG+XPxO9fts4EaTKOyc//NBLvVc3EUhZo/rvM0y/dwX5DM 9 | 8tN+qTDyo8V7wKdH0nqoW1JCvpVFjFIRjtjcWu0yMxaMZqhmubhRWDjQZXncTdxZ 10 | +AuBvHg6xjVpjV/Cjy2TH9OToW38oOpxAgMBAAGjSzBJMA4GA1UdDwEB/wQEAwIF 11 | oDATBgNVHSUEDDAKBggrBgEFBQcDATAMBgNVHRMBAf8EAjAAMBQGA1UdEQQNMAuC 12 | CWxvY2FsaG9zdDANBgkqhkiG9w0BAQsFAAOCAQEAmBTqUdOFoZcgKNo4U1Gmhsyf 13 | 0aj077Mx7b7VQ4e8wuH4hWarybUf94hiQkHp/NUNZ3Jcs19ukfvq6gBov052TC6s 14 | u0G5gZMj9mQDWGkOorgiIcLnJqaqap98ALQtBt7ZTg9FFK6kCuvCNuuO3StPTHAc 15 | Z4eNxFw5ROZ3opHBeDS0jI5UYTHGFzIQwgDRIZwFVCmKwyL6umNiD4gCJ1aeBsjg 16 | 7Uahq9L+RE5fpHmBP2LfAzhr6KE3VP4Tzu86LQVtpYDoCJJ9vBXbqX8KOA1OGuFL 17 | lDYPqsYR14eiJhH0J0DgbGRBwicnTvJH/l6iOBgzTHrQMJm1WRJg6Ks0Woeo/Q== 18 | -----END CERTIFICATE----- 19 | -------------------------------------------------------------------------------- /rdf_test.go: -------------------------------------------------------------------------------- 1 | package helix 2 | 3 | import ( 4 | "testing" 5 | 6 | rdf "github.com/deiu/rdf2go" 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func Test_RDF_CanParse(t *testing.T) { 11 | ctype := "text/turtle" 12 | assert.True(t, canParse(ctype)) 13 | ctype = "application/ld+json" 14 | assert.True(t, canParse(ctype)) 15 | ctype = "application/rdf+xml" 16 | assert.False(t, canParse(ctype)) 17 | } 18 | 19 | func Test_RDF_CanSerialize(t *testing.T) { 20 | ctype := "text/turtle" 21 | assert.True(t, canSerialize(ctype)) 22 | ctype = "application/ld+json" 23 | assert.True(t, canSerialize(ctype)) 24 | ctype = "application/rdf+xml" 25 | assert.False(t, canSerialize(ctype)) 26 | } 27 | 28 | func Test_RDF_AddRemoveGraph(t *testing.T) { 29 | var err error 30 | c := NewContext() 31 | URI := "https://example.org" 32 | g := rdf.NewGraph(URI) 33 | graph := NewGraph() 34 | graph.Graph = g 35 | c.addGraph(URI, graph) 36 | graph, err = c.getGraph(URI) 37 | assert.NoError(t, err) 38 | assert.Equal(t, URI, graph.Graph.URI()) 39 | err = c.delGraph(URI) 40 | assert.NoError(t, err) 41 | graph, err = c.getGraph(URI) 42 | assert.Error(t, err) 43 | assert.Nil(t, graph) 44 | } 45 | -------------------------------------------------------------------------------- /rdf.go: -------------------------------------------------------------------------------- 1 | package helix 2 | 3 | import ( 4 | "errors" 5 | rdf "github.com/deiu/rdf2go" 6 | ) 7 | 8 | var mimeParser = map[string]string{ 9 | "text/turtle": "turtle", 10 | "application/ld+json": "jsonld", 11 | // "application/sparql-update": "internal", 12 | } 13 | 14 | var mimeSerializer = map[string]string{ 15 | "text/turtle": "turtle", 16 | "application/ld+json": "jsonld", 17 | } 18 | 19 | var rdfMimes = []string{ 20 | "text/turtle", 21 | "application/ld+json", 22 | } 23 | 24 | type Graph struct { 25 | *rdf.Graph 26 | Etag string 27 | } 28 | 29 | func NewGraph() *Graph { 30 | return &Graph{} 31 | } 32 | 33 | func canParse(ctype string) bool { 34 | if len(mimeParser[ctype]) > 0 { 35 | return true 36 | } 37 | return false 38 | } 39 | 40 | func canSerialize(ctype string) bool { 41 | if len(mimeSerializer[ctype]) > 0 { 42 | return true 43 | } 44 | return false 45 | } 46 | 47 | func (c *Context) addGraph(URI string, graph *Graph) { 48 | c.Store[URI] = graph 49 | } 50 | 51 | func (c *Context) getGraph(URI string) (*Graph, error) { 52 | if c.Store[URI] == nil { 53 | return nil, errors.New("Cannot find graph that matches URI: " + URI) 54 | } 55 | return c.Store[URI], nil 56 | } 57 | 58 | func (c *Context) delGraph(URI string) error { 59 | if c.Store[URI] == nil { 60 | return errors.New("Cannot delete graph that matches URI: " + URI) 61 | } 62 | delete(c.Store, URI) 63 | return nil 64 | } 65 | -------------------------------------------------------------------------------- /DELETE_test.go: -------------------------------------------------------------------------------- 1 | package helix 2 | 3 | import ( 4 | "bytes" 5 | "net/http" 6 | "strings" 7 | "testing" 8 | 9 | rdf "github.com/deiu/rdf2go" 10 | "github.com/stretchr/testify/assert" 11 | ) 12 | 13 | func Test_DELETE_NonExistent(t *testing.T) { 14 | req, err := http.NewRequest("DELETE", testServer.URL+"/foo", nil) 15 | assert.NoError(t, err) 16 | res, err := testClient.Do(req) 17 | assert.NoError(t, err) 18 | assert.Equal(t, 404, res.StatusCode) 19 | } 20 | 21 | func Test_DELETE_RDF(t *testing.T) { 22 | mime := "text/turtle" 23 | URI := testServer.URL + "/foo" 24 | graph := rdf.NewGraph(URI) 25 | graph.AddTriple(rdf.NewResource(URI), rdf.NewResource("pred"), rdf.NewLiteral("obj")) 26 | 27 | buf := new(bytes.Buffer) 28 | graph.Serialize(buf, mime) 29 | 30 | req, err := http.NewRequest("POST", URI, strings.NewReader(buf.String())) 31 | assert.NoError(t, err) 32 | req.Header.Add("Content-Type", mime) 33 | res, err := testClient.Do(req) 34 | assert.NoError(t, err) 35 | assert.Equal(t, 201, res.StatusCode) 36 | 37 | req, err = http.NewRequest("DELETE", URI, nil) 38 | assert.NoError(t, err) 39 | res, err = testClient.Do(req) 40 | assert.NoError(t, err) 41 | assert.Equal(t, 200, res.StatusCode) 42 | 43 | req, err = http.NewRequest("GET", URI, nil) 44 | assert.NoError(t, err) 45 | req.Header.Add("Accept", mime) 46 | res, err = testClient.Do(req) 47 | assert.NoError(t, err) 48 | assert.Equal(t, 404, res.StatusCode) 49 | } 50 | -------------------------------------------------------------------------------- /helix/daemon.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "net/http" 5 | "os" 6 | 7 | "github.com/deiu/helix" 8 | ) 9 | 10 | var ( 11 | port = os.Getenv("HELIX_PORT") 12 | host = os.Getenv("HELIX_HOST") 13 | root = os.Getenv("HELIX_ROOT") 14 | static = os.Getenv("HELIX_STATIC_DIR") 15 | debug = os.Getenv("HELIX_DEBUG") 16 | log = os.Getenv("HELIX_LOGGING") 17 | cert = os.Getenv("HELIX_CERT") 18 | key = os.Getenv("HELIX_KEY") 19 | hsts = os.Getenv("HELIX_HSTS") 20 | bolt = os.Getenv("HELIX_BOLT_PATH") 21 | ) 22 | 23 | func main() { 24 | println("Starting server...") 25 | 26 | config := helix.NewConfig() 27 | config.Port = port 28 | config.Hostname = host 29 | config.Root = root 30 | config.StaticDir = static 31 | config.Cert = cert 32 | config.Key = key 33 | config.BoltPath = bolt 34 | if len(debug) > 0 { 35 | config.Debug = true 36 | } 37 | if len(log) > 0 { 38 | config.Logging = true 39 | } 40 | if len(hsts) > 0 { 41 | config.HSTS = true 42 | } 43 | 44 | println("Listening on " + config.Hostname + ":" + config.Port) 45 | 46 | if len(config.BoltPath) > 0 { 47 | // Start Bolt 48 | err := config.StartBolt() 49 | if err != nil { 50 | println(err.Error()) 51 | return 52 | } 53 | defer config.BoltDB.Close() 54 | } 55 | 56 | // prepare new handler 57 | handler := helix.NewServer(config) 58 | // prepare server 59 | s := &http.Server{ 60 | Addr: ":" + config.Port, 61 | Handler: handler, 62 | } 63 | // set TLS config 64 | tlsCfg, err := helix.NewTLSConfig(config.Cert, config.Key) 65 | if err != nil { 66 | println(err.Error()) 67 | return 68 | } 69 | s.TLSConfig = tlsCfg 70 | // start server 71 | s.ListenAndServeTLS(config.Cert, config.Key) 72 | } 73 | -------------------------------------------------------------------------------- /init_test.go: -------------------------------------------------------------------------------- 1 | package helix 2 | 3 | import ( 4 | "crypto/tls" 5 | "io/ioutil" 6 | "net/http" 7 | "net/http/httptest" 8 | "strings" 9 | 10 | "golang.org/x/net/http2" 11 | ) 12 | 13 | var ( 14 | testConfig *Config 15 | testServer *httptest.Server 16 | testClient *http.Client 17 | 18 | testUser = "alice" 19 | testPass = "testpass" 20 | testEmail = "foo@bar.baz" 21 | testDir = "./test" 22 | ) 23 | 24 | func init() { 25 | // uncomment for extra logging 26 | testConfig = NewConfig() 27 | testConfig.Debug = true 28 | testConfig.HSTS = true 29 | testConfig.StaticDir = testDir 30 | 31 | var err error 32 | testServer, err = newTestServer(testConfig) 33 | if err != nil { 34 | println(err.Error()) 35 | return 36 | } 37 | // testClient 38 | testClient = newTestClient() 39 | } 40 | 41 | func newTestServer(cfg *Config) (*httptest.Server, error) { 42 | var ts *httptest.Server 43 | // testServer 44 | handler := NewServer(cfg) 45 | ts = httptest.NewUnstartedServer(handler) 46 | 47 | // prepare TLS config 48 | tlsCfg, err := NewTLSConfig(cfg.Cert, cfg.Key) 49 | if err != nil { 50 | return ts, err 51 | } 52 | 53 | ts.TLS = tlsCfg 54 | ts.StartTLS() 55 | 56 | ts.URL = strings.Replace(ts.URL, "127.0.0.1", "localhost", 1) 57 | 58 | return ts, nil 59 | } 60 | 61 | func newTestClient() *http.Client { 62 | return &http.Client{ 63 | Transport: &http2.Transport{ 64 | TLSClientConfig: &tls.Config{ 65 | InsecureSkipVerify: true, 66 | NextProtos: []string{"h2"}, 67 | }, 68 | }, 69 | } 70 | } 71 | 72 | func newTempFile(dir, name string) (string, error) { 73 | tmpfile, err := ioutil.TempFile(dir, name) 74 | if err != nil { 75 | return "", err 76 | } 77 | return tmpfile.Name(), nil 78 | } 79 | -------------------------------------------------------------------------------- /test_key.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIIEpAIBAAKCAQEAo0augwpHy6+xYKIvtzFH7qk9kGGAVzO4v99lZkpWjRUO5pnt 3 | xIrpi9saWRK9YjMm5zefHJYQnSsu9NIzQSz7zIeNgG/vp2vVTFKvWPGUOHGA6kJl 4 | GU3Xbf8K3MsRDyEVjAH/B5kQQVN40MOQE8VfdheO23sruu40qvO0filvQwRkrXf+ 5 | /ikhqEleZPs0HsGxYwGthvJSRvlz8TvX7bOBGkyjsnP/zQS71XNxFIWaP67zNMv3 6 | cF+QzPLTfqkw8qPFe8CnR9J6qFtSQr6VRYxSEY7Y3FrtMjMWjGaoZrm4UVg40GV5 7 | 3E3cWfgLgbx4OsY1aY1fwo8tkx/Tk6Ft/KDqcQIDAQABAoIBAA0f3V9DMEo7MTLn 8 | VpaPK40PpZc5fyuDSNKDjo8OYq6shqoarXYjBGrtjcyjKP3/xpzHZ87QcT1w/zFG 9 | xD/08bibHNC6LrVygY7FBrtLj/KJjSdHdwD1tN9upNzipdhlfGnorytZLmlR4GBH 10 | mAk+0FGZyy3xVK6N/0XOmS+a/QqR6vdiOS2r/O4CizMfiDgldKClw6SzJcqj9ewh 11 | EWW+yZwbMd7BFv1IN5uyeRwXqho0sxVWy8HndLwTPX3wgau/vL+tZdytaCsKcIcw 12 | 0XRCfriYH3GZ+pMsbNyox52uie1AWQzjsXcgLOy2Ve/o6PQzpJftwoPFhvpammmZ 13 | UNTzUAECgYEAzcKtXq5vHBCBtOSRJRGAFfviLEGTtija237E65odVQJlbn0YABR1 14 | Lg8FhTMXSqxA7QWx0Ad/gWDZtwRvn0jjVzXGNf4LIbPZJUoxlDeG7dxKOzCk1aY2 15 | HHAv/PLQpCdQqPTUdFS4CXzwuj4D0mHk985RtoK0ceVx/FcAgpnTybECgYEAyyR3 16 | A9b1kgiL7aX90gge+Db+o/IJ7tZOxlEUffnGBXccX0tf/wXExV9OZHd0nmddDONl 17 | hUMCVmToPtNZRaXLrkDR77q/638rgKuCq6tHJE0J6TODvRqEp0BJ5mzO2zh/Yh4z 18 | /jNWpblMY21f99JvrvOP52CCJ/RbYsrtxrtunMECgYBeiwIgTCQvkAIZPSDYGHdz 19 | In6k+SjG/XS6gEA5RWIO6n/yybXaa4wAMtTFhFlCbW2Tuxcd0CQtLXQ8HOSxGsuj 20 | Cclei7FPthSjhrjLMsxjxOGy2sISjUG1xXK3Vla55nqwd3abUUYSzf7KhK4639JW 21 | bs2q/9mrr9K1MMDCQa5HsQKBgQCgRWLI2r3gu3F6y+2X2eRlPS5mNr3ze42nFa0v 22 | PvMmuLTf0l4onGqEtg7pYP3XRAG7+2TLYPTlKLO7bZAPTSGHl4iKtTJaIHk4CRkN 23 | TBLS5x0cqhIUDmn+ctBbRhlmCAsoZF/s/KAuHCXShCalJZgL1goBKLlHwJihNy2m 24 | D5bTgQKBgQCju3iBQIoc3dHNC+47zbgUTzYCQCC9TdmRToRB34QtomGtIuBi7ATz 25 | QbBSmibtKzQwwkoqcM04HhgAgO8QDgEFenrTjaLUBmz9rT6EdtsgWQMEmr9zNc1q 26 | N53eC8cjv1b56TZKcjpN9S4RRXEqiQB50aEzF6MhsxwWzDcPRNriZA== 27 | -----END RSA PRIVATE KEY----- 28 | -------------------------------------------------------------------------------- /config.go: -------------------------------------------------------------------------------- 1 | package helix 2 | 3 | import ( 4 | "encoding/json" 5 | "io/ioutil" 6 | "os" 7 | "path/filepath" 8 | "strings" 9 | "time" 10 | 11 | "github.com/boltdb/bolt" 12 | ) 13 | 14 | type Config struct { 15 | Conf string 16 | Port string 17 | Logging bool 18 | Debug bool 19 | SkipVerify bool 20 | Root string 21 | StaticDir string 22 | StaticPath string 23 | Hostname string 24 | Cert string 25 | Key string 26 | TokenAge int64 27 | HSTS bool 28 | BoltPath string 29 | BoltDB *bolt.DB 30 | FilePath string 31 | DataPath string 32 | ACLPath string 33 | MetaPath string 34 | } 35 | 36 | func NewConfig() *Config { 37 | return &Config{ 38 | Port: "8443", 39 | Root: GetCurrentRoot(), 40 | Logging: false, 41 | Debug: false, 42 | SkipVerify: false, 43 | Cert: "test_cert.pem", 44 | Key: "test_key.pem", 45 | TokenAge: 5, 46 | HSTS: false, 47 | BoltPath: filepath.Join(os.TempDir(), "bolt.db"), 48 | BoltDB: &bolt.DB{}, 49 | StaticPath: "/static/", 50 | FilePath: "/files/", 51 | DataPath: "/data/", 52 | ACLPath: "/acl/", 53 | MetaPath: "/meta/", 54 | } 55 | } 56 | 57 | // LoadJSONFile loads server configuration 58 | func (c *Config) LoadJSONFile(filename string) error { 59 | b, err := ioutil.ReadFile(filename) 60 | if err != nil { 61 | return err 62 | } 63 | return json.Unmarshal(b, &c) 64 | } 65 | 66 | func GetCurrentRoot() string { 67 | root, _ := os.Getwd() 68 | if !strings.HasSuffix(root, "/") { 69 | root += "/" 70 | } 71 | return root 72 | } 73 | 74 | func (c *Config) StartBolt() error { 75 | var err error 76 | c.BoltDB, err = bolt.Open(c.BoltPath, 0664, &bolt.Options{Timeout: 1 * time.Second}) 77 | if err != nil { 78 | return err 79 | } 80 | return nil 81 | } 82 | -------------------------------------------------------------------------------- /auth_test.go: -------------------------------------------------------------------------------- 1 | package helix 2 | 3 | import ( 4 | "net/http" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func Test_AuthenticationRequired(t *testing.T) { 11 | req, err := http.NewRequest("GET", testServer.URL+"/account/", nil) 12 | assert.NoError(t, err) 13 | res, err := testClient.Do(req) 14 | assert.NoError(t, err) 15 | res.Body.Close() 16 | assert.Equal(t, http.StatusUnauthorized, res.StatusCode) 17 | } 18 | 19 | func Test_ParseBearerAuthorizationHeader(t *testing.T) { 20 | token := "verylongnonce" 21 | h := "Bearer " + token 22 | tkn, err := ParseBearerAuthorizationHeader(h) 23 | assert.NoError(t, err) 24 | assert.Equal(t, token, tkn) 25 | 26 | tkn, err = ParseBearerAuthorizationHeader("") 27 | assert.Error(t, err) 28 | assert.Empty(t, tkn) 29 | 30 | h = "Foo bar" 31 | tkn, err = ParseBearerAuthorizationHeader(h) 32 | assert.Error(t, err) 33 | assert.Empty(t, tkn) 34 | } 35 | 36 | func Test_SavePassFail(t *testing.T) { 37 | ctx := NewContext() 38 | ctx.Config = NewConfig() 39 | 40 | err := ctx.savePass(testUser, "") 41 | assert.Error(t, err) 42 | 43 | err = ctx.Config.StartBolt() 44 | assert.NoError(t, err) 45 | 46 | err = ctx.savePass("foo", testPass) 47 | assert.Error(t, err) 48 | 49 | boltCleanup(ctx.Config) 50 | } 51 | 52 | func Test_VerifyPass(t *testing.T) { 53 | ctx := NewContext() 54 | ctx.Config = NewConfig() 55 | 56 | err := ctx.Config.StartBolt() 57 | assert.NoError(t, err) 58 | 59 | ok, err := ctx.verifyPass("", "") 60 | assert.Error(t, err) 61 | assert.False(t, ok) 62 | 63 | err = ctx.addUser(testUser, testPass, testEmail) 64 | assert.NoError(t, err) 65 | 66 | ok, err = ctx.verifyPass("bob", "foo") 67 | assert.Error(t, err) 68 | assert.False(t, ok) 69 | 70 | ok, err = ctx.verifyPass(testUser, "foo") 71 | assert.Error(t, err) 72 | assert.False(t, ok) 73 | 74 | ok, err = ctx.verifyPass(testUser, testPass) 75 | assert.NoError(t, err) 76 | assert.True(t, ok) 77 | 78 | boltCleanup(ctx.Config) 79 | } 80 | -------------------------------------------------------------------------------- /POST_test.go: -------------------------------------------------------------------------------- 1 | package helix 2 | 3 | import ( 4 | "bytes" 5 | "net/http" 6 | "strings" 7 | "testing" 8 | 9 | rdf "github.com/deiu/rdf2go" 10 | "github.com/stretchr/testify/assert" 11 | ) 12 | 13 | func Test_POST_OtherMime(t *testing.T) { 14 | URI := testServer.URL + "/foo" 15 | req, err := http.NewRequest("POST", URI, strings.NewReader("foo")) 16 | assert.NoError(t, err) 17 | req.Header.Add("Content-Type", "text/plain") 18 | res, err := testClient.Do(req) 19 | assert.NoError(t, err) 20 | assert.Equal(t, 400, res.StatusCode) 21 | assert.Equal(t, strings.Join(rdfMimes, ", "), res.Header.Get("Accept-Post")) 22 | } 23 | 24 | func Test_POST_TurtleEmpty(t *testing.T) { 25 | mime := "text/turtle" 26 | URI := testServer.URL + "/foo" 27 | 28 | req, err := http.NewRequest("POST", URI, nil) 29 | assert.NoError(t, err) 30 | req.Header.Add("Content-Type", mime) 31 | res, err := testClient.Do(req) 32 | assert.NoError(t, err) 33 | assert.Equal(t, 400, res.StatusCode) 34 | } 35 | 36 | func Test_POST_Turtle(t *testing.T) { 37 | mime := "text/turtle" 38 | URI := testServer.URL + "/foo" 39 | graph := rdf.NewGraph(URI) 40 | graph.AddTriple(rdf.NewResource(URI), rdf.NewResource("pred"), rdf.NewLiteral("obj")) 41 | 42 | buf := new(bytes.Buffer) 43 | graph.Serialize(buf, mime) 44 | 45 | req, err := http.NewRequest("POST", URI, strings.NewReader(buf.String())) 46 | assert.NoError(t, err) 47 | req.Header.Add("Content-Type", mime) 48 | res, err := testClient.Do(req) 49 | assert.NoError(t, err) 50 | assert.Equal(t, 201, res.StatusCode) 51 | etag := res.Header.Get("Etag") 52 | assert.NotEmpty(t, etag) 53 | 54 | req, err = http.NewRequest("POST", URI, strings.NewReader(buf.String())) 55 | assert.NoError(t, err) 56 | req.Header.Add("Content-Type", mime) 57 | res, err = testClient.Do(req) 58 | assert.NoError(t, err) 59 | assert.Equal(t, 409, res.StatusCode) 60 | 61 | req, err = http.NewRequest("GET", URI, nil) 62 | assert.NoError(t, err) 63 | req.Header.Add("Accept", mime) 64 | res, err = testClient.Do(req) 65 | assert.NoError(t, err) 66 | assert.Equal(t, 200, res.StatusCode) 67 | assert.Equal(t, etag, res.Header.Get("Etag")) 68 | } 69 | -------------------------------------------------------------------------------- /server_test.go: -------------------------------------------------------------------------------- 1 | package helix 2 | 3 | import ( 4 | "crypto/tls" 5 | "net/http" 6 | "os" 7 | "testing" 8 | 9 | "github.com/stretchr/testify/assert" 10 | ) 11 | 12 | func Test_NewTLSConfig_NoCertKey(t *testing.T) { 13 | _, err := NewTLSConfig("", "test_key.pem") 14 | assert.Error(t, err) 15 | 16 | _, err = NewTLSConfig("test_cert.pem", "") 17 | assert.Error(t, err) 18 | } 19 | 20 | func Test_HTTP11(t *testing.T) { 21 | // Create a temporary http/1.1 client 22 | httpClient := &http.Client{ 23 | Transport: &http.Transport{ 24 | TLSClientConfig: &tls.Config{ 25 | InsecureSkipVerify: true, 26 | NextProtos: []string{"http/1.1"}, 27 | }, 28 | }, 29 | } 30 | req, err := http.NewRequest("GET", testServer.URL, nil) 31 | assert.NoError(t, err) 32 | 33 | res, err := httpClient.Do(req) 34 | assert.NoError(t, err) 35 | assert.Equal(t, http.StatusOK, res.StatusCode) 36 | assert.True(t, res.ProtoAtLeast(1, 1)) 37 | } 38 | 39 | func Test_HTTP2(t *testing.T) { 40 | req, err := http.NewRequest("GET", testServer.URL, nil) 41 | assert.NoError(t, err) 42 | 43 | res, err := testClient.Do(req) 44 | assert.NoError(t, err) 45 | assert.Equal(t, http.StatusOK, res.StatusCode) 46 | assert.True(t, res.ProtoAtLeast(2, 0)) 47 | } 48 | 49 | func Test_AbsoluteURI(t *testing.T) { 50 | req, err := http.NewRequest("GET", "http://example.com", nil) 51 | assert.NoError(t, err) 52 | req.Header.Add("X-Forward-Host", "example.org") 53 | assert.Equal(t, "http://example.org", absoluteURI(req)) 54 | 55 | req, err = http.NewRequest("GET", "/foo", nil) 56 | assert.NoError(t, err) 57 | assert.Equal(t, "http://localhost/foo", absoluteURI(req)) 58 | 59 | req, err = http.NewRequest("GET", "http://localhost:80", nil) 60 | assert.NoError(t, err) 61 | assert.Equal(t, "http://localhost", absoluteURI(req)) 62 | } 63 | 64 | func Test_StartBolt(t *testing.T) { 65 | ctx := NewContext() 66 | ctx.Config = NewConfig() 67 | ctx.Config.BoltPath = os.TempDir() 68 | 69 | err := ctx.Config.StartBolt() 70 | assert.Error(t, err) 71 | 72 | ctx.Config = NewConfig() 73 | err = ctx.Config.StartBolt() 74 | assert.NoError(t, err) 75 | defer ctx.Config.BoltDB.Close() 76 | 77 | err = os.Remove(ctx.Config.BoltPath) 78 | assert.NoError(t, err) 79 | } 80 | -------------------------------------------------------------------------------- /CORS_test.go: -------------------------------------------------------------------------------- 1 | package helix 2 | 3 | import ( 4 | "io/ioutil" 5 | "net/http" 6 | "strings" 7 | "testing" 8 | 9 | "github.com/stretchr/testify/assert" 10 | ) 11 | 12 | var ( 13 | exUri = "https://example.org" 14 | ) 15 | 16 | func TestHSTS(t *testing.T) { 17 | req, err := http.NewRequest("HEAD", testServer.URL, nil) 18 | assert.NoError(t, err) 19 | res, err := testClient.Do(req) 20 | assert.NoError(t, err) 21 | assert.Equal(t, "max-age=63072000", res.Header.Get("Strict-Transport-Security")) 22 | } 23 | 24 | func Test_CORS_NoOrigin(t *testing.T) { 25 | req, err := http.NewRequest("GET", testServer.URL, nil) 26 | assert.NoError(t, err) 27 | res, err := testClient.Do(req) 28 | assert.NoError(t, err) 29 | 30 | assert.Equal(t, "*", res.Header.Get("Access-Control-Allow-Origin")) 31 | } 32 | 33 | func Test_CORS_Origin(t *testing.T) { 34 | req, err := http.NewRequest("GET", testServer.URL, nil) 35 | assert.NoError(t, err) 36 | 37 | req.Header.Set("Origin", exUri) 38 | res, err := testClient.Do(req) 39 | assert.NoError(t, err) 40 | 41 | assert.Equal(t, exUri, res.Header.Get("Access-Control-Allow-Origin")) 42 | } 43 | func Test_CORS_AllowHeaders(t *testing.T) { 44 | req, err := http.NewRequest("OPTIONS", testServer.URL, nil) 45 | assert.NoError(t, err) 46 | req.Header.Add("Access-Control-Request-Headers", "User, ETag") 47 | res, err := testClient.Do(req) 48 | assert.NoError(t, err) 49 | body, err := ioutil.ReadAll(res.Body) 50 | assert.NoError(t, err) 51 | res.Body.Close() 52 | assert.Empty(t, string(body)) 53 | assert.Equal(t, 200, res.StatusCode) 54 | assert.Equal(t, "User, ETag", res.Header.Get("Access-Control-Allow-Headers")) 55 | } 56 | 57 | func Test_CORS_NoReqMethod(t *testing.T) { 58 | req, err := http.NewRequest("OPTIONS", testServer.URL, nil) 59 | assert.NoError(t, err) 60 | res, err := testClient.Do(req) 61 | assert.NoError(t, err) 62 | body, err := ioutil.ReadAll(res.Body) 63 | assert.NoError(t, err) 64 | res.Body.Close() 65 | assert.Empty(t, string(body)) 66 | assert.Equal(t, 200, res.StatusCode) 67 | assert.Equal(t, strings.Join(methodsAll, ", "), res.Header.Get("Access-Control-Allow-Methods")) 68 | } 69 | 70 | func Test_CORS_ReqMethod(t *testing.T) { 71 | req, err := http.NewRequest("OPTIONS", testServer.URL, nil) 72 | assert.NoError(t, err) 73 | req.Header.Add("Access-Control-Request-Method", "PATCH") 74 | res, err := testClient.Do(req) 75 | assert.NoError(t, err) 76 | body, err := ioutil.ReadAll(res.Body) 77 | assert.NoError(t, err) 78 | res.Body.Close() 79 | assert.Empty(t, string(body)) 80 | assert.Equal(t, 200, res.StatusCode) 81 | assert.Equal(t, "PATCH", res.Header.Get("Access-Control-Allow-Methods")) 82 | } 83 | -------------------------------------------------------------------------------- /account.go: -------------------------------------------------------------------------------- 1 | package helix 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | "net/http" 7 | 8 | "github.com/boltdb/bolt" 9 | "github.com/gocraft/web" 10 | ) 11 | 12 | type User struct { 13 | Username string 14 | Email string 15 | } 16 | 17 | func NewUser() *User { 18 | return &User{} 19 | } 20 | 21 | func (c *Context) GetAccountHandler(w web.ResponseWriter, req *web.Request) { 22 | if len(c.User) == 0 { 23 | c.AuthenticationRequired(w, req) 24 | } 25 | } 26 | 27 | func (c *Context) LoginHandler(w web.ResponseWriter, req *web.Request) { 28 | ok, err := c.verifyPass(req.FormValue("username"), req.FormValue("password")) 29 | if err != nil { 30 | logger.Info().Msg("Login error: " + err.Error()) 31 | } 32 | if !ok { 33 | c.AuthenticationRequired(w, req) 34 | return 35 | } 36 | user := req.FormValue("username") 37 | 38 | w.Header().Set("User", user) 39 | 40 | c.newAuthzToken(w, req.Request, user) 41 | } 42 | 43 | func (c *Context) LogoutHandler(w web.ResponseWriter, req *web.Request) { 44 | // delete session/cookie 45 | if len(c.User) == 0 { 46 | c.AuthenticationRequired(w, req) 47 | return 48 | } 49 | } 50 | 51 | func (c *Context) DeleteAccountHandler(w web.ResponseWriter, req *web.Request) { 52 | if len(c.User) == 0 { 53 | c.AuthenticationRequired(w, req) 54 | return 55 | } 56 | err := c.deleteUser(c.User) 57 | if err != nil { 58 | logger.Info().Msg("Error closing account for " + c.User + ":" + err.Error()) 59 | w.WriteHeader(http.StatusInternalServerError) 60 | return 61 | } 62 | // also clean sessions, etc by logging user out 63 | } 64 | 65 | func (c *Context) NewAccountHandler(w web.ResponseWriter, req *web.Request) { 66 | err := c.addUser(req.FormValue("username"), req.FormValue("password"), req.FormValue("email")) 67 | if err != nil { 68 | errMsg := "Error creating account: " + err.Error() 69 | logger.Info().Msg(errMsg) 70 | w.WriteHeader(http.StatusBadRequest) 71 | w.Write([]byte(errMsg)) 72 | return 73 | } 74 | // start new session 75 | c.User = req.FormValue("username") 76 | w.Write([]byte("Account created!")) 77 | } 78 | 79 | func (c *Context) addUser(user, pass, email string) error { 80 | if len(user) == 0 || len(pass) == 0 || len(email) == 0 { 81 | return errors.New("The username and password cannot be empty") 82 | } 83 | u := NewUser() 84 | u.Username = user 85 | u.Email = email 86 | 87 | // store the new user 88 | err := c.saveUser(u) 89 | if err != nil { 90 | return err 91 | } 92 | // store the new pass 93 | return c.savePass(user, pass) 94 | } 95 | 96 | func (c *Context) getUser(username string) (*User, error) { 97 | user := NewUser() 98 | 99 | err := c.Config.BoltDB.View(func(tx *bolt.Tx) error { 100 | userBucket := tx.Bucket([]byte(username)) 101 | if userBucket == nil { 102 | return errors.New("Could not find a user bucket for " + username) 103 | } 104 | err := json.Unmarshal(userBucket.Get([]byte("user")), user) 105 | return err 106 | }) 107 | return user, err 108 | } 109 | 110 | func (c *Context) saveUser(user *User) error { 111 | err := c.Config.BoltDB.Update(func(tx *bolt.Tx) error { 112 | userBucket, err := tx.CreateBucketIfNotExists([]byte(user.Username)) 113 | if err != nil { 114 | return err 115 | } 116 | // No need to handle error since we only have strings in the user struct 117 | buf, _ := json.Marshal(user) 118 | err = userBucket.Put([]byte("user"), buf) 119 | return err 120 | }) 121 | return err 122 | } 123 | 124 | func (c *Context) deleteUser(user string) error { 125 | err := c.Config.BoltDB.Update(func(tx *bolt.Tx) error { 126 | return tx.DeleteBucket([]byte(user)) 127 | }) 128 | return err 129 | } 130 | -------------------------------------------------------------------------------- /auth.go: -------------------------------------------------------------------------------- 1 | package helix 2 | 3 | import ( 4 | "errors" 5 | "github.com/rs/zerolog" 6 | "net/http" 7 | "net/url" 8 | "os" 9 | "strings" 10 | 11 | "github.com/boltdb/bolt" 12 | "github.com/deiu/webid-rsa" 13 | "github.com/gocraft/web" 14 | "gopkg.in/hlandau/passlib.v1" 15 | ) 16 | 17 | func (c *Context) AuthenticationRequired(w web.ResponseWriter, req *web.Request) { 18 | if len(c.User) == 0 { 19 | authn := webidrsa.NewAuthenticateHeader(req.Request) 20 | w.Header().Set("WWW-Authenticate", authn) 21 | w.WriteHeader(http.StatusUnauthorized) 22 | return 23 | } 24 | } 25 | 26 | func (c *Context) Authentication(w web.ResponseWriter, req *web.Request, next web.NextMiddlewareFunc) { 27 | errMsg := "" 28 | user := "" 29 | 30 | if len(req.Header.Get("Authorization")) > 0 { 31 | authz, err := webidrsa.ParseAuthorizationHeader(req.Header.Get("Authorization")) 32 | if err != nil { 33 | errMsg = err.Error() 34 | } 35 | switch authz.Type { 36 | case "WebID-RSA": 37 | user, err = webidrsa.Authenticate(req.Request) 38 | case "Bearer": 39 | token, _ := ParseBearerAuthorizationHeader(req.Header.Get("Authorization")) 40 | user, err = c.getAuthzUserFromToken(token, req.Host, webidrsa.GetOrigin(req.Request)) 41 | } 42 | 43 | if err != nil { 44 | errMsg = "Could not authenticate user: " + err.Error() 45 | } 46 | } 47 | // set the user 48 | c.User = user 49 | 50 | logger = zerolog.New(os.Stderr).With(). 51 | Timestamp(). 52 | Str("Method", req.Method). 53 | Str("Path", req.Request.URL.String()). 54 | Str("User", user). 55 | Logger() 56 | 57 | logger.Info().Msg(errMsg) 58 | 59 | next(w, req) 60 | } 61 | 62 | func (c *Context) verifyPass(user, pass string) (bool, error) { 63 | if len(user) == 0 || len(pass) == 0 { 64 | return false, errors.New("The username and password cannot be empty") 65 | } 66 | hash, err := c.getPass(user) 67 | if err != nil { 68 | return false, err 69 | } 70 | 71 | _, err = passlib.Verify(pass, hash) 72 | if err != nil { 73 | // incorrect password, malformed hash, etc. 74 | // either way, reject 75 | return false, err 76 | } 77 | 78 | // TODO: the context has decided, as per its policy, that 79 | // the hash which was used to validate the password 80 | // should be changed. It has upgraded the hash using 81 | // the verified password. 82 | // if newHash != "" { 83 | // c.storePass(user, newHash) 84 | // } 85 | 86 | return true, nil 87 | } 88 | 89 | func (c *Context) getPass(user string) (string, error) { 90 | hash := "" 91 | err := c.Config.BoltDB.View(func(tx *bolt.Tx) error { 92 | userBucket := tx.Bucket([]byte(user)) 93 | if userBucket == nil { 94 | return errors.New("Could not find a user bucket for " + user) 95 | } 96 | hash = string(userBucket.Get([]byte("pass"))) 97 | return nil 98 | }) 99 | return hash, err 100 | } 101 | 102 | func (c *Context) savePass(user, pass string) error { 103 | if len(pass) == 0 { 104 | return errors.New("The password cannot be empty") 105 | } 106 | 107 | hash, _ := passlib.Hash(pass) 108 | 109 | err := c.Config.BoltDB.Update(func(tx *bolt.Tx) error { 110 | userBucket := tx.Bucket([]byte(user)) 111 | if userBucket == nil { 112 | return errors.New("Could not find user bucket for " + user) 113 | } 114 | err := userBucket.Put([]byte("pass"), []byte(hash)) 115 | return err 116 | }) 117 | return err 118 | } 119 | 120 | func ParseBearerAuthorizationHeader(header string) (string, error) { 121 | if len(header) == 0 { 122 | return "", errors.New("Cannot parse Authorization header: no header present") 123 | } 124 | 125 | parts := strings.SplitN(header, " ", 2) 126 | if parts[0] != "Bearer" { 127 | return "", errors.New("Not a Bearer header. Got: " + parts[0]) 128 | } 129 | return url.QueryUnescape(parts[1]) 130 | } 131 | -------------------------------------------------------------------------------- /server.go: -------------------------------------------------------------------------------- 1 | package helix 2 | 3 | import ( 4 | "crypto/tls" 5 | "errors" 6 | "net" 7 | "net/http" 8 | "os" 9 | "path" 10 | 11 | "github.com/gocraft/web" 12 | "github.com/rs/zerolog" 13 | ) 14 | 15 | const HelixVersion = "0.1" 16 | 17 | var ( 18 | methodsAll = []string{ 19 | "OPTIONS", "HEAD", "GET", "POST", "PUT", "PATCH", "DELETE", 20 | } 21 | logger = zerolog.New(os.Stderr).With().Timestamp().Logger() 22 | ) 23 | 24 | type ( 25 | Context struct { 26 | Config *Config 27 | Store map[string]*Graph 28 | User string 29 | AccessToken string 30 | } 31 | ) 32 | 33 | func NewContext() *Context { 34 | return &Context{ 35 | Config: NewConfig(), 36 | Store: make(map[string]*Graph), 37 | User: "", 38 | } 39 | } 40 | 41 | func NewServer(cfg *Config) *web.Router { 42 | ctx := NewContext() 43 | ctx.Config = cfg 44 | if !ctx.Config.Logging { 45 | zerolog.SetGlobalLevel(zerolog.Disabled) 46 | } 47 | 48 | currentRoot, _ := os.Getwd() 49 | ctx.Config.StaticDir = path.Join(currentRoot, ctx.Config.StaticDir) 50 | 51 | // Create router and add middleware 52 | router := web.New(*ctx). 53 | // Middleware(web.LoggerMiddleware). // turn off once done with tweaking 54 | Middleware(ctx.CORS). 55 | Middleware(ctx.Authentication). 56 | Middleware(ctx.RequestLogger). 57 | Middleware(web.StaticMiddleware(ctx.Config.StaticDir, web.StaticOption{Prefix: ctx.Config.StaticPath})). 58 | OptionsHandler(ctx.OptionsHandler) 59 | 60 | // Account routes 61 | router.Get("/", ctx.RootHandler). 62 | Post("/account/new", ctx.NewAccountHandler). 63 | Post("/account/logout", ctx.LogoutHandler). 64 | Post("/account/login", ctx.LoginHandler). 65 | Post("/account/delete", ctx.DeleteAccountHandler). 66 | Get("/account/", ctx.GetAccountHandler) 67 | 68 | // API routes 69 | router.Get("/:*", ctx.GetHandler). 70 | Post("/:*", ctx.PostHandler). 71 | Put("/:*", ctx.PutHandler). 72 | Delete("/:*", ctx.DeleteHandler). 73 | Patch("/:*", ctx.PatchHandler) 74 | 75 | if ctx.Config.Debug { 76 | router.Middleware(web.ShowErrorsMiddleware) 77 | } 78 | 79 | return router 80 | } 81 | 82 | func NewTLSConfig(cert, key string) (*tls.Config, error) { 83 | var err error 84 | cfg := &tls.Config{} 85 | 86 | if len(cert) == 0 || len(key) == 0 { 87 | return cfg, errors.New("Missing cert and key for TLS configuration") 88 | } 89 | 90 | cfg.MinVersion = tls.VersionTLS12 91 | cfg.NextProtos = []string{"h2"} 92 | // use strong crypto 93 | cfg.PreferServerCipherSuites = true 94 | cfg.CurvePreferences = []tls.CurveID{tls.CurveP521, tls.CurveP384, tls.CurveP256} 95 | cfg.CipherSuites = []uint16{ 96 | tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, 97 | tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, 98 | tls.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA, 99 | tls.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA, 100 | tls.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA, 101 | tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA, 102 | } 103 | cfg.Certificates = make([]tls.Certificate, 1) 104 | cfg.Certificates[0], err = tls.LoadX509KeyPair(cert, key) 105 | 106 | return cfg, err 107 | } 108 | 109 | func absoluteURI(req *http.Request) string { 110 | scheme := "http" 111 | if req.TLS != nil || req.Header.Get("X-Forwarded-Proto") == "https" { 112 | scheme += "s" 113 | } 114 | reqHost := req.Host 115 | if len(req.Header.Get("X-Forward-Host")) > 0 { 116 | reqHost = req.Header.Get("X-Forward-Host") 117 | } 118 | host, port, err := net.SplitHostPort(reqHost) 119 | if err != nil { 120 | host = reqHost 121 | } 122 | if len(host) == 0 { 123 | host = "localhost" 124 | } 125 | if len(port) > 0 { 126 | port = ":" + port 127 | } 128 | if (scheme == "https" && port == ":443") || (scheme == "http" && port == ":80") { 129 | port = "" 130 | } 131 | return scheme + "://" + host + port + req.URL.Path 132 | } 133 | -------------------------------------------------------------------------------- /conneg_test.go: -------------------------------------------------------------------------------- 1 | package helix 2 | 3 | import ( 4 | "net/http" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | var ( 11 | chrome = "application/xml,application/xhtml+xml,text/html;q=0.9,text/plain;q=0.8,image/png,*/*;q=0.5" 12 | rdflib = "application/rdf+xml;q=0.9, application/xhtml+xml;q=0.3, text/xml;q=0.2, application/xml;q=0.2, text/html;q=0.3, text/plain;q=0.1, text/n3;q=1.0, application/x-turtle;q=1, text/turtle;q=1" 13 | ) 14 | 15 | func mockAccept(accept string) (al AcceptList, err error) { 16 | req := &http.Request{} 17 | req.Header = make(http.Header) 18 | req.Header["Accept"] = []string{accept} 19 | al, err = conneg(req) 20 | return 21 | } 22 | 23 | func Test_Negotiate_PicturesOfWebPages(t *testing.T) { 24 | al, err := mockAccept(chrome) 25 | if err != nil { 26 | t.Fatal(err) 27 | } 28 | 29 | contentType, err := al.Negotiate("text/html", "image/png") 30 | assert.NoError(t, err) 31 | assert.Equal(t, "image/png", contentType) 32 | } 33 | 34 | func Test_Negotiate_RDF(t *testing.T) { 35 | al, err := mockAccept(rdflib) 36 | if err != nil { 37 | t.Fatal(err) 38 | } 39 | 40 | contentType, err := al.Negotiate(rdfMimes...) 41 | assert.NoError(t, err) 42 | assert.Equal(t, "text/turtle", contentType) 43 | } 44 | 45 | func Test_Negotiate_FirstMatch(t *testing.T) { 46 | al, err := mockAccept(chrome) 47 | assert.NoError(t, err) 48 | 49 | contentType, err := al.Negotiate("text/html", "text/plain", "text/n3") 50 | assert.NoError(t, err) 51 | assert.Equal(t, "text/html", contentType) 52 | } 53 | 54 | func Test_Negotiate_SecondMatch(t *testing.T) { 55 | al, err := mockAccept(chrome) 56 | assert.NoError(t, err) 57 | 58 | contentType, err := al.Negotiate("text/n3", "text/plain") 59 | assert.NoError(t, err) 60 | assert.Equal(t, "text/plain", contentType) 61 | } 62 | 63 | func Test_Negotiate_WildcardMatch(t *testing.T) { 64 | al, err := mockAccept(chrome) 65 | assert.NoError(t, err) 66 | 67 | contentType, err := al.Negotiate("text/n3", "application/rdf+xml") 68 | assert.NoError(t, err) 69 | assert.Equal(t, "text/n3", contentType) 70 | } 71 | 72 | func Test_Negotiate_SubType(t *testing.T) { 73 | al, err := mockAccept("text/turtle, application/*") 74 | assert.NoError(t, err) 75 | 76 | contentType, err := al.Negotiate("foo/bar", "application/ld+json") 77 | assert.NoError(t, err) 78 | assert.Equal(t, "application/ld+json", contentType) 79 | } 80 | 81 | func Test_Negotiate_InvalidMediaRange(t *testing.T) { 82 | _, err := mockAccept("something/valid, fail, other/valid") 83 | assert.Error(t, err) 84 | } 85 | 86 | func Test_Negotiate_Invalid_Param(t *testing.T) { 87 | _, err := mockAccept("text/plain; foo") 88 | assert.Error(t, err) 89 | } 90 | 91 | func Test_Negotiate_OtherParam(t *testing.T) { 92 | _, err := mockAccept("text/plain;foo=bar") 93 | assert.NoError(t, err) 94 | } 95 | 96 | func Test_Negotiate_EmptyAccept(t *testing.T) { 97 | al, err := mockAccept("") 98 | assert.NoError(t, err) 99 | 100 | _, err = al.Negotiate("text/plain") 101 | assert.Error(t, err) 102 | } 103 | 104 | func Test_Negotiate_NoAlternative(t *testing.T) { 105 | al, err := mockAccept(chrome) 106 | assert.NoError(t, err) 107 | 108 | _, err = al.Negotiate() 109 | assert.Error(t, err) 110 | } 111 | 112 | func Test_Negotiate_StarAccept(t *testing.T) { 113 | al, err := mockAccept("*") 114 | assert.NoError(t, err) 115 | assert.Equal(t, "*/*", al[0].Type+"/"+al[0].SubType) 116 | 117 | al, err = mockAccept("text/*") 118 | assert.NoError(t, err) 119 | assert.Equal(t, "text/*", al[0].Type+"/"+al[0].SubType) 120 | } 121 | 122 | func Test_Negotiate_Sorter(t *testing.T) { 123 | accept := []Accept{} 124 | a := Accept{ 125 | Type: "text", 126 | SubType: "*", 127 | Q: float32(3), 128 | } 129 | accept = append(accept, a) 130 | 131 | a = Accept{ 132 | Type: "*", 133 | SubType: "text", 134 | Q: float32(5), 135 | } 136 | accept = append(accept, a) 137 | 138 | a = Accept{ 139 | Type: "*", 140 | SubType: "text", 141 | } 142 | accept = append(accept, a) 143 | 144 | sorter := acceptSorter(accept) 145 | assert.True(t, sorter.Less(0, 1)) 146 | assert.True(t, sorter.Less(2, 0)) 147 | } 148 | -------------------------------------------------------------------------------- /conneg.go: -------------------------------------------------------------------------------- 1 | package helix 2 | 3 | import ( 4 | "errors" 5 | "net/http" 6 | "sort" 7 | "strconv" 8 | "strings" 9 | ) 10 | 11 | // Accept structure is used to represent a clause in an HTTP Accept Header. 12 | type Accept struct { 13 | Type, SubType string 14 | Q float32 15 | Params map[string]string 16 | } 17 | 18 | // For internal use, so that we can use the sort interface. 19 | type acceptSorter []Accept 20 | 21 | func (accept acceptSorter) Len() int { 22 | return len(accept) 23 | } 24 | 25 | // purposely sorts "backwards" so we have the most appropriate 26 | // (largest q-value) at the beginning of the list. 27 | func (accept acceptSorter) Less(i, j int) bool { 28 | ai, aj := accept[i], accept[j] 29 | if ai.Q > aj.Q { 30 | return true 31 | } 32 | if ai.Type != "*" && aj.Type == "*" { 33 | return true 34 | } 35 | if ai.SubType != "*" && aj.SubType == "*" { 36 | return true 37 | } 38 | return false 39 | } 40 | 41 | func (accept acceptSorter) Swap(i, j int) { 42 | accept[i], accept[j] = accept[j], accept[i] 43 | } 44 | 45 | // AcceptList is a sorted list of clauses from an Accept header. 46 | type AcceptList []Accept 47 | 48 | // Negotiate the most appropriate contentType given the list of alternatives. 49 | // Returns an error if no alternative is acceptable. 50 | func (al AcceptList) Negotiate(alternatives ...string) (contentType string, err error) { 51 | asp := make([][]string, 0, len(alternatives)) 52 | for _, ctype := range alternatives { 53 | asp = append(asp, strings.SplitN(ctype, "/", 2)) 54 | } 55 | for _, clause := range al { 56 | for i, ctsp := range asp { 57 | if clause.Type == ctsp[0] && clause.SubType == ctsp[1] { 58 | contentType = alternatives[i] 59 | return 60 | } 61 | if clause.Type == ctsp[0] && clause.SubType == "*" { 62 | contentType = alternatives[i] 63 | return 64 | } 65 | if clause.Type == "*" && clause.SubType == "*" { 66 | contentType = alternatives[i] 67 | return 68 | } 69 | } 70 | } 71 | err = errors.New("No acceptable alternatives") 72 | return 73 | } 74 | 75 | // Parse an Accept Header string returning a sorted list of clauses. 76 | func parseAccept(header string) (accept []Accept, err error) { 77 | header = strings.Trim(header, " ") 78 | if len(header) == 0 { 79 | accept = make([]Accept, 0) 80 | return 81 | } 82 | 83 | parts := strings.SplitN(header, ",", -1) 84 | accept = make([]Accept, 0, len(parts)) 85 | for _, part := range parts { 86 | part := strings.Trim(part, " ") 87 | 88 | a := Accept{} 89 | a.Params = make(map[string]string) 90 | a.Q = 1.0 91 | 92 | mrp := strings.SplitN(part, ";", -1) 93 | 94 | mediaRange := mrp[0] 95 | sp := strings.SplitN(mediaRange, "/", -1) 96 | a.Type = strings.Trim(sp[0], " ") 97 | 98 | switch { 99 | case len(sp) == 1 && a.Type == "*": 100 | // The case where the Accept header is just "*" is strictly speaking 101 | // invalid but is seen in the wild. We take it to be equivalent to 102 | // "*/*" 103 | a.SubType = "*" 104 | case len(sp) == 2: 105 | a.SubType = strings.Trim(sp[1], " ") 106 | default: 107 | err = errors.New("Invalid media range in " + part) 108 | return 109 | } 110 | 111 | if len(mrp) == 1 { 112 | accept = append(accept, a) 113 | continue 114 | } 115 | 116 | for _, param := range mrp[1:] { 117 | sp := strings.SplitN(param, "=", 2) 118 | if len(sp) != 2 { 119 | err = errors.New("Invalid parameter in " + part) 120 | return 121 | } 122 | token := strings.Trim(sp[0], " ") 123 | if token == "q" { 124 | q, _ := strconv.ParseFloat(sp[1], 32) 125 | a.Q = float32(q) 126 | } else { 127 | a.Params[token] = strings.Trim(sp[1], " ") 128 | } 129 | } 130 | 131 | accept = append(accept, a) 132 | } 133 | 134 | sorter := acceptSorter(accept) 135 | sort.Sort(sorter) 136 | 137 | return 138 | } 139 | 140 | // Parse the Accept header and return a sorted list of clauses. If the Accept header 141 | // is present but empty this will be an empty list. If the header is not present it will 142 | // default to a wildcard: */*. Returns an error if the Accept header is ill-formed. 143 | func conneg(req *http.Request) (al AcceptList, err error) { 144 | var accept string 145 | headers, ok := req.Header["Accept"] 146 | if ok && len(headers) > 0 { 147 | // if multiple Accept headers are specified just take the first one 148 | // such a client would be quite broken... 149 | accept = headers[0] 150 | } else { 151 | // default if not present 152 | accept = "*/*" 153 | } 154 | al, err = parseAccept(accept) 155 | return 156 | } 157 | -------------------------------------------------------------------------------- /GET_test.go: -------------------------------------------------------------------------------- 1 | package helix 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "io/ioutil" 7 | "net/http" 8 | "strings" 9 | "testing" 10 | 11 | rdf "github.com/deiu/rdf2go" 12 | "github.com/stretchr/testify/assert" 13 | ) 14 | 15 | func Test_GET_RDF(t *testing.T) { 16 | mime := "text/turtle" 17 | URI := testServer.URL + "/foo.ttl" 18 | graph := rdf.NewGraph(URI) 19 | graph.AddTriple(rdf.NewResource(URI), rdf.NewResource("http://test.com/foo"), rdf.NewLiteral("obj")) 20 | buf := new(bytes.Buffer) 21 | graph.Serialize(buf, mime) 22 | 23 | req, err := http.NewRequest("POST", URI, strings.NewReader(buf.String())) 24 | assert.NoError(t, err) 25 | req.Header.Add("Content-Type", mime) 26 | res, err := testClient.Do(req) 27 | assert.NoError(t, err) 28 | assert.Equal(t, 201, res.StatusCode) 29 | 30 | req, err = http.NewRequest("GET", URI, nil) 31 | assert.NoError(t, err) 32 | req.Header.Add("Accept", mime) 33 | res, err = testClient.Do(req) 34 | assert.NoError(t, err) 35 | assert.Equal(t, 200, res.StatusCode) 36 | assert.Equal(t, mime, res.Header.Get("Content-Type")) 37 | 38 | graph = rdf.NewGraph(URI) 39 | graph.Parse(res.Body, res.Header.Get("Content-Type")) 40 | res.Body.Close() 41 | assert.Equal(t, 1, graph.Len()) 42 | assert.NotNil(t, graph.One(rdf.NewResource(URI), rdf.NewResource("http://test.com/foo"), rdf.NewLiteral("obj"))) 43 | } 44 | 45 | func Test_GET_Static(t *testing.T) { 46 | req, err := http.NewRequest("GET", testServer.URL+testConfig.StaticPath+"index.html", nil) 47 | assert.NoError(t, err) 48 | res, err := testClient.Do(req) 49 | assert.NoError(t, err) 50 | assert.Equal(t, 200, res.StatusCode) 51 | body, err := ioutil.ReadAll(res.Body) 52 | assert.NoError(t, err) 53 | res.Body.Close() 54 | assert.Equal(t, "Hello static!", string(body)) 55 | 56 | req, err = http.NewRequest("GET", testServer.URL+testConfig.StaticPath+"foo.html", nil) 57 | assert.NoError(t, err) 58 | res, err = testClient.Do(req) 59 | assert.NoError(t, err) 60 | assert.Equal(t, 404, res.StatusCode) 61 | } 62 | 63 | func Test_GET_NonRDF(t *testing.T) { 64 | req, err := http.NewRequest("GET", testServer.URL, nil) 65 | assert.NoError(t, err) 66 | res, err := testClient.Do(req) 67 | assert.NoError(t, err) 68 | assert.Equal(t, 200, res.StatusCode) 69 | 70 | req, err = http.NewRequest("GET", testServer.URL+"/foo", nil) 71 | assert.NoError(t, err) 72 | res, err = testClient.Do(req) 73 | assert.NoError(t, err) 74 | assert.Equal(t, 404, res.StatusCode) 75 | } 76 | 77 | func Test_GET_RDFNotFound(t *testing.T) { 78 | req, err := http.NewRequest("GET", testServer.URL+"/bar", nil) 79 | assert.NoError(t, err) 80 | req.Header.Add("Accept", "text/turtle") 81 | res, err := testClient.Do(req) 82 | assert.NoError(t, err) 83 | assert.Equal(t, 404, res.StatusCode) 84 | } 85 | 86 | func Test_GET_NotAcceptable(t *testing.T) { 87 | req, err := http.NewRequest("GET", testServer.URL+"/foo", nil) 88 | assert.NoError(t, err) 89 | req.Header.Add("Accept", "text/foo") 90 | res, err := testClient.Do(req) 91 | assert.NoError(t, err) 92 | body, err := ioutil.ReadAll(res.Body) 93 | assert.NoError(t, err) 94 | res.Body.Close() 95 | assert.Equal(t, 406, res.StatusCode) 96 | assert.NotEmpty(t, body) 97 | } 98 | 99 | func Test_HEAD(t *testing.T) { 100 | req, err := http.NewRequest("HEAD", testServer.URL+"/baz", nil) 101 | assert.NoError(t, err) 102 | req.Header.Add("Accept", "text/turtle") 103 | res, err := testClient.Do(req) 104 | assert.NoError(t, err) 105 | assert.Equal(t, 404, res.StatusCode) 106 | 107 | req, err = http.NewRequest("HEAD", testServer.URL+"/foo.ttl", nil) 108 | assert.NoError(t, err) 109 | req.Header.Add("Accept", "text/turtle") 110 | res, err = testClient.Do(req) 111 | assert.NoError(t, err) 112 | assert.Equal(t, 200, res.StatusCode) 113 | body, err := ioutil.ReadAll(res.Body) 114 | assert.NoError(t, err) 115 | res.Body.Close() 116 | assert.Equal(t, 0, len(body)) 117 | } 118 | 119 | func BenchmarkGET(b *testing.B) { 120 | mime := "text/turtle" 121 | URI := testServer.URL + "/benchttl" 122 | graph := rdf.NewGraph(URI) 123 | graph.AddTriple(rdf.NewResource(URI), rdf.NewResource("pred"), rdf.NewLiteral("obj")) 124 | 125 | buf := new(bytes.Buffer) 126 | graph.Serialize(buf, mime) 127 | 128 | req, err := http.NewRequest("POST", URI, strings.NewReader(buf.String())) 129 | if err != nil { 130 | b.Fail() 131 | } 132 | req.Header.Add("Content-Type", mime) 133 | _, err = testClient.Do(req) 134 | if err != nil { 135 | b.Fail() 136 | } 137 | 138 | // Run the bench 139 | e := 0 140 | for i := 0; i < b.N; i++ { 141 | req, _ := http.NewRequest("GET", URI, nil) 142 | if res, _ := testClient.Do(req); res.StatusCode != 200 { 143 | e++ 144 | } 145 | } 146 | 147 | // delete resource 148 | req, err = http.NewRequest("DELETE", URI, nil) 149 | if err != nil { 150 | b.Fail() 151 | } 152 | _, err = testClient.Do(req) 153 | if err != nil { 154 | b.Fail() 155 | } 156 | 157 | if e > 0 { 158 | b.Log(fmt.Sprintf("%d/%d failed", e, b.N)) 159 | b.Fail() 160 | } 161 | } 162 | -------------------------------------------------------------------------------- /tokens.go: -------------------------------------------------------------------------------- 1 | package helix 2 | 3 | import ( 4 | "crypto/sha256" 5 | "encoding/json" 6 | "errors" 7 | "fmt" 8 | "net/http" 9 | "strconv" 10 | "time" 11 | 12 | "github.com/boltdb/bolt" 13 | "github.com/deiu/webid-rsa" 14 | "github.com/gocraft/web" 15 | ) 16 | 17 | var ( 18 | tokenDuration = time.Hour * 5040 19 | ) 20 | 21 | func tokenDateIsValid(valid string) error { 22 | v, err := strconv.ParseInt(valid, 10, 64) 23 | if err != nil { 24 | return err 25 | } 26 | if time.Now().Local().UnixNano() > v { 27 | return errors.New("Token has expired!") 28 | } 29 | 30 | return nil 31 | } 32 | 33 | func (ctx *Context) getAuthzUserFromToken(token, host, origin string) (string, error) { 34 | values, err := ctx.getPersistedToken("Authorization", host, token) 35 | if err != nil { 36 | return "", err 37 | } 38 | if len(values["webid"]) == 0 || len(values["valid"]) == 0 || len(values["origin"]) == 0 { 39 | return "", errors.New("Token is missing required values") 40 | } 41 | err = tokenDateIsValid(values["valid"]) 42 | if err != nil { 43 | return "", err 44 | } 45 | if origin != values["origin"] { 46 | return "", errors.New("Cannot authorize user: " + values["webid"] + ". Origin: " + origin + " does not match the origin in the token: " + values["origin"]) 47 | } 48 | return values["webid"], nil 49 | } 50 | 51 | func (c *Context) newAuthzToken(w web.ResponseWriter, req *http.Request, user string) { 52 | values := map[string]string{ 53 | "webid": user, 54 | "origin": webidrsa.GetOrigin(req), 55 | } 56 | token, err := c.newPersistedToken("Authorization", req.Host, values) 57 | if err != nil { 58 | logger.Info().Msg(err.Error()) 59 | return 60 | } 61 | w.Header().Set("Token", token) 62 | } 63 | 64 | // newPersistedToken saves an API token to the bolt db. It returns the API token and a possible error 65 | func (ctx *Context) newPersistedToken(tokenType, host string, values map[string]string) (string, error) { 66 | var token string 67 | if len(tokenType) == 0 { 68 | return token, errors.New("Missing token type when trying to generate new token") 69 | } 70 | // bucket(host) -> bucket(type) -> values 71 | err := ctx.Config.BoltDB.Update(func(tx *bolt.Tx) error { 72 | userBucket, err := tx.CreateBucketIfNotExists([]byte(host)) 73 | if err != nil { 74 | return err 75 | } 76 | bucket, err := userBucket.CreateBucketIfNotExists([]byte(tokenType)) 77 | id, _ := bucket.NextSequence() 78 | values["id"] = fmt.Sprintf("%d", id) 79 | // set validity if not alreay set 80 | if len(values["valid"]) == 0 { 81 | // age times the duration of 6 month 82 | values["valid"] = fmt.Sprintf("%d", 83 | time.Now().Add(time.Duration(ctx.Config.TokenAge)*tokenDuration).UnixNano()) 84 | } 85 | // marshal values to JSON; this will never error since we only marshal strings 86 | tokenJson, _ := json.Marshal(values) 87 | token = fmt.Sprintf("%x", sha256.Sum256(tokenJson)) 88 | bucket.Put([]byte(token), tokenJson) 89 | 90 | return nil 91 | }) 92 | 93 | return token, err 94 | } 95 | 96 | func (ctx *Context) getPersistedToken(tokenType, host, token string) (map[string]string, error) { 97 | tokenValues := map[string]string{} 98 | if len(tokenType) == 0 || len(host) == 0 || len(token) == 0 { 99 | return tokenValues, errors.New("Can't retrieve token from db. tokenType, host and token value are requrired.") 100 | } 101 | err := ctx.Config.BoltDB.View(func(tx *bolt.Tx) error { 102 | userBucket := tx.Bucket([]byte(host)) 103 | if userBucket == nil { 104 | return errors.New(host + " bucket not found!") 105 | } 106 | bucket := userBucket.Bucket([]byte(tokenType)) 107 | if bucket == nil { 108 | return errors.New(tokenType + " bucket not found!") 109 | } 110 | 111 | // unmarshal 112 | b := bucket.Get([]byte(token)) 113 | err := json.Unmarshal(b, &tokenValues) 114 | return err 115 | }) 116 | return tokenValues, err 117 | } 118 | 119 | func (ctx *Context) getTokenByOrigin(tokenType, host, origin string) (string, error) { 120 | token := "" 121 | if len(tokenType) == 0 || len(host) == 0 || len(origin) == 0 { 122 | return token, errors.New("Can't retrieve token from db. tokenType, host and token value are requrired.") 123 | } 124 | err := ctx.Config.BoltDB.View(func(tx *bolt.Tx) error { 125 | userBucket := tx.Bucket([]byte(host)) 126 | if userBucket == nil { 127 | return errors.New(host + " bucket not found!") 128 | } 129 | bucket := userBucket.Bucket([]byte(tokenType)) 130 | if bucket == nil { 131 | return errors.New(tokenType + " bucket not found!") 132 | } 133 | 134 | // unmarshal 135 | c := bucket.Cursor() 136 | 137 | for k, _ := c.First(); k != nil; k, _ = c.Next() { 138 | key := string(k) 139 | values, err := ctx.getPersistedToken(tokenType, host, key) 140 | if err == nil && values["origin"] == origin { 141 | token = key 142 | break 143 | } 144 | } 145 | 146 | return nil 147 | }) 148 | return token, err 149 | } 150 | 151 | func (ctx *Context) deletePersistedToken(tokenType, host, token string) error { 152 | if len(tokenType) == 0 || len(host) == 0 || len(token) == 0 { 153 | return errors.New("Can't retrieve token from db. tokenType, host and token value are requrired.") 154 | } 155 | err := ctx.Config.BoltDB.Update(func(tx *bolt.Tx) error { 156 | b := tx.Bucket([]byte(host)) 157 | if b == nil { 158 | return errors.New("No bucket for host " + host) 159 | } 160 | bucket := b.Bucket([]byte(tokenType)) 161 | if bucket == nil { 162 | return errors.New("No bucket for token type " + tokenType) 163 | } 164 | 165 | return bucket.Delete([]byte(token)) 166 | }) 167 | return err 168 | } 169 | 170 | func (ctx *Context) getTokensByType(tokenType, host string) (map[string]map[string]string, error) { 171 | tokens := make(map[string]map[string]string) 172 | err := ctx.Config.BoltDB.View(func(tx *bolt.Tx) error { 173 | // Assume bucket exists and has keys 174 | b := tx.Bucket([]byte(host)) 175 | if b == nil { 176 | return errors.New("No bucket for host " + host) 177 | } 178 | ba := b.Bucket([]byte(tokenType)) 179 | if ba == nil { 180 | return errors.New("No bucket for type " + tokenType) 181 | } 182 | 183 | c := ba.Cursor() 184 | 185 | for k, _ := c.First(); k != nil; k, _ = c.Next() { 186 | key := string(k) 187 | token, err := ctx.getPersistedToken(tokenType, host, key) 188 | if err == nil { 189 | tokens[key] = token 190 | } 191 | } 192 | return nil 193 | }) 194 | return tokens, err 195 | } 196 | -------------------------------------------------------------------------------- /account_test.go: -------------------------------------------------------------------------------- 1 | package helix 2 | 3 | import ( 4 | "net/http" 5 | "net/url" 6 | "os" 7 | "strings" 8 | "testing" 9 | 10 | "github.com/stretchr/testify/assert" 11 | ) 12 | 13 | func Test_GetAccountHandler(t *testing.T) { 14 | req, err := http.NewRequest("GET", testServer.URL+"/account/", nil) 15 | assert.NoError(t, err) 16 | res, err := testClient.Do(req) 17 | assert.NoError(t, err) 18 | assert.Equal(t, http.StatusUnauthorized, res.StatusCode) 19 | } 20 | 21 | func Test_GetAccountHandlerBadAuthz(t *testing.T) { 22 | req, err := http.NewRequest("GET", testServer.URL+"/account/", nil) 23 | assert.NoError(t, err) 24 | res, err := testClient.Do(req) 25 | assert.NoError(t, err) 26 | res.Body.Close() 27 | assert.Equal(t, http.StatusUnauthorized, res.StatusCode) 28 | 29 | req, err = http.NewRequest("GET", testServer.URL+"/account/", nil) 30 | assert.NoError(t, err) 31 | req.Header.Set("Authorization", res.Header.Get("WWW-Authenticate")) 32 | res, err = testClient.Do(req) 33 | assert.NoError(t, err) 34 | assert.Equal(t, http.StatusUnauthorized, res.StatusCode) 35 | } 36 | 37 | func Test_AccountIntegration(t *testing.T) { 38 | config := NewConfig() 39 | config.StaticDir = testDir 40 | 41 | boltpath, err := newTempFile(config.StaticDir, "tmpbolt") 42 | assert.NoError(t, err) 43 | config.BoltPath = boltpath 44 | 45 | err = config.StartBolt() 46 | assert.NoError(t, err) 47 | 48 | ts, err := newTestServer(config) 49 | assert.NoError(t, err) 50 | 51 | req, err := http.NewRequest("POST", ts.URL+"/account/new", nil) 52 | assert.NoError(t, err) 53 | req.Header.Add("Content-Type", "application/x-www-form-urlencoded") 54 | res, err := testClient.Do(req) 55 | assert.NoError(t, err) 56 | res.Body.Close() 57 | assert.Equal(t, http.StatusBadRequest, res.StatusCode) 58 | 59 | form := url.Values{} 60 | form.Add("username", testUser) 61 | 62 | req, err = http.NewRequest("POST", ts.URL+"/account/new", strings.NewReader(form.Encode())) 63 | assert.NoError(t, err) 64 | req.Header.Add("Content-Type", "application/x-www-form-urlencoded") 65 | res, err = testClient.Do(req) 66 | assert.NoError(t, err) 67 | res.Body.Close() 68 | assert.Equal(t, http.StatusBadRequest, res.StatusCode) 69 | 70 | form.Add("password", testPass) 71 | form.Add("email", testEmail) 72 | 73 | req, err = http.NewRequest("POST", ts.URL+"/account/new", strings.NewReader(form.Encode())) 74 | assert.NoError(t, err) 75 | req.Header.Add("Content-Type", "application/x-www-form-urlencoded") 76 | res, err = testClient.Do(req) 77 | assert.NoError(t, err) 78 | res.Body.Close() 79 | assert.Equal(t, http.StatusOK, res.StatusCode) 80 | 81 | req, err = http.NewRequest("POST", ts.URL+"/account/login", strings.NewReader(form.Encode())) 82 | assert.NoError(t, err) 83 | req.Header.Add("Content-Type", "application/x-www-form-urlencoded") 84 | res, err = testClient.Do(req) 85 | assert.NoError(t, err) 86 | res.Body.Close() 87 | assert.Equal(t, http.StatusOK, res.StatusCode) 88 | assert.Equal(t, testUser, res.Header.Get("User")) 89 | 90 | token := res.Header.Get("Token") 91 | assert.NotEmpty(t, token) 92 | 93 | req, err = http.NewRequest("GET", ts.URL+"/account/", nil) 94 | assert.NoError(t, err) 95 | req.Header.Set("Authorization", "Bearer "+token) 96 | res, err = testClient.Do(req) 97 | assert.NoError(t, err) 98 | res.Body.Close() 99 | assert.Equal(t, http.StatusOK, res.StatusCode) 100 | 101 | req, err = http.NewRequest("POST", ts.URL+"/account/logout", nil) 102 | assert.NoError(t, err) 103 | res, err = testClient.Do(req) 104 | assert.NoError(t, err) 105 | res.Body.Close() 106 | assert.Equal(t, http.StatusUnauthorized, res.StatusCode) 107 | 108 | req, err = http.NewRequest("POST", ts.URL+"/account/logout", nil) 109 | assert.NoError(t, err) 110 | req.Header.Set("Authorization", "Bearer") 111 | res, err = testClient.Do(req) 112 | assert.NoError(t, err) 113 | res.Body.Close() 114 | assert.Equal(t, http.StatusUnauthorized, res.StatusCode) 115 | 116 | req, err = http.NewRequest("POST", ts.URL+"/account/logout", nil) 117 | assert.NoError(t, err) 118 | req.Header.Set("Authorization", "Bearer "+token) 119 | res, err = testClient.Do(req) 120 | assert.NoError(t, err) 121 | res.Body.Close() 122 | assert.Equal(t, http.StatusOK, res.StatusCode) 123 | 124 | req, err = http.NewRequest("POST", ts.URL+"/account/delete", nil) 125 | assert.NoError(t, err) 126 | res, err = testClient.Do(req) 127 | assert.NoError(t, err) 128 | res.Body.Close() 129 | assert.Equal(t, http.StatusUnauthorized, res.StatusCode) 130 | 131 | req, err = http.NewRequest("POST", ts.URL+"/account/delete", nil) 132 | assert.NoError(t, err) 133 | req.Header.Set("Authorization", "Bearer "+token) 134 | res, err = testClient.Do(req) 135 | assert.NoError(t, err) 136 | res.Body.Close() 137 | assert.Equal(t, http.StatusOK, res.StatusCode) 138 | 139 | req, err = http.NewRequest("POST", ts.URL+"/account/delete", nil) 140 | assert.NoError(t, err) 141 | req.Header.Set("Authorization", "Bearer "+token) 142 | res, err = testClient.Do(req) 143 | assert.NoError(t, err) 144 | res.Body.Close() 145 | assert.Equal(t, http.StatusInternalServerError, res.StatusCode) 146 | 147 | boltCleanup(config) 148 | } 149 | 150 | func Test_LoginBad(t *testing.T) { 151 | form := url.Values{} 152 | form.Add("username", testUser) 153 | 154 | req, err := http.NewRequest("POST", testServer.URL+"/account/login", nil) 155 | assert.NoError(t, err) 156 | req.Header.Add("Content-Type", "application/x-www-form-urlencoded") 157 | res, err := testClient.Do(req) 158 | assert.NoError(t, err) 159 | res.Body.Close() 160 | assert.Equal(t, http.StatusUnauthorized, res.StatusCode) 161 | assert.Empty(t, res.Header.Get("User")) 162 | 163 | req, err = http.NewRequest("POST", testServer.URL+"/account/login", strings.NewReader(form.Encode())) 164 | assert.NoError(t, err) 165 | req.Header.Add("Content-Type", "application/x-www-form-urlencoded") 166 | res, err = testClient.Do(req) 167 | assert.NoError(t, err) 168 | res.Body.Close() 169 | assert.Equal(t, http.StatusUnauthorized, res.StatusCode) 170 | assert.Empty(t, res.Header.Get("User")) 171 | } 172 | 173 | func Test_AddUser(t *testing.T) { 174 | ctx := NewContext() 175 | ctx.Config = NewConfig() 176 | 177 | err := ctx.addUser("", "", "") 178 | assert.Error(t, err) 179 | 180 | err = ctx.addUser(testUser, "", "") 181 | assert.Error(t, err) 182 | 183 | err = ctx.addUser(testUser, testPass, "") 184 | assert.Error(t, err) 185 | 186 | err = ctx.addUser("", testPass, "") 187 | assert.Error(t, err) 188 | 189 | err = ctx.addUser("", "", testEmail) 190 | assert.Error(t, err) 191 | 192 | err = ctx.addUser(testUser, testPass, testEmail) 193 | assert.Error(t, err) 194 | 195 | err = ctx.Config.StartBolt() 196 | assert.NoError(t, err) 197 | 198 | err = ctx.addUser(testUser, testPass, testEmail) 199 | assert.NoError(t, err) 200 | 201 | user, err := ctx.getUser(testUser) 202 | assert.NoError(t, err) 203 | assert.Equal(t, testUser, user.Username) 204 | assert.Equal(t, testEmail, user.Email) 205 | 206 | boltCleanup(ctx.Config) 207 | } 208 | 209 | func Test_DeleteUser(t *testing.T) { 210 | ctx := NewContext() 211 | ctx.Config = NewConfig() 212 | 213 | err := ctx.Config.StartBolt() 214 | assert.NoError(t, err) 215 | 216 | err = ctx.addUser(testUser, testPass, testEmail) 217 | assert.NoError(t, err) 218 | 219 | err = ctx.deleteUser(testUser) 220 | assert.NoError(t, err) 221 | 222 | boltCleanup(ctx.Config) 223 | } 224 | 225 | func Test_SaveUserFail(t *testing.T) { 226 | ctx := NewContext() 227 | ctx.Config = NewConfig() 228 | 229 | err := ctx.Config.StartBolt() 230 | assert.NoError(t, err) 231 | 232 | user := NewUser() 233 | 234 | err = ctx.saveUser(user) 235 | assert.Error(t, err) 236 | 237 | boltCleanup(ctx.Config) 238 | } 239 | 240 | func Test_GetUser(t *testing.T) { 241 | ctx := NewContext() 242 | ctx.Config = NewConfig() 243 | 244 | err := ctx.Config.StartBolt() 245 | assert.NoError(t, err) 246 | 247 | err = ctx.addUser(testUser, testPass, testEmail) 248 | assert.NoError(t, err) 249 | 250 | _, err = ctx.getUser("foo") 251 | assert.Error(t, err) 252 | 253 | user, err := ctx.getUser(testUser) 254 | assert.NoError(t, err) 255 | assert.Equal(t, testUser, user.Username) 256 | assert.Equal(t, testEmail, user.Email) 257 | 258 | boltCleanup(ctx.Config) 259 | } 260 | 261 | func boltCleanup(cfg *Config) { 262 | cfg.BoltDB.Close() 263 | err := os.Remove(cfg.BoltPath) 264 | if err != nil { 265 | panic("Failed to remove " + cfg.BoltPath) 266 | } 267 | } 268 | -------------------------------------------------------------------------------- /tokens_test.go: -------------------------------------------------------------------------------- 1 | package helix 2 | 3 | import ( 4 | "bufio" 5 | "fmt" 6 | "net" 7 | "net/http" 8 | "net/http/httptest" 9 | "testing" 10 | "time" 11 | 12 | "github.com/gocraft/web" 13 | "github.com/stretchr/testify/assert" 14 | ) 15 | 16 | type testWriter struct { 17 | http.ResponseWriter 18 | statusCode int 19 | size int 20 | } 21 | 22 | func Test_GetAuthzUserFromToken(t *testing.T) { 23 | ctx := NewContext() 24 | ctx.Config = NewConfig() 25 | ctx.Config.StaticDir = testDir 26 | 27 | boltpath, err := newTempFile(ctx.Config.StaticDir, "tmpbolt") 28 | assert.NoError(t, err) 29 | ctx.Config.BoltPath = boltpath 30 | 31 | err = ctx.Config.StartBolt() 32 | assert.NoError(t, err) 33 | 34 | tokenType := "Authorization" 35 | host := "localhost" 36 | origin := "example.org" 37 | values := map[string]string{ 38 | "webid": testUser, 39 | } 40 | 41 | token, err := ctx.newPersistedToken(tokenType, host, values) 42 | assert.NoError(t, err) 43 | assert.NotEmpty(t, token) 44 | 45 | _, err = ctx.getAuthzUserFromToken("", host, origin) 46 | assert.Error(t, err) 47 | 48 | _, err = ctx.getAuthzUserFromToken(token, "", origin) 49 | assert.Error(t, err) 50 | 51 | _, err = ctx.getAuthzUserFromToken(token, host, "") 52 | assert.Error(t, err) 53 | 54 | webid, err := ctx.getAuthzUserFromToken(token, host, "foo") 55 | assert.Error(t, err) 56 | assert.Empty(t, webid) 57 | 58 | webid, err = ctx.getAuthzUserFromToken(token, host, origin) 59 | assert.Error(t, err) 60 | assert.Empty(t, webid) 61 | 62 | values["origin"] = "example.org" 63 | token, err = ctx.newPersistedToken(tokenType, host, values) 64 | assert.NoError(t, err) 65 | assert.NotEmpty(t, token) 66 | 67 | _, err = ctx.getAuthzUserFromToken(token, host, "foo") 68 | assert.Error(t, err) 69 | 70 | webid, err = ctx.getAuthzUserFromToken(token, host, origin) 71 | assert.NoError(t, err) 72 | assert.Equal(t, testUser, webid) 73 | 74 | values["valid"] = fmt.Sprintf("%d", time.Now().Add(time.Duration(1)*time.Microsecond).UnixNano()) 75 | token, err = ctx.newPersistedToken(tokenType, host, values) 76 | assert.NoError(t, err) 77 | assert.NotEmpty(t, token) 78 | 79 | time.Sleep(time.Millisecond * 1) 80 | 81 | webid, err = ctx.getAuthzUserFromToken(token, host, origin) 82 | assert.Error(t, err) 83 | assert.Empty(t, webid) 84 | 85 | boltCleanup(ctx.Config) 86 | } 87 | 88 | func Test_PersistedTokens(t *testing.T) { 89 | ctx := NewContext() 90 | ctx.Config = NewConfig() 91 | ctx.Config.StaticDir = testDir 92 | 93 | boltpath, err := newTempFile(ctx.Config.StaticDir, "tmpbolt") 94 | assert.NoError(t, err) 95 | ctx.Config.BoltPath = boltpath 96 | 97 | err = ctx.Config.StartBolt() 98 | assert.NoError(t, err) 99 | 100 | tokenType := "Authorization" 101 | host := "localhost" 102 | origin := "example.org" 103 | values := map[string]string{ 104 | "webid": testUser, 105 | "origin": origin, 106 | } 107 | 108 | _, err = ctx.newPersistedToken("", host, values) 109 | assert.Error(t, err) 110 | 111 | _, err = ctx.newPersistedToken(tokenType, "", values) 112 | assert.Error(t, err) 113 | 114 | token, err := ctx.newPersistedToken(tokenType, host, values) 115 | assert.NoError(t, err) 116 | assert.NotEmpty(t, token) 117 | 118 | _, err = ctx.getPersistedToken("", host, token) 119 | assert.Error(t, err) 120 | 121 | _, err = ctx.getPersistedToken(tokenType, "", token) 122 | assert.Error(t, err) 123 | 124 | _, err = ctx.getPersistedToken(tokenType, host, "") 125 | assert.Error(t, err) 126 | 127 | _, err = ctx.getPersistedToken("bar", host, token) 128 | assert.Error(t, err) 129 | 130 | _, err = ctx.getPersistedToken(tokenType, "foo", token) 131 | assert.Error(t, err) 132 | 133 | vals, err := ctx.getPersistedToken(tokenType, host, token) 134 | assert.NoError(t, err) 135 | assert.Equal(t, values["webid"], vals["webid"]) 136 | assert.Equal(t, values["origin"], vals["origin"]) 137 | 138 | _, err = ctx.getTokenByOrigin("", host, origin) 139 | assert.Error(t, err) 140 | 141 | _, err = ctx.getTokenByOrigin(tokenType, "", origin) 142 | assert.Error(t, err) 143 | 144 | _, err = ctx.getTokenByOrigin(tokenType, host, "") 145 | assert.Error(t, err) 146 | 147 | _, err = ctx.getTokenByOrigin("foo", host, origin) 148 | assert.Error(t, err) 149 | 150 | _, err = ctx.getTokenByOrigin(tokenType, "bar", origin) 151 | assert.Error(t, err) 152 | 153 | tkn, err := ctx.getTokenByOrigin(tokenType, host, "test.com") 154 | assert.NoError(t, err) 155 | assert.Empty(t, tkn) 156 | 157 | tkn, err = ctx.getTokenByOrigin(tokenType, host, origin) 158 | assert.NoError(t, err) 159 | assert.Equal(t, token, tkn) 160 | 161 | _, err = ctx.getTokensByType("", host) 162 | assert.Error(t, err) 163 | 164 | _, err = ctx.getTokensByType("foo", host) 165 | assert.Error(t, err) 166 | 167 | _, err = ctx.getTokensByType(tokenType, "baz") 168 | assert.Error(t, err) 169 | 170 | _, err = ctx.getTokensByType(tokenType, "") 171 | assert.Error(t, err) 172 | 173 | tokens, err := ctx.getTokensByType(tokenType, host) 174 | assert.NoError(t, err) 175 | assert.NotEqual(t, 0, len(tokens)) 176 | assert.Equal(t, tokens[token]["webid"], values["webid"]) 177 | assert.Equal(t, tokens[token]["origin"], values["origin"]) 178 | 179 | err = ctx.deletePersistedToken("", host, token) 180 | assert.Error(t, err) 181 | 182 | err = ctx.deletePersistedToken("foo", host, token) 183 | assert.Error(t, err) 184 | 185 | err = ctx.deletePersistedToken(tokenType, "", token) 186 | assert.Error(t, err) 187 | 188 | err = ctx.deletePersistedToken(tokenType, "foo", token) 189 | assert.Error(t, err) 190 | 191 | err = ctx.deletePersistedToken(tokenType, host, "") 192 | assert.Error(t, err) 193 | 194 | err = ctx.deletePersistedToken(tokenType, host, token) 195 | assert.NoError(t, err) 196 | 197 | boltCleanup(ctx.Config) 198 | } 199 | 200 | func Test_NewAuthzToken(t *testing.T) { 201 | ctx := NewContext() 202 | ctx.Config = NewConfig() 203 | ctx.Config.StaticDir = testDir 204 | 205 | boltpath, err := newTempFile(ctx.Config.StaticDir, "tmpbolt") 206 | assert.NoError(t, err) 207 | ctx.Config.BoltPath = boltpath 208 | 209 | err = ctx.Config.StartBolt() 210 | assert.NoError(t, err) 211 | 212 | err = ctx.addUser(testUser, testPass, testEmail) 213 | assert.NoError(t, err) 214 | 215 | req, _ := http.NewRequest("POST", testServer.URL, nil) 216 | rec := httptest.NewRecorder() 217 | w := web.ResponseWriter(&testWriter{ResponseWriter: rec}) 218 | 219 | ctx.newAuthzToken(w, req, testUser) 220 | 221 | assert.Equal(t, w.Header().Get("Token"), rec.Header().Get("Token")) 222 | 223 | ctx.Config.BoltDB.Close() 224 | 225 | rec = httptest.NewRecorder() 226 | w = web.ResponseWriter(&testWriter{ResponseWriter: rec}) 227 | 228 | ctx.newAuthzToken(w, req, testUser) 229 | assert.Empty(t, rec.Header().Get("Token")) 230 | 231 | boltCleanup(ctx.Config) 232 | } 233 | 234 | func Test_TokenDateIsValid(t *testing.T) { 235 | err := tokenDateIsValid("") 236 | assert.Error(t, err) 237 | 238 | valid := fmt.Sprintf("%d", time.Now().Add(time.Duration(1)*time.Microsecond).UnixNano()) 239 | time.Sleep(time.Millisecond * 1) 240 | 241 | err = tokenDateIsValid(valid) 242 | assert.Error(t, err) 243 | 244 | valid = fmt.Sprintf("%d", time.Now().Add(time.Duration(1)*time.Millisecond).UnixNano()) 245 | time.Sleep(time.Microsecond * 1) 246 | 247 | err = tokenDateIsValid(valid) 248 | assert.NoError(t, err) 249 | } 250 | 251 | // Don't need this yet because we get it for free: 252 | func (w *testWriter) Write(data []byte) (n int, err error) { 253 | if w.statusCode == 0 { 254 | w.statusCode = http.StatusOK 255 | } 256 | size, err := w.ResponseWriter.Write(data) 257 | w.size += size 258 | return size, err 259 | } 260 | 261 | func (w *testWriter) WriteHeader(statusCode int) { 262 | w.statusCode = statusCode 263 | w.ResponseWriter.WriteHeader(statusCode) 264 | } 265 | 266 | func (w *testWriter) StatusCode() int { 267 | return w.statusCode 268 | } 269 | 270 | func (w *testWriter) Written() bool { 271 | return w.statusCode != 0 272 | } 273 | 274 | func (w *testWriter) Size() int { 275 | return w.size 276 | } 277 | 278 | func (w *testWriter) Hijack() (net.Conn, *bufio.ReadWriter, error) { 279 | hijacker, ok := w.ResponseWriter.(http.Hijacker) 280 | if !ok { 281 | return nil, nil, fmt.Errorf("the ResponseWriter doesn't support the Hijacker interface") 282 | } 283 | return hijacker.Hijack() 284 | } 285 | 286 | func (w *testWriter) CloseNotify() <-chan bool { 287 | return w.ResponseWriter.(http.CloseNotifier).CloseNotify() 288 | } 289 | 290 | func (w *testWriter) Flush() { 291 | flusher, ok := w.ResponseWriter.(http.Flusher) 292 | if ok { 293 | flusher.Flush() 294 | } 295 | } 296 | --------------------------------------------------------------------------------