├── 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 | [](https://travis-ci.org/deiu/helix)
4 | [](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 |
--------------------------------------------------------------------------------