├── .gitignore ├── .travis.yml ├── CHANGES ├── Dockerfile ├── Gopkg.lock ├── Gopkg.toml ├── Makefile ├── README.md ├── VERSION ├── acl.go ├── acl_test.go ├── auth.go ├── auth_test.go ├── autoneg.go ├── autoneg_test.go ├── config.go ├── cors_test.go ├── crypto.go ├── crypto_test.go ├── gold.conf-example ├── graph.go ├── graph_test.go ├── init.go ├── ldp.go ├── ldp_test.go ├── locks.go ├── mime.go ├── mime_test.go ├── pathinfo.go ├── pathinfo_test.go ├── pkg ├── apps │ └── apps.go └── routes │ └── routes.go ├── proxy.go ├── proxy_test.go ├── push.sh ├── rdf.go ├── rdf_test.go ├── server.go ├── server ├── daemon.go └── tls.go ├── server_test.go ├── smtp.go ├── smtp_test.go ├── sparqlupdate.go ├── sparqlupdate_test.go ├── spkac.go ├── spkac_test.go ├── statics ├── 404.html └── popup.html ├── system.go ├── system_test.go ├── templates.go ├── templates ├── 404.html └── tabulator.html ├── term.go ├── term_test.go ├── tests └── img.jpg ├── triple.go ├── triple_test.go ├── webid.go ├── webid_test.go ├── websocket.go └── websocket_test.go /.gitignore: -------------------------------------------------------------------------------- 1 | _bench 2 | coverage.out 3 | gold.conf 4 | .idea 5 | vendor 6 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | 3 | go: 4 | - 1.10.x 5 | 6 | before_install: 7 | - sudo apt-get update -qq 8 | - sudo apt-get install -qq libraptor2-dev libmagic-dev 9 | - go get -u github.com/golang/dep/... 10 | - dep ensure 11 | 12 | script: 13 | - go test ./... 14 | 15 | notifications: 16 | webhooks: 17 | urls: 18 | - https://webhooks.gitter.im/e/436c5c3b940207e4a069 19 | on_success: change # options: [always|never|change] default: always 20 | on_failure: always # options: [always|never|change] default: always 21 | on_start: false # default: false 22 | 23 | -------------------------------------------------------------------------------- /CHANGES: -------------------------------------------------------------------------------- 1 | Version 1.0.1: 2 | - Minor fix 3 | 4 | Version 1.0.0: 5 | 6 | 7 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang 2 | 3 | RUN \ 4 | apt-get update -y && \ 5 | apt-get install -y libraptor2-dev libmagic-dev && \ 6 | rm -rf /var/lib/apt/lists/* && \ 7 | go get -u -x github.com/linkeddata/gold/server 8 | 9 | EXPOSE 443 10 | EXPOSE 80 11 | VOLUME ["/data"] 12 | ENV TMPDIR="/tmp" 13 | 14 | CMD ["server", "-https=:443", "-http=:80", "-root=/data/", "-boltPath=/tmp/bolt.db", "-debug"] 15 | -------------------------------------------------------------------------------- /Gopkg.lock: -------------------------------------------------------------------------------- 1 | # This file is autogenerated, do not edit; changes may be undone by the next 'dep ensure'. 2 | 3 | 4 | [[projects]] 5 | digest = "1:0f98f59e9a2f4070d66f0c9c39561f68fcd1dc837b22a852d28d0003aebd1b1e" 6 | name = "github.com/boltdb/bolt" 7 | packages = ["."] 8 | pruneopts = "UT" 9 | revision = "2f1ce7a837dcb8da3ec595b1dac9d0632f0f99e8" 10 | version = "v1.3.1" 11 | 12 | [[projects]] 13 | digest = "1:ffe9824d294da03b391f44e1ae8281281b4afc1bdaa9588c9097785e3af10cec" 14 | name = "github.com/davecgh/go-spew" 15 | packages = ["spew"] 16 | pruneopts = "UT" 17 | revision = "8991bc29aa16c548c550c7ff78260e27b9ab7c73" 18 | version = "v1.1.1" 19 | 20 | [[projects]] 21 | digest = "1:2b7b174ae68705866555b73fd848de0749b93b1f99e3295e27f89bebe8702203" 22 | name = "github.com/elazarl/goproxy" 23 | packages = ["."] 24 | pruneopts = "UT" 25 | revision = "947c36da3153ff334e74d9d980de341d25f358ba" 26 | version = "v1.1" 27 | 28 | [[projects]] 29 | branch = "master" 30 | digest = "1:d4d1295925d5dfab49954082388dd3556860fa2daa70df9775a6d5bfec6c5054" 31 | name = "github.com/gabriel-vasile/mimetype" 32 | packages = [ 33 | ".", 34 | "matchers", 35 | ] 36 | pruneopts = "UT" 37 | revision = "d4f1602c882009c84583421e8f6e124d3eca176e" 38 | 39 | [[projects]] 40 | digest = "1:e72d1ebb8d395cf9f346fd9cbc652e5ae222dd85e0ac842dc57f175abed6d195" 41 | name = "github.com/gorilla/securecookie" 42 | packages = ["."] 43 | pruneopts = "UT" 44 | revision = "e59506cc896acb7f7bf732d4fdf5e25f7ccd8983" 45 | version = "v1.1.1" 46 | 47 | [[projects]] 48 | branch = "master" 49 | digest = "1:b7d73b96db1c3cd12fcf58c80c4226784cca158b96b8e7d2db0795c3dcfb4c88" 50 | name = "github.com/linkeddata/gojsonld" 51 | packages = ["."] 52 | pruneopts = "UT" 53 | revision = "4f5db6791326b8962ede4edbba693edcf20fd1ad" 54 | 55 | [[projects]] 56 | digest = "1:0028cb19b2e4c3112225cd871870f2d9cf49b9b4276531f03438a88e94be86fe" 57 | name = "github.com/pmezard/go-difflib" 58 | packages = ["difflib"] 59 | pruneopts = "UT" 60 | revision = "792786c7400a136282c1664665ae0a8db921c6c2" 61 | version = "v1.0.0" 62 | 63 | [[projects]] 64 | branch = "master" 65 | digest = "1:cb671a4d71ede584c1584051326929ba4fb0d0653650c26a24df04e4b6e7060c" 66 | name = "github.com/presbrey/goraptor" 67 | packages = ["."] 68 | pruneopts = "UT" 69 | revision = "d14aff371f65d19049be51de3b604f22a2ca853e" 70 | 71 | [[projects]] 72 | digest = "1:18752d0b95816a1b777505a97f71c7467a8445b8ffb55631a7bf779f6ba4fa83" 73 | name = "github.com/stretchr/testify" 74 | packages = ["assert"] 75 | pruneopts = "UT" 76 | revision = "f35b8ab0b5a2cef36673838d662e249dd9c94686" 77 | version = "v1.2.2" 78 | 79 | [[projects]] 80 | branch = "master" 81 | digest = "1:da939eb838c6b235621e8551297b050c2e32f1b5400ca6bab0ced87c7827ed81" 82 | name = "golang.org/x/net" 83 | packages = [ 84 | "context", 85 | "webdav", 86 | "webdav/internal/xml", 87 | "websocket", 88 | ] 89 | pruneopts = "UT" 90 | revision = "4dfa2610cdf3b287375bbba5b8f2a14d3b01d8de" 91 | 92 | [[projects]] 93 | branch = "master" 94 | digest = "1:e2ced6fdf654069e8713c96012275fbd245381673d53e8c8ace640b28441a28b" 95 | name = "golang.org/x/sys" 96 | packages = ["unix"] 97 | pruneopts = "UT" 98 | revision = "e4b3c5e9061176387e7cea65e4dc5853801f3fb7" 99 | 100 | [solve-meta] 101 | analyzer-name = "dep" 102 | analyzer-version = 1 103 | input-imports = [ 104 | "github.com/boltdb/bolt", 105 | "github.com/elazarl/goproxy", 106 | "github.com/gabriel-vasile/mimetype", 107 | "github.com/gorilla/securecookie", 108 | "github.com/linkeddata/gojsonld", 109 | "github.com/presbrey/goraptor", 110 | "github.com/stretchr/testify/assert", 111 | "golang.org/x/net/webdav", 112 | "golang.org/x/net/websocket", 113 | ] 114 | solver-name = "gps-cdcl" 115 | solver-version = 1 116 | -------------------------------------------------------------------------------- /Gopkg.toml: -------------------------------------------------------------------------------- 1 | # Gopkg.toml example 2 | # 3 | # Refer to https://golang.github.io/dep/docs/Gopkg.toml.html 4 | # for detailed Gopkg.toml documentation. 5 | # 6 | # required = ["github.com/user/thing/cmd/thing"] 7 | # ignored = ["github.com/user/project/pkgX", "bitbucket.org/user/project/pkgA/pkgY"] 8 | # 9 | # [[constraint]] 10 | # name = "github.com/user/project" 11 | # version = "1.0.0" 12 | # 13 | # [[constraint]] 14 | # name = "github.com/user/project2" 15 | # branch = "dev" 16 | # source = "github.com/myfork/project2" 17 | # 18 | # [[override]] 19 | # name = "github.com/x/y" 20 | # version = "2.4.0" 21 | # 22 | # [prune] 23 | # non-go = false 24 | # go-tests = true 25 | # unused-packages = true 26 | 27 | 28 | [[constraint]] 29 | name = "github.com/boltdb/bolt" 30 | version = "1.3.1" 31 | 32 | [[constraint]] 33 | name = "github.com/elazarl/goproxy" 34 | version = "1.1.0" 35 | 36 | [[constraint]] 37 | name = "github.com/gorilla/securecookie" 38 | version = "1.1.1" 39 | 40 | [[constraint]] 41 | branch = "master" 42 | name = "github.com/linkeddata/gojsonld" 43 | 44 | [[constraint]] 45 | branch = "master" 46 | name = "github.com/presbrey/goraptor" 47 | 48 | [[constraint]] 49 | name = "github.com/stretchr/testify" 50 | version = "1.2.2" 51 | 52 | [[constraint]] 53 | branch = "master" 54 | name = "golang.org/x/net" 55 | 56 | [prune] 57 | go-tests = true 58 | unused-packages = true 59 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: test cover 2 | test: 3 | go get golang.org/x/tools/cmd/cover 4 | go test -cover -v . 5 | 6 | bench: 7 | @go test -bench . -benchmem 8 | 9 | cover: 10 | go test -coverprofile=coverage.out 11 | go tool cover -html=coverage.out 12 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # gold 2 | 3 | [![](https://img.shields.io/badge/project-Solid-7C4DFF.svg?style=flat-square)](https://github.com/solid/solid) 4 | [![Join the chat at https://gitter.im/linkeddata/gold](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/linkeddata/gold?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) 5 | 6 | `gold` is a reference Linked Data Platform server for the 7 | **[Solid platform](https://github.com/solid/solid-spec)**. 8 | 9 | Written in Go, based on 10 | [initial work done by William Waites](https://bitbucket.org/ww/gold). 11 | 12 | [![Build Status](https://travis-ci.org/linkeddata/gold.svg?branch=master)](https://travis-ci.org/linkeddata/gold) 13 | 14 | ## Installing 15 | 16 | ### From docker repository: 17 | 18 | ``` 19 | sudo docker pull linkeddata/gold 20 | sudo docker run -p ip:port:443 linkeddata/gold 21 | ``` 22 | Replace `ip` and `port` with your host computer's IP address and port number. 23 | 24 | To check the status of the container, type: 25 | 26 | ``` 27 | sudo docker ps 28 | ``` 29 | 30 | `IMPORTANT`: if you want to mount a host directory into the container, you can use the -v parameter: 31 | 32 | ``` 33 | sudo docker run -p ip:port:443 -v /home/user/data:/data linkeddata/gold 34 | ``` 35 | 36 | This will mount the host directory, `/home/user/data`, into the container as the `/data/` directory. Doing this will allow you to reuse the data directory without worrying about persistence inside the container. 37 | 38 | ### From Github: 39 | 40 | 1. Setup Go: 41 | 42 | * **Mac OS X**: `brew install go` 43 | * **Ubuntu**: `sudo apt-get install golang-go` 44 | * **Fedora**: `sudo dnf install golang` 45 | 46 | 1. Set the `GOPATH` variable (required by Go): 47 | 48 | ```bash 49 | mkdir ~/go 50 | export GOPATH=~/go 51 | ``` 52 | 53 | (Optionally consider adding `export GOPATH=~/go` to your `.bashrc` or profile). 54 | 55 | 1. Check that you have the required Go version (**Go 1.4 or later**): 56 | 57 | ``` 58 | go version 59 | ``` 60 | 61 | If you don't, please [install](http://golang.org/doc/install) a more recent 62 | version. 63 | 64 | 1. Use the `go get` command to install the server and all the dependencies: 65 | 66 | ``` 67 | go get github.com/linkeddata/gold/server 68 | ``` 69 | 70 | 1. Install dependencies: 71 | * **Mac OS X**: `brew install raptor libmagic` 72 | * **Ubuntu**: `sudo apt-get install libraptor2-dev libmagic-dev` 73 | * **Fedora**: `sudo dnf install raptor2-devel file-devel` 74 | 75 | 76 | 1. (Optional) Install extra dependencies used by the tests: 77 | 78 | ``` 79 | go get github.com/stretchr/testify/assert 80 | ``` 81 | 82 | ## Running the Server 83 | 84 | **IMPORTANT**: Among other things, `gold` is a web server. Please consider 85 | running it as a regular user instead of root. Since gold treats all files 86 | equally, and even though uploaded files are not made executable, it will not 87 | prevent clients from uploading malicious shell scripts. 88 | 89 | Pay attention to the data root parameter, `-root`. By default, it will serve 90 | files from its current directory (so, for example, if you installed it from 91 | Github, its data root will be `$GOPATH/src/github.com/linkeddata/gold/`). 92 | Otherwise, make sure to pass it a dedicated data directory to serve, either 93 | using a command-line parameter or the [config file](#configuration). 94 | Something like: `-root=/var/www/data/` or `-root=~/data/`. 95 | 96 | 1. If you installed it from package via `go get`, you can run it by: 97 | 98 | ``` 99 | $GOPATH/bin/server -http=":8080" -https=":8443" -debug 100 | ``` 101 | 102 | 2. When developing locally, you can `cd` into the repo cloned by `go get`: 103 | 104 | ``` 105 | cd $GOPATH/src/github.com/linkeddata/gold 106 | ``` 107 | 108 | And launch the server by: 109 | 110 | ``` 111 | go run server/*.go -http=":8080" -https=":8443" -debug -boltPath=/tmp/bolt.db 112 | ``` 113 | 114 | Alternatively, you can compile and run it from the source dir in one command: 115 | 116 | ``` 117 | go run $GOPATH/src/github.com/linkeddata/gold/server/*.go -http=":8080" -https=":8443" \ 118 | -root=/home/user/data/ -debug -boltPath=/tmp/bolt.db 119 | ``` 120 | 121 | 122 | ## Configuration 123 | 124 | You can use the provided `gold.conf-example` file to create your own 125 | configuration file, and specify it with the `-conf` parameter. 126 | 127 | ```bash 128 | cd $GOPATH/src/github.com/linkeddata/gold/ 129 | cp gold.conf-example server/gold.conf 130 | 131 | # edit the configuration file 132 | nano server/gold.conf 133 | 134 | # pass the config file when launching the gold server 135 | $GOPATH/bin/server -conf=$GOPATH/src/github.com/linkeddata/gold/server/gold.conf 136 | ``` 137 | 138 | To see a list of available options: 139 | 140 | ~/go/bin/server -help 141 | 142 | Some important options and defaults: 143 | 144 | * `-conf` - Optional path to a config file. 145 | 146 | * `-debug` - Outputs config parameters and extra logging. Default: `false`. 147 | 148 | * `-root` - Specifies the data root directory which `gold` will be serving. 149 | Default: `.` (so, likely to be `$GOPATH/src/github.com/linkeddata/gold/`). 150 | 151 | * `-http` - HTTP port on which the server listens. For local development, 152 | the default HTTP port, `80`, is likely to be reserved, so pass in an 153 | alternative. Default: `":80"`. Example: `-http=":8080"`. 154 | 155 | * `-https` - HTTPS port on which the server listens. For local development, 156 | the default HTTPS port, `443`, is likely to be reserved, so pass in an 157 | alternative. Default: `":443"`. Example: `-https=":8443"`. 158 | 159 | ## Testing 160 | To run the unit tests (assuming you've installed `assert` via 161 | `go get github.com/stretchr/testify/assert`): 162 | 163 | ``` 164 | make test 165 | ``` 166 | 167 | ## Notes 168 | 169 | * HOWTO : [Get an example X.509 cert](https://gist.github.com/melvincarvalho/e14753a7137d02d756f19299fed292b4) 170 | * HOWTO : [Login after getting a 401](https://gist.github.com/melvincarvalho/72eaff2fbf1b51a805846320e0bff0cc) 171 | * HOWTO : [Recover an account](https://gist.github.com/melvincarvalho/bcc04e1529dd3a4509892346109b1d37) 172 | 173 | ## License 174 | [MIT](http://joe.mit-license.org/) 175 | -------------------------------------------------------------------------------- /VERSION: -------------------------------------------------------------------------------- 1 | 1.0.1 2 | -------------------------------------------------------------------------------- /acl.go: -------------------------------------------------------------------------------- 1 | package gold 2 | 3 | import ( 4 | "errors" 5 | "log" 6 | "net/http" 7 | "strings" 8 | "time" 9 | ) 10 | 11 | // WAC WebAccessControl object 12 | type WAC struct { 13 | req *httpRequest 14 | srv *Server 15 | w http.ResponseWriter 16 | user string 17 | key string 18 | } 19 | 20 | // NewWAC creates a new WAC object 21 | func NewWAC(req *httpRequest, srv *Server, w http.ResponseWriter, user string, key string) *WAC { 22 | return &WAC{req: req, srv: srv, w: w, user: user, key: key} 23 | } 24 | 25 | // Return an HTTP code and error (200 if authd, 401 if auth required, 403 if not authorized, 500 if error) 26 | func (acl *WAC) allow(mode string, path string) (int, error) { 27 | origin := acl.req.Header.Get("Origin") 28 | accessType := "accessTo" 29 | p, err := acl.req.pathInfo(path) 30 | if err != nil { 31 | return 500, err 32 | } 33 | depth := strings.Split(p.Path, "/") 34 | 35 | for d := len(depth); d >= 0; d-- { 36 | p, err := acl.req.pathInfo(path) 37 | if err != nil { 38 | return 500, err 39 | } 40 | 41 | acl.srv.debug.Println("Checking " + accessType + " <" + mode + "> to " + p.URI + " for WebID: " + acl.user) 42 | acl.srv.debug.Println("Looking for policies in " + p.AclFile) 43 | 44 | aclGraph := NewGraph(p.AclURI) 45 | aclGraph.ReadFile(p.AclFile) 46 | if aclGraph.Len() > 0 { 47 | acl.srv.debug.Println("Found policies in " + p.AclFile) 48 | // TODO make it more elegant instead of duplicating code 49 | for _, i := range aclGraph.All(nil, ns.acl.Get("mode"), ns.acl.Get("Control")) { 50 | for range aclGraph.All(i.Subject, ns.acl.Get(accessType), NewResource(p.URI)) { 51 | //@@TODO add resourceKey to ACL vocab 52 | if len(acl.user) > 0 { 53 | acl.srv.debug.Println("Looking for policy matching user:", acl.user) 54 | for range aclGraph.All(i.Subject, ns.acl.Get("owner"), NewResource(acl.user)) { 55 | acl.srv.debug.Println(mode + " access allowed (as owner) for: " + acl.user) 56 | return 200, nil 57 | } 58 | for range aclGraph.All(i.Subject, ns.acl.Get("agent"), NewResource(acl.user)) { 59 | acl.srv.debug.Println(mode + " access allowed (as agent) for: " + acl.user) 60 | return 200, nil 61 | } 62 | } 63 | if len(acl.key) > 0 { 64 | acl.srv.debug.Println("Looking for policy matching key:", acl.key) 65 | for range aclGraph.All(i.Subject, ns.acl.Get("resourceKey"), NewLiteral(acl.key)) { 66 | acl.srv.debug.Println(mode + " access allowed based on matching resource key") 67 | return 200, nil 68 | } 69 | } 70 | for _, t := range aclGraph.All(i.Subject, ns.acl.Get("agentClass"), nil) { 71 | // check for foaf groups 72 | acl.srv.debug.Println("Found agentClass policy") 73 | if t.Object.Equal(ns.foaf.Get("Agent")) { 74 | acl.srv.debug.Println(mode + " access allowed as FOAF Agent") 75 | return 200, nil 76 | } 77 | 78 | groupURI := debrack(t.Object.String()) 79 | groupGraph := NewGraph(groupURI) 80 | groupGraph.LoadURI(groupURI) 81 | if groupGraph.Len() > 0 && groupGraph.One(t.Object, ns.rdf.Get("type"), ns.foaf.Get("Group")) != nil { 82 | for range groupGraph.All(t.Object, ns.foaf.Get("member"), NewResource(acl.user)) { 83 | acl.srv.debug.Println(acl.user + " listed as a member of the group " + groupURI) 84 | return 200, nil 85 | } 86 | } 87 | } 88 | } 89 | } 90 | for _, i := range aclGraph.All(nil, ns.acl.Get("mode"), ns.acl.Get(mode)) { 91 | acl.srv.debug.Println("Found " + accessType + " policy for <" + mode + ">") 92 | 93 | for range aclGraph.All(i.Subject, ns.acl.Get(accessType), NewResource(p.URI)) { 94 | origins := aclGraph.All(i.Subject, ns.acl.Get("origin"), nil) 95 | if len(origin) > 0 && len(origins) > 0 { 96 | acl.srv.debug.Println("Origin set to: " + brack(origin)) 97 | for _, o := range origins { 98 | if brack(origin) == o.Object.String() { 99 | acl.srv.debug.Println("Found policy for origin: " + o.Object.String()) 100 | goto allowOrigin 101 | } 102 | } 103 | continue 104 | } else { 105 | acl.srv.debug.Println("No origin found, moving on") 106 | } 107 | allowOrigin: 108 | if len(acl.user) > 0 { 109 | acl.srv.debug.Println("Looking for policy matching user:", acl.user) 110 | for range aclGraph.All(i.Subject, ns.acl.Get("owner"), NewResource(acl.user)) { 111 | acl.srv.debug.Println(mode + " access allowed (as owner) for: " + acl.user) 112 | return 200, nil 113 | } 114 | for range aclGraph.All(i.Subject, ns.acl.Get("agent"), NewResource(acl.user)) { 115 | acl.srv.debug.Println(mode + " access allowed (as agent) for: " + acl.user) 116 | return 200, nil 117 | } 118 | } 119 | if len(acl.key) > 0 { 120 | acl.srv.debug.Println("Looking for policy matching key:", acl.key) 121 | for range aclGraph.All(i.Subject, ns.acl.Get("resourceKey"), NewLiteral(acl.key)) { 122 | acl.srv.debug.Println(mode + " access allowed based on matching resource key") 123 | return 200, nil 124 | } 125 | } 126 | for _, t := range aclGraph.All(i.Subject, ns.acl.Get("agentClass"), nil) { 127 | // check for foaf groups 128 | acl.srv.debug.Println("Found agentClass policy") 129 | if t.Object.Equal(ns.foaf.Get("Agent")) { 130 | acl.srv.debug.Println(mode + " access allowed as FOAF Agent") 131 | return 200, nil 132 | } 133 | groupURI := debrack(t.Object.String()) 134 | groupGraph := NewGraph(groupURI) 135 | groupGraph.LoadURI(groupURI) 136 | if groupGraph.Len() > 0 && groupGraph.One(t.Object, ns.rdf.Get("type"), ns.foaf.Get("Group")) != nil { 137 | for range groupGraph.All(t.Object, ns.foaf.Get("member"), NewResource(acl.user)) { 138 | acl.srv.debug.Println(acl.user + " listed as a member of the group " + groupURI) 139 | return 200, nil 140 | } 141 | } 142 | } 143 | } 144 | } 145 | if len(acl.user) == 0 && len(acl.key) == 0 { 146 | acl.srv.debug.Println("Authentication required") 147 | tokenValues := map[string]string{ 148 | "secret": string(acl.srv.cookieSalt), 149 | } 150 | // set validity for now + 1 min 151 | validity := 1 * time.Minute 152 | token, err := NewSecureToken("WWW-Authenticate", tokenValues, validity, acl.srv) 153 | if err != nil { 154 | acl.srv.debug.Println("Error generating Auth token: ", err) 155 | return 500, err 156 | } 157 | wwwAuth := `WebID-RSA source="` + acl.req.BaseURI() + `", nonce="` + token + `"` 158 | acl.w.Header().Set("WWW-Authenticate", wwwAuth) 159 | return 401, errors.New("Access to " + p.URI + " requires authentication") 160 | } 161 | acl.srv.debug.Println(mode + " access denied for: " + acl.user) 162 | return 403, errors.New("Access denied for: " + acl.user) 163 | } 164 | 165 | accessType = "defaultForNew" 166 | 167 | // cd one level: walkPath("/foo/bar/baz") => /foo/bar/ 168 | // decrement depth 169 | if len(depth) > 0 { 170 | depth = depth[:len(depth)-1] 171 | } else { 172 | depth = depth[:1] 173 | } 174 | path = walkPath(p.Base, depth) 175 | } 176 | acl.srv.debug.Println("No ACL policies present - access allowed") 177 | return 200, nil 178 | } 179 | 180 | func walkPath(base string, depth []string) string { 181 | path := base + "/" 182 | if len(depth) > 0 { 183 | path += strings.Join(depth, "/") + "/" 184 | } 185 | return path 186 | } 187 | 188 | // AllowRead checks if Read access is allowed 189 | func (acl *WAC) AllowRead(path string) (int, error) { 190 | return acl.allow("Read", path) 191 | } 192 | 193 | // AllowWrite checks if Write access is allowed 194 | func (acl *WAC) AllowWrite(path string) (int, error) { 195 | return acl.allow("Write", path) 196 | } 197 | 198 | // AllowAppend checks if Append access is allowed 199 | func (acl *WAC) AllowAppend(path string) (int, error) { 200 | return acl.allow("Append", path) 201 | } 202 | 203 | // AllowControl checks if Control access is allowed 204 | func (acl *WAC) AllowControl(path string) (int, error) { 205 | return acl.allow("Control", path) 206 | } 207 | 208 | func verifyDelegator(delegator string, delegatee string) bool { 209 | g := NewGraph(delegator) 210 | err := g.LoadURI(delegator) 211 | if err != nil { 212 | log.Println("Error loading graph for " + delegator) 213 | } 214 | 215 | for _, val := range g.All(NewResource(delegator), NewResource("http://www.w3.org/ns/auth/acl#delegates"), nil) { 216 | if debrack(val.Object.String()) == delegatee { 217 | return true 218 | } 219 | } 220 | return false 221 | } 222 | -------------------------------------------------------------------------------- /auth.go: -------------------------------------------------------------------------------- 1 | package gold 2 | 3 | import ( 4 | "crypto/sha256" 5 | "errors" 6 | "fmt" 7 | "net/http" 8 | "net/url" 9 | "strconv" 10 | "strings" 11 | "time" 12 | ) 13 | 14 | // DigestAuthentication structure 15 | type DigestAuthentication struct { 16 | Type, Source, Username, Realm, Nonce, URI, QOP, NC, CNonce, Response, Opaque, Algorithm string 17 | } 18 | 19 | // DigestAuthorization structure 20 | type DigestAuthorization struct { 21 | Type, Source, Username, Nonce, Signature string 22 | } 23 | 24 | func (req *httpRequest) authn(w http.ResponseWriter) string { 25 | user, err := req.userCookie() 26 | if err != nil { 27 | req.Server.debug.Println("userCookie error:", err) 28 | } 29 | if len(user) > 0 { 30 | req.Server.debug.Println("Cookie auth OK for User: " + user) 31 | return user 32 | } 33 | 34 | // try WebID-RSA 35 | if len(req.Header.Get("Authorization")) > 0 { 36 | user, err = WebIDDigestAuth(req) 37 | if err != nil { 38 | req.Server.debug.Println("WebID-RSA auth error:", err) 39 | } 40 | if len(user) > 0 { 41 | req.Server.debug.Println("WebID-RSA auth OK for User: " + user) 42 | } 43 | } 44 | // fall back to WebID-TLS 45 | if len(user) == 0 { 46 | user, err = WebIDTLSAuth(req) 47 | if err != nil { 48 | req.Server.debug.Println("WebID-TLS error:", err) 49 | } 50 | if len(user) > 0 { 51 | req.Server.debug.Println("WebID-TLS auth OK for User: " + user) 52 | } 53 | } 54 | 55 | if len(user) > 0 { 56 | if len(req.Header.Get("On-Behalf-Of")) > 0 { 57 | delegator := debrack(req.Header.Get("On-Behalf-Of")) 58 | if verifyDelegator(delegator, user) { 59 | req.Server.debug.Println("Setting delegation user to:", delegator) 60 | user = delegator 61 | } 62 | } 63 | req.Server.userCookieSet(w, user) 64 | return user 65 | } 66 | 67 | user = "" 68 | req.Server.debug.Println("Unauthenticated User") 69 | return user 70 | } 71 | 72 | func (req *httpRequest) userCookie() (string, error) { 73 | value := make(map[string]string) 74 | cookie, err := req.Cookie("Session") 75 | if err != nil { 76 | return "", errors.New(err.Error() + " Got: " + fmt.Sprintf("%s", req.Cookies())) 77 | } 78 | err = req.Server.cookie.Decode("Session", cookie.Value, &value) 79 | if err != nil { 80 | return "", err 81 | } 82 | return value["user"], nil 83 | } 84 | 85 | func (srv *Server) userCookieSet(w http.ResponseWriter, user string) error { 86 | value := map[string]string{ 87 | "user": user, 88 | } 89 | 90 | encoded, err := srv.cookie.Encode("Session", value) 91 | if err != nil { 92 | return err 93 | } 94 | t := time.Duration(srv.Config.CookieAge) * time.Hour 95 | cookieCfg := &http.Cookie{ 96 | Expires: time.Now().Add(t), 97 | Name: "Session", 98 | Path: "/", 99 | Value: encoded, 100 | Secure: true, 101 | } 102 | http.SetCookie(w, cookieCfg) 103 | return nil 104 | } 105 | 106 | func (srv *Server) userCookieDelete(w http.ResponseWriter) { 107 | http.SetCookie(w, &http.Cookie{ 108 | Name: "Session", 109 | Value: "deleted", 110 | Path: "/", 111 | MaxAge: -1, 112 | }) 113 | } 114 | 115 | // ParseDigestAuthenticateHeader parses an Authenticate header and returns a DigestAuthentication object 116 | func ParseDigestAuthenticateHeader(header string) (*DigestAuthentication, error) { 117 | auth := DigestAuthentication{} 118 | 119 | if len(header) == 0 { 120 | return &auth, errors.New("Cannot parse WWW-Authenticate header: no header present") 121 | } 122 | 123 | opts := make(map[string]string) 124 | parts := strings.SplitN(header, " ", 2) 125 | opts["type"] = parts[0] 126 | parts = strings.Split(parts[1], ",") 127 | 128 | for _, part := range parts { 129 | vals := strings.SplitN(strings.TrimSpace(part), "=", 2) 130 | key := vals[0] 131 | val := strings.Replace(vals[1], "\"", "", -1) 132 | opts[key] = val 133 | } 134 | 135 | auth = DigestAuthentication{ 136 | opts["type"], 137 | opts["source"], 138 | opts["username"], 139 | opts["realm"], 140 | opts["nonce"], 141 | opts["uri"], 142 | opts["qop"], 143 | opts["nc"], 144 | opts["qnonce"], 145 | opts["response"], 146 | opts["opaque"], 147 | opts["algorithm"], 148 | } 149 | return &auth, nil 150 | } 151 | 152 | // ParseDigestAuthorizationHeader parses an Authorization header and returns a DigestAuthorization object 153 | func ParseDigestAuthorizationHeader(header string) (*DigestAuthorization, error) { 154 | auth := DigestAuthorization{} 155 | 156 | if len(header) == 0 { 157 | return &auth, errors.New("Cannot parse Authorization header: no header present") 158 | } 159 | 160 | opts := make(map[string]string) 161 | parts := strings.SplitN(header, " ", 2) 162 | opts["type"] = parts[0] 163 | if opts["type"] == "Bearer" { 164 | return &auth, errors.New("Not a Digest authorization header. Got " + opts["type"]) 165 | } 166 | 167 | parts = strings.Split(parts[1], ",") 168 | 169 | for _, part := range parts { 170 | vals := strings.SplitN(strings.TrimSpace(part), "=", 2) 171 | key := vals[0] 172 | val := strings.Replace(vals[1], "\"", "", -1) 173 | opts[key] = val 174 | } 175 | 176 | auth = DigestAuthorization{ 177 | opts["type"], 178 | opts["source"], 179 | opts["username"], 180 | opts["nonce"], 181 | opts["sig"], 182 | } 183 | return &auth, nil 184 | } 185 | 186 | func ParseBearerAuthorizationHeader(header string) (string, error) { 187 | if len(header) == 0 { 188 | return "", errors.New("Cannot parse Authorization header: no header present") 189 | } 190 | 191 | parts := strings.SplitN(header, " ", 2) 192 | if parts[0] != "Bearer" { 193 | return "", errors.New("Not a Bearer header. Got: " + parts[0]) 194 | } 195 | return decodeQuery(parts[1]) 196 | } 197 | 198 | func NewTokenValues() map[string]string { 199 | return make(map[string]string) 200 | } 201 | 202 | // NewSecureToken generates a signed token to be used during account recovery 203 | func NewSecureToken(tokenType string, values map[string]string, duration time.Duration, s *Server) (string, error) { 204 | valid := time.Now().Add(duration).Unix() 205 | values["valid"] = fmt.Sprintf("%d", valid) 206 | token, err := s.cookie.Encode(tokenType, values) 207 | if err != nil { 208 | s.debug.Println("Error encoding new token: " + err.Error()) 209 | return "", err 210 | } 211 | return token, nil 212 | } 213 | 214 | // ValidateSecureToken returns the values of a secure cookie 215 | func ValidateSecureToken(tokenType string, token string, s *Server) (map[string]string, error) { 216 | values := make(map[string]string) 217 | err := s.cookie.Decode(tokenType, token, &values) 218 | if err != nil { 219 | s.debug.Println("Secure token decoding error: " + err.Error()) 220 | return values, err 221 | } 222 | 223 | return values, nil 224 | } 225 | 226 | func GetValuesFromToken(tokenType string, token string, req *httpRequest, s *Server) (map[string]string, error) { 227 | values := NewTokenValues() 228 | token, err := decodeQuery(token) 229 | if err != nil { 230 | s.debug.Println("Token URL decoding error for type: " + tokenType + " : " + err.Error()) 231 | return values, err 232 | } 233 | err = s.cookie.Decode(tokenType, token, &values) 234 | if err != nil { 235 | s.debug.Println("Token decoding error for type: " + tokenType + " \nToken: " + token + "\n" + err.Error()) 236 | return values, err 237 | } 238 | return values, nil 239 | } 240 | 241 | func IsTokenDateValid(valid string) error { 242 | v, err := strconv.ParseInt(valid, 10, 64) 243 | if err != nil { 244 | return err 245 | } 246 | 247 | if time.Now().Local().Unix() > v { 248 | return errors.New("Token has expired!") 249 | } 250 | 251 | return nil 252 | } 253 | 254 | func GetAuthzFromToken(token string, req *httpRequest) (string, error) { 255 | // values, err := GetValuesFromToken("Authorization", token, req, s) 256 | values, err := req.Server.getPersistedToken("Authorization", req.Host, token) 257 | if err != nil { 258 | return "", err 259 | } 260 | if len(values["webid"]) == 0 && len(values["valid"]) == 0 && 261 | len(values["origin"]) == 0 { 262 | return "", errors.New("Malformed token is missing required values") 263 | } 264 | err = IsTokenDateValid(values["valid"]) 265 | if err != nil { 266 | return "", err 267 | } 268 | origin := req.Header.Get("Origin") 269 | if len(origin) > 0 && origin != values["origin"] { 270 | return "", errors.New("Cannot authorize user: " + req.User + ". Origin: " + origin + " does not match the origin in the token: " + values["origin"]) 271 | } 272 | return values["webid"], nil 273 | } 274 | 275 | func saltedPassword(salt, pass string) string { 276 | s := sha256.Sum256([]byte(salt + pass)) 277 | toString := fmt.Sprintf("%x", s) 278 | return toString 279 | } 280 | 281 | func encodeQuery(s string) string { 282 | return url.QueryEscape(s) 283 | } 284 | 285 | func decodeQuery(s string) (string, error) { 286 | return url.QueryUnescape(s) 287 | } 288 | -------------------------------------------------------------------------------- /auth_test.go: -------------------------------------------------------------------------------- 1 | package gold 2 | 3 | import ( 4 | "crypto/sha1" 5 | "crypto/x509" 6 | "encoding/base64" 7 | "encoding/pem" 8 | "net/http" 9 | "strings" 10 | "testing" 11 | "time" 12 | 13 | "github.com/stretchr/testify/assert" 14 | ) 15 | 16 | func TestUrlEncodeDecode(t *testing.T) { 17 | str := "test#me=" 18 | dec, err := decodeQuery(encodeQuery(str)) 19 | assert.NoError(t, err) 20 | assert.Equal(t, str, dec) 21 | } 22 | 23 | func TestNewSecureToken(t *testing.T) { 24 | tokenValues := map[string]string{ 25 | "secret": string(handler.cookieSalt), 26 | } 27 | validity := 1 * time.Minute 28 | token, err := NewSecureToken("WWW-Authenticate", tokenValues, validity, handler) 29 | assert.NoError(t, err) 30 | assert.Equal(t, 184, len(token)) 31 | } 32 | 33 | func TestParseBearerAuthorizationHeader(t *testing.T) { 34 | decoded := "MTQ5MzMyMDM2NHx1YVUxT21EYUkxSXZKZ29VdC03NjFibDkzZGx1WEtyUEVpM21XUnVUSGh2LUQtN0ZUTTV0REVPcjNSWEIwUm1Ob2FHMm83LVkxd3d5UGZiYTZUb0pUSmRoZFBwM1BCVWxJN1drbjFMaTZ2bHloc3FtbVJnSkxfN2MzNkQ3eGFpS3FPS2JTOGdCN3NlZnNmb2lncG13ZUdDaUtWLTBmQ3BCMEhDNmVMRUNaWDdzSjlfVXxU5vqaGdhcpGEl9-qrIs-GBl2HJCXwC85bCDr_zrmbjA==" 35 | encoded := "MTQ5MzMyMDM2NHx1YVUxT21EYUkxSXZKZ29VdC03NjFibDkzZGx1WEtyUEVpM21XUnVUSGh2LUQtN0ZUTTV0REVPcjNSWEIwUm1Ob2FHMm83LVkxd3d5UGZiYTZUb0pUSmRoZFBwM1BCVWxJN1drbjFMaTZ2bHloc3FtbVJnSkxfN2MzNkQ3eGFpS3FPS2JTOGdCN3NlZnNmb2lncG13ZUdDaUtWLTBmQ3BCMEhDNmVMRUNaWDdzSjlfVXxU5vqaGdhcpGEl9-qrIs-GBl2HJCXwC85bCDr_zrmbjA%3D%3D" 36 | assert.Equal(t, encoded, encodeQuery(decoded)) 37 | dec, err := decodeQuery(encoded) 38 | assert.NoError(t, err) 39 | assert.Equal(t, decoded, dec) 40 | 41 | h := "Bearer " + encoded 42 | dec, err = ParseBearerAuthorizationHeader(h) 43 | assert.NoError(t, err) 44 | assert.Equal(t, decoded, dec) 45 | } 46 | 47 | func TestParseDigestAuthorizationHeader(t *testing.T) { 48 | h := "WebID-RSA source=\"http://server.org/\", username=\"http://example.org/\", nonce=\"string1\", sig=\"string2\"" 49 | p, err := ParseDigestAuthorizationHeader(h) 50 | assert.NoError(t, err) 51 | assert.Equal(t, "WebID-RSA", p.Type) 52 | assert.Equal(t, "http://server.org/", p.Source) 53 | assert.Equal(t, "http://example.org/", p.Username) 54 | assert.Equal(t, "string1", p.Nonce) 55 | assert.Equal(t, "string2", p.Signature) 56 | 57 | h = "WebID-RSA source=\"http://server.org/\", \nusername=\"http://example.org/\", \nnonce=\"string1\",\n sig=\"string2\"" 58 | p, err = ParseDigestAuthorizationHeader(h) 59 | assert.NoError(t, err) 60 | assert.Equal(t, "WebID-RSA", p.Type) 61 | assert.Equal(t, "http://server.org/", p.Source) 62 | assert.Equal(t, "http://example.org/", p.Username) 63 | assert.Equal(t, "string1", p.Nonce) 64 | assert.Equal(t, "string2", p.Signature) 65 | } 66 | 67 | func TestParseDigestAuthenticateHeader(t *testing.T) { 68 | h := `WebID-RSA source="http://server.org/", nonce="string1"` 69 | 70 | p, err := ParseDigestAuthenticateHeader(h) 71 | assert.NoError(t, err) 72 | assert.Equal(t, "WebID-RSA", p.Type) 73 | assert.Equal(t, "string1", p.Nonce) 74 | assert.Equal(t, "http://server.org/", p.Source) 75 | } 76 | 77 | func TestCookieAuth(t *testing.T) { 78 | request, err := http.NewRequest("MKCOL", testServer.URL+aclDir, nil) 79 | assert.NoError(t, err) 80 | response, err := user1h.Do(request) 81 | assert.NoError(t, err) 82 | response.Body.Close() 83 | assert.Equal(t, 201, response.StatusCode) 84 | 85 | request, err = http.NewRequest("PUT", testServer.URL+aclDir+"abc", strings.NewReader(" .")) 86 | assert.NoError(t, err) 87 | request.Header.Add("Content-Type", "text/turtle") 88 | response, err = user1h.Do(request) 89 | assert.NoError(t, err) 90 | response.Body.Close() 91 | assert.Equal(t, 201, response.StatusCode) 92 | 93 | request, err = http.NewRequest("HEAD", testServer.URL+aclDir+"abc", nil) 94 | assert.NoError(t, err) 95 | response, err = user1h.Do(request) 96 | assert.NoError(t, err) 97 | assert.Equal(t, 200, response.StatusCode) 98 | cookie1 := response.Header.Get("Set-Cookie") 99 | assert.NotNil(t, cookie1) 100 | acl := ParseLinkHeader(response.Header.Get("Link")).MatchRel("acl") 101 | assert.NotNil(t, acl) 102 | 103 | body := "<#Owner>" + 104 | " <" + aclDir + "abc>, <" + acl + ">;" + 105 | " <" + user1 + ">;" + 106 | " , ." + 107 | "<#Restricted>" + 108 | " <" + aclDir + "abc>;" + 109 | " <" + user2 + ">;" + 110 | " , ." 111 | request, err = http.NewRequest("PUT", acl, strings.NewReader(body)) 112 | assert.NoError(t, err) 113 | request.Header.Add("Content-Type", "text/turtle") 114 | response, err = user1h.Do(request) 115 | assert.NoError(t, err) 116 | response.Body.Close() 117 | assert.Equal(t, 201, response.StatusCode) 118 | 119 | request, err = http.NewRequest("HEAD", testServer.URL+aclDir+"abc", nil) 120 | assert.NoError(t, err) 121 | request.Header.Add("Cookie", cookie1) 122 | response, err = httpClient.Do(request) 123 | assert.NoError(t, err) 124 | assert.Equal(t, 200, response.StatusCode) 125 | 126 | request, err = http.NewRequest("HEAD", testServer.URL+aclDir+"abc", nil) 127 | assert.NoError(t, err) 128 | response, err = user1h.Do(request) 129 | assert.NoError(t, err) 130 | assert.Equal(t, 200, response.StatusCode) 131 | cookie2 := response.Header.Get("Set-Cookie") 132 | assert.NotNil(t, cookie2) 133 | 134 | request, err = http.NewRequest("HEAD", testServer.URL+aclDir+"abc", nil) 135 | assert.NoError(t, err) 136 | request.Header.Add("Cookie", cookie2) 137 | response, err = user2h.Do(request) 138 | assert.NoError(t, err) 139 | assert.Equal(t, 200, response.StatusCode) 140 | 141 | request, err = http.NewRequest("HEAD", testServer.URL+aclDir+"abc", nil) 142 | assert.NoError(t, err) 143 | response, err = httpClient.Do(request) 144 | assert.NoError(t, err) 145 | assert.Equal(t, 401, response.StatusCode) 146 | 147 | } 148 | 149 | func TestWebIDRSAAuth(t *testing.T) { 150 | request, err := http.NewRequest("GET", testServer.URL+aclDir+"abc", nil) 151 | assert.NoError(t, err) 152 | response, err := httpClient.Do(request) 153 | assert.NoError(t, err) 154 | assert.Equal(t, 401, response.StatusCode) 155 | wwwAuth := response.Header.Get("WWW-Authenticate") 156 | assert.NotEmpty(t, wwwAuth) 157 | 158 | p, _ := ParseDigestAuthenticateHeader(wwwAuth) 159 | 160 | // Load private key 161 | pKey := x509.MarshalPKCS1PrivateKey(user1k) 162 | keyBytes := pem.EncodeToMemory(&pem.Block{ 163 | Type: "RSA PRIVATE KEY", 164 | Bytes: pKey, 165 | }) 166 | signer, err := ParseRSAPrivatePEMKey(keyBytes) 167 | assert.NoError(t, err) 168 | 169 | claim := sha1.Sum([]byte(p.Source + user1 + p.Nonce)) 170 | signed, err := signer.Sign(claim[:]) 171 | assert.NoError(t, err) 172 | b64Sig := base64.StdEncoding.EncodeToString(signed) 173 | assert.NotEmpty(t, b64Sig) 174 | 175 | authHeader := `WebID-RSA source="` + p.Source + `", username="` + user1 + `", nonce="` + p.Nonce + `", sig="` + b64Sig + `"` 176 | 177 | request, err = http.NewRequest("GET", testServer.URL+aclDir+"abc", nil) 178 | request.Header.Add("Authorization", authHeader) 179 | assert.NoError(t, err) 180 | response, err = httpClient.Do(request) 181 | assert.NoError(t, err) 182 | assert.Equal(t, 200, response.StatusCode) 183 | } 184 | 185 | func TestWebIDRSAAuthBadSource(t *testing.T) { 186 | request, err := http.NewRequest("GET", testServer.URL+aclDir+"abc", nil) 187 | assert.NoError(t, err) 188 | response, err := httpClient.Do(request) 189 | assert.NoError(t, err) 190 | assert.Equal(t, 401, response.StatusCode) 191 | wwwAuth := response.Header.Get("WWW-Authenticate") 192 | assert.NotEmpty(t, wwwAuth) 193 | 194 | p, _ := ParseDigestAuthenticateHeader(wwwAuth) 195 | 196 | // Load private key 197 | pKey := x509.MarshalPKCS1PrivateKey(user1k) 198 | keyBytes := pem.EncodeToMemory(&pem.Block{ 199 | Type: "RSA PRIVATE KEY", 200 | Bytes: pKey, 201 | }) 202 | signer, err := ParseRSAPrivatePEMKey(keyBytes) 203 | assert.NoError(t, err) 204 | 205 | // Bad source 206 | claim := sha1.Sum([]byte("http://baddude.org/" + user1 + p.Nonce)) 207 | signed, err := signer.Sign(claim[:]) 208 | assert.NoError(t, err) 209 | b64Sig := base64.StdEncoding.EncodeToString(signed) 210 | assert.NotEmpty(t, b64Sig) 211 | 212 | authHeader := `WebID-RSA source="http://baddude.org/", username="` + user1 + `", nonce="` + p.Nonce + `", sig="` + b64Sig + `"` 213 | 214 | request, err = http.NewRequest("GET", testServer.URL+aclDir+"abc", nil) 215 | request.Header.Add("Authorization", authHeader) 216 | assert.NoError(t, err) 217 | response, err = httpClient.Do(request) 218 | assert.NoError(t, err) 219 | assert.Equal(t, 401, response.StatusCode) 220 | } 221 | 222 | func TestCleanupAuth(t *testing.T) { 223 | request, err := http.NewRequest("HEAD", testServer.URL+aclDir+"abc", nil) 224 | assert.NoError(t, err) 225 | response, err := user1h.Do(request) 226 | assert.NoError(t, err) 227 | assert.Equal(t, 200, response.StatusCode) 228 | acl := ParseLinkHeader(response.Header.Get("Link")).MatchRel("acl") 229 | 230 | request, err = http.NewRequest("DELETE", acl, nil) 231 | assert.NoError(t, err) 232 | response, err = user1h.Do(request) 233 | assert.NoError(t, err) 234 | response.Body.Close() 235 | assert.Equal(t, 200, response.StatusCode) 236 | 237 | request, err = http.NewRequest("DELETE", testServer.URL+aclDir+"abc", nil) 238 | assert.NoError(t, err) 239 | response, err = user1h.Do(request) 240 | assert.NoError(t, err) 241 | response.Body.Close() 242 | assert.Equal(t, 200, response.StatusCode) 243 | 244 | request, err = http.NewRequest("DELETE", testServer.URL+aclDir, nil) 245 | assert.NoError(t, err) 246 | response, err = user1h.Do(request) 247 | assert.NoError(t, err) 248 | response.Body.Close() 249 | assert.Equal(t, 200, response.StatusCode) 250 | } 251 | 252 | func TestACLCleanUsers(t *testing.T) { 253 | request, err := http.NewRequest("DELETE", testServer.URL+"/_test/user1", nil) 254 | assert.NoError(t, err) 255 | response, err := user1h.Do(request) 256 | assert.NoError(t, err) 257 | assert.Equal(t, 200, response.StatusCode) 258 | 259 | request, err = http.NewRequest("DELETE", testServer.URL+"/_test/user2", nil) 260 | assert.NoError(t, err) 261 | response, err = user1h.Do(request) 262 | assert.NoError(t, err) 263 | assert.Equal(t, 200, response.StatusCode) 264 | } 265 | -------------------------------------------------------------------------------- /autoneg.go: -------------------------------------------------------------------------------- 1 | // Package gold implements several LD standards 2 | // Copyright 2011 The Go Authors. All rights reserved. 3 | // Use of this source code is governed by a BSD-style 4 | // license that can be found in the LICENSE file. 5 | // 6 | // The functions in this package implement the behaviour specified in 7 | // http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html 8 | // 9 | // This deviates from RFC2616 in one respect. When a client sets their 10 | // Accept header to "*" (which is illegal) it will be interpreted as "*/*". 11 | // This has been observed in the wild, and the choice was made in the 12 | // spirit of being liberal in values that are accepted from the 'net. 13 | package gold 14 | 15 | import ( 16 | "errors" 17 | "sort" 18 | "strconv" 19 | "strings" 20 | ) 21 | 22 | // Accept structure is used to represent a clause in an HTTP Accept Header. 23 | type Accept struct { 24 | Type, SubType string 25 | Q float32 26 | Params map[string]string 27 | } 28 | 29 | // For internal use, so that we can use the sort interface. 30 | type acceptSorter []Accept 31 | 32 | func (accept acceptSorter) Len() int { 33 | return len(accept) 34 | } 35 | 36 | // purposely sorts "backwards" so we have the most appropriate 37 | // (largest q-value) at the beginning of the list. 38 | func (accept acceptSorter) Less(i, j int) bool { 39 | ai, aj := accept[i], accept[j] 40 | if ai.Q > aj.Q { 41 | return true 42 | } 43 | if ai.Type != "*" && aj.Type == "*" { 44 | return true 45 | } 46 | if ai.SubType != "*" && aj.SubType == "*" { 47 | return true 48 | } 49 | return false 50 | } 51 | 52 | func (accept acceptSorter) Swap(i, j int) { 53 | accept[i], accept[j] = accept[j], accept[i] 54 | } 55 | 56 | // AcceptList is a sorted list of clauses from an Accept header. 57 | type AcceptList []Accept 58 | 59 | // Negotiate the most appropriate contentType given the list of alternatives. 60 | // Returns an error if no alternative is acceptable. 61 | func (al AcceptList) Negotiate(alternatives ...string) (contentType string, err error) { 62 | asp := make([][]string, 0, len(alternatives)) 63 | for _, ctype := range alternatives { 64 | asp = append(asp, strings.SplitN(ctype, "/", 2)) 65 | } 66 | for _, clause := range al { 67 | for i, ctsp := range asp { 68 | if clause.Type == ctsp[0] && clause.SubType == ctsp[1] { 69 | contentType = alternatives[i] 70 | return 71 | } 72 | if clause.Type == ctsp[0] && clause.SubType == "*" { 73 | contentType = alternatives[i] 74 | return 75 | } 76 | if clause.Type == "*" && clause.SubType == "*" { 77 | contentType = alternatives[i] 78 | return 79 | } 80 | } 81 | } 82 | err = errors.New("No acceptable alternatives") 83 | return 84 | } 85 | 86 | // Parse an Accept Header string returning a sorted list of clauses. 87 | func parseAccept(header string) (accept []Accept, err error) { 88 | header = strings.Trim(header, " ") 89 | if len(header) == 0 { 90 | accept = make([]Accept, 0) 91 | return 92 | } 93 | 94 | parts := strings.SplitN(header, ",", -1) 95 | accept = make([]Accept, 0, len(parts)) 96 | for _, part := range parts { 97 | part := strings.Trim(part, " ") 98 | 99 | a := Accept{} 100 | a.Params = make(map[string]string) 101 | a.Q = 1.0 102 | 103 | mrp := strings.SplitN(part, ";", -1) 104 | 105 | mediaRange := mrp[0] 106 | sp := strings.SplitN(mediaRange, "/", -1) 107 | a.Type = strings.Trim(sp[0], " ") 108 | 109 | switch { 110 | case len(sp) == 1 && a.Type == "*": 111 | // The case where the Accept header is just "*" is strictly speaking 112 | // invalid but is seen in the wild. We take it to be equivalent to 113 | // "*/*" 114 | a.SubType = "*" 115 | case len(sp) == 2: 116 | a.SubType = strings.Trim(sp[1], " ") 117 | default: 118 | err = errors.New("Invalid media range in " + part) 119 | return 120 | } 121 | 122 | if len(mrp) == 1 { 123 | accept = append(accept, a) 124 | continue 125 | } 126 | 127 | for _, param := range mrp[1:] { 128 | sp := strings.SplitN(param, "=", 2) 129 | if len(sp) != 2 { 130 | err = errors.New("Invalid parameter in " + part) 131 | return 132 | } 133 | token := strings.Trim(sp[0], " ") 134 | if token == "q" { 135 | q, _ := strconv.ParseFloat(sp[1], 32) 136 | a.Q = float32(q) 137 | } else { 138 | a.Params[token] = strings.Trim(sp[1], " ") 139 | } 140 | } 141 | 142 | accept = append(accept, a) 143 | } 144 | 145 | sorter := acceptSorter(accept) 146 | sort.Sort(sorter) 147 | 148 | return 149 | } 150 | 151 | // Parse the Accept header and return a sorted list of clauses. If the Accept header 152 | // is present but empty this will be an empty list. If the header is not present it will 153 | // default to a wildcard: */*. Returns an error if the Accept header is ill-formed. 154 | func (req *httpRequest) Accept() (al AcceptList, err error) { 155 | var accept string 156 | headers, ok := req.Header["Accept"] 157 | if ok && len(headers) > 0 { 158 | // if multiple Accept headers are specified just take the first one 159 | // such a client would be quite broken... 160 | accept = headers[0] 161 | } else { 162 | // default if not present 163 | accept = "*/*" 164 | } 165 | al, err = parseAccept(accept) 166 | return 167 | } 168 | -------------------------------------------------------------------------------- /autoneg_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2011 The Go Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package gold 6 | 7 | import ( 8 | "net/http" 9 | "testing" 10 | 11 | "github.com/stretchr/testify/assert" 12 | ) 13 | 14 | var ( 15 | chrome = "application/xml,application/xhtml+xml,text/html;q=0.9,text/plain;q=0.8,image/png,*/*;q=0.5" 16 | 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" 17 | ) 18 | 19 | func mockAccept(accept string) (al AcceptList, err error) { 20 | req := &http.Request{} 21 | req.Header = make(http.Header) 22 | req.Header["Accept"] = []string{accept} 23 | myreq := &httpRequest{req, nil, "", "", "", false} 24 | al, err = myreq.Accept() 25 | return 26 | } 27 | 28 | func TestNegotiatePicturesOfWebPages(t *testing.T) { 29 | al, err := mockAccept(chrome) 30 | if err != nil { 31 | t.Fatal(err) 32 | } 33 | 34 | contentType, err := al.Negotiate("text/html", "image/png") 35 | if err != nil { 36 | t.Fatal(err) 37 | } 38 | 39 | if contentType != "image/png" { 40 | t.Errorf("got %s expected image/png", contentType) 41 | } 42 | } 43 | 44 | func TestNegotiateRDF(t *testing.T) { 45 | al, err := mockAccept(rdflib) 46 | if err != nil { 47 | t.Fatal(err) 48 | } 49 | 50 | contentType, err := al.Negotiate(serializerMimes...) 51 | if err != nil { 52 | t.Fatal(err) 53 | } 54 | 55 | assert.Equal(t, "text/turtle", contentType) 56 | } 57 | 58 | func TestNegotiateFirstMatch(t *testing.T) { 59 | al, err := mockAccept(chrome) 60 | if err != nil { 61 | t.Fatal(err) 62 | } 63 | 64 | contentType, err := al.Negotiate("text/html", "text/plain", "text/n3") 65 | if err != nil { 66 | t.Fatal(err) 67 | } 68 | 69 | if contentType != "text/html" { 70 | t.Errorf("got %s expected text/html", contentType) 71 | } 72 | } 73 | 74 | func TestNegotiateSecondMatch(t *testing.T) { 75 | al, err := mockAccept(chrome) 76 | if err != nil { 77 | t.Fatal(err) 78 | } 79 | 80 | contentType, err := al.Negotiate("text/n3", "text/plain") 81 | if err != nil { 82 | t.Fatal(err) 83 | } 84 | 85 | if contentType != "text/plain" { 86 | t.Errorf("got %s expected text/plain", contentType) 87 | } 88 | } 89 | 90 | func TestNegotiateWildcardMatch(t *testing.T) { 91 | al, err := mockAccept(chrome) 92 | if err != nil { 93 | t.Fatal(err) 94 | } 95 | 96 | contentType, err := al.Negotiate("text/n3", "application/rdf+xml") 97 | if err != nil { 98 | t.Fatal(err) 99 | } 100 | 101 | if contentType != "text/n3" { 102 | t.Errorf("got %s expected text/n3", contentType) 103 | } 104 | } 105 | 106 | func TestNegotiateInvalidMediaRange(t *testing.T) { 107 | _, err := mockAccept("something/valid, rubbish, other/valid") 108 | if err == nil { 109 | t.Fatal("expected error on obviously invalid media range") 110 | } 111 | } 112 | 113 | func TestNegotiateInvalidParam(t *testing.T) { 114 | _, err := mockAccept("text/plain; foo") 115 | if err == nil { 116 | t.Fatal("expected error on ill-formed params") 117 | } 118 | } 119 | 120 | func TestNegotiateEmptyAccept(t *testing.T) { 121 | al, err := mockAccept("") 122 | if err != nil { 123 | t.Fatal(err) 124 | } 125 | 126 | _, err = al.Negotiate("text/plain") 127 | if err == nil { 128 | t.Error("expected error with empty but present accept header") 129 | } 130 | } 131 | 132 | func TestNegotiateStarAccept(t *testing.T) { 133 | al, err := mockAccept("*") 134 | if err != nil { 135 | t.Fatal(err) 136 | } 137 | if al[0].Type+"/"+al[0].SubType != "*/*" { 138 | t.Error("expected subtype * for single * accept header") 139 | } 140 | } 141 | 142 | func TestNegotiateNoAlternative(t *testing.T) { 143 | al, err := mockAccept(chrome) 144 | if err != nil { 145 | t.Fatal(err) 146 | } 147 | 148 | _, err = al.Negotiate() 149 | if err == nil { 150 | t.Error("expected error with no alternative") 151 | } 152 | } 153 | -------------------------------------------------------------------------------- /config.go: -------------------------------------------------------------------------------- 1 | package gold 2 | 3 | import ( 4 | "encoding/json" 5 | "io/ioutil" 6 | "log" 7 | "os" 8 | "path/filepath" 9 | "strings" 10 | ) 11 | 12 | // ServerConfig holds a list of configuration parameters for the server 13 | type ServerConfig struct { 14 | // PortHTTP contains the HTTPS listening port number in format ":80" 15 | ListenHTTP string 16 | 17 | // PortHTTPS contains the HTTPS listening port number in format ":443" 18 | ListenHTTPS string 19 | 20 | // WebIDTLS enables/disables client cert authentication (WebID-TLS) (on by default) 21 | WebIDTLS bool 22 | 23 | // TLSCert holds the server certificate eg. cert.pem 24 | TLSCert string 25 | 26 | // TLSKey holds the server key eg. key.pem 27 | TLSKey string 28 | 29 | // Root points to the folder that will be used as root for data 30 | DataRoot string 31 | 32 | // Vhosts enables the use of virtual hosts (i.e. user.example.org) 33 | Vhosts bool 34 | 35 | // Insecure enables insecure (HTTP) operation mode only 36 | Insecure bool 37 | 38 | // NoHTTP allows to enable or disable redirects from HTTP to HTTPS 39 | NoHTTP bool 40 | 41 | // HSTS enables or disables strict security transport 42 | HSTS bool 43 | 44 | // Debug (display or hide stdout logging) 45 | Debug bool 46 | 47 | // CookieAge contains the validity duration for cookies (in hours) 48 | CookieAge int64 49 | 50 | // TokenAge contains the validity duration for recovery tokens (in minutes) 51 | TokenAge int64 52 | 53 | // METASuffix sets the default suffix for meta files (e.g. ,meta or .meta) 54 | MetaSuffix string 55 | 56 | // ACLSuffix sets the default suffix for ACL files (e.g. ,acl or .acl) 57 | ACLSuffix string 58 | 59 | // DataApp sets the default app for viewing RDF resources 60 | DataApp string 61 | 62 | // DirApp points to the app for browsing the data space 63 | DirApp string 64 | 65 | // SignUpApp points to the app used for creating new accounts 66 | SignUpApp string 67 | 68 | // ProxyTemplate is the URL of the service that handles WebID-TLS delegation 69 | ProxyTemplate string 70 | 71 | // ProxyLocal enables/disables proxying of resources on localhost 72 | ProxyLocal bool 73 | 74 | // QueryTemplate is the URL of the service that handles query request using twinql 75 | QueryTemplate string 76 | 77 | // DirIndex contains the default index file name 78 | DirIndex []string 79 | 80 | // DiskLimit is the maximum total disk (in bytes) to be allocated to a given user 81 | DiskLimit int 82 | 83 | // Agent is the WebID of the agent used for WebID-TLS delegation (and proxy) 84 | Agent string 85 | 86 | // Salt is the value used for hashing passwords 87 | Salt string 88 | 89 | // BoltPath points to the location of the Bolt db on the filesystem 90 | BoltPath string 91 | 92 | // SMTPConfig holds the settings for the remote SMTP user/server 93 | SMTPConfig EmailConfig 94 | } 95 | 96 | // NewServerConfig creates a new config object 97 | func NewServerConfig() *ServerConfig { 98 | return &ServerConfig{ 99 | CookieAge: 8736, // hours (1 year) 100 | TokenAge: 5, 101 | HSTS: true, 102 | WebIDTLS: true, 103 | MetaSuffix: ".meta", 104 | ACLSuffix: ".acl", 105 | DataApp: "tabulator", 106 | DirIndex: []string{"index.html", "index.htm"}, 107 | DirApp: "http://linkeddata.github.io/warp/#list/", 108 | SignUpApp: "https://solid.github.io/solid-signup/?domain=", 109 | DiskLimit: 100000000, // 100MB 110 | DataRoot: serverDefaultRoot(), 111 | BoltPath: filepath.Join(os.TempDir(), "bolt.db"), 112 | ProxyLocal: true, 113 | } 114 | } 115 | 116 | // LoadJSONFile loads server configuration 117 | func (c *ServerConfig) LoadJSONFile(filename string) error { 118 | b, err := ioutil.ReadFile(filename) 119 | if err != nil { 120 | return err 121 | } 122 | return json.Unmarshal(b, &c) 123 | } 124 | 125 | func serverDefaultRoot() string { 126 | serverRoot, err := os.Getwd() 127 | if err != nil { 128 | log.Fatalln(err) 129 | } 130 | 131 | if !strings.HasSuffix(serverRoot, "/") { 132 | serverRoot += "/" 133 | } 134 | return serverRoot 135 | } 136 | -------------------------------------------------------------------------------- /cors_test.go: -------------------------------------------------------------------------------- 1 | package gold 2 | 3 | import ( 4 | "net/http" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func TestCORSRequestHasOrigin(t *testing.T) { 11 | requestOrigin := "https://example.com" 12 | url := testServer.URL + "/_test/user1" 13 | req, err := http.NewRequest("GET", url, nil) 14 | assert.NoError(t, err) 15 | req.Header.Set("Origin", requestOrigin) 16 | resp, err := httpClient.Do(req) 17 | assert.NoError(t, err) 18 | assert.Equal(t, requestOrigin, resp.Header.Get("Access-Control-Allow-Origin")) 19 | } 20 | 21 | func TestCORSRequestHasNoOrigin(t *testing.T) { 22 | url := testServer.URL + "/_test/user1" 23 | req, err := http.NewRequest("GET", url, nil) 24 | assert.NoError(t, err) 25 | resp, err := httpClient.Do(req) 26 | assert.NoError(t, err) 27 | assert.Equal(t, "*", resp.Header.Get("Access-Control-Allow-Origin")) 28 | } 29 | 30 | func TestVaryHeader(t *testing.T) { 31 | url := testServer.URL + "/_test/user1" 32 | req, err := http.NewRequest("GET", url, nil) 33 | assert.NoError(t, err) 34 | resp, err := httpClient.Do(req) 35 | assert.NoError(t, err) 36 | assert.Equal(t, "Origin", resp.Header.Get("Vary")) 37 | } 38 | -------------------------------------------------------------------------------- /crypto.go: -------------------------------------------------------------------------------- 1 | package gold 2 | 3 | import ( 4 | "crypto" 5 | "crypto/rand" 6 | "crypto/rsa" 7 | "crypto/x509" 8 | "encoding/pem" 9 | "errors" 10 | "fmt" 11 | "math/big" 12 | "strconv" 13 | ) 14 | 15 | // Signer creates signatures that verify against a public key. 16 | type Signer interface { 17 | Sign(data []byte) ([]byte, error) 18 | } 19 | 20 | // Verifier verifies signatures against a public key. 21 | type Verifier interface { 22 | Verify(data []byte, sig []byte) error 23 | } 24 | 25 | type rsaPubKey struct { 26 | *rsa.PublicKey 27 | } 28 | 29 | type rsaPrivKey struct { 30 | *rsa.PrivateKey 31 | } 32 | 33 | // ParseRSAPublicKeyNE parses a modulus and exponent and returns a new verifier object 34 | func ParseRSAPublicKeyNE(keyT, keyN, keyE string) (Verifier, error) { 35 | if len(keyN) == 0 && len(keyE) == 0 { 36 | return nil, errors.New("No modulus and/or exponent provided") 37 | } 38 | intN := new(big.Int) 39 | intN.SetString(keyN, 16) 40 | 41 | intE, err := strconv.ParseInt(keyE, 10, 0) 42 | if err != nil { 43 | return nil, err 44 | } 45 | 46 | var rawkey interface{} 47 | switch keyT { 48 | case "RSAPublicKey": 49 | rawkey = &rsa.PublicKey{ 50 | N: intN, 51 | E: int(intE), 52 | } 53 | default: 54 | return nil, fmt.Errorf("Unsupported key type %q", keyT) 55 | } 56 | return newVerifierFromKey(rawkey) 57 | } 58 | 59 | // ParseRSAPublicKey parses an RSA public key and returns a new verifier object 60 | func ParseRSAPublicKey(key *rsa.PublicKey) (Verifier, error) { 61 | return newVerifierFromKey(key) 62 | } 63 | 64 | // ParseRSAPrivateKey parses an RSA private key and returns a new signer object 65 | func ParseRSAPrivateKey(key *rsa.PrivateKey) (Signer, error) { 66 | return newSignerFromKey(key) 67 | } 68 | 69 | // ParseRSAPublicPEMKey parses a PEM encoded private key and returns a new verifier object 70 | func ParseRSAPublicPEMKey(pemBytes []byte) (Verifier, error) { 71 | block, _ := pem.Decode(pemBytes) 72 | if block == nil { 73 | return nil, errors.New("No key found") 74 | } 75 | 76 | var rawkey interface{} 77 | switch block.Type { 78 | case "RSA PUBLIC KEY", "PUBLIC KEY": 79 | rsa, err := x509.ParsePKIXPublicKey(block.Bytes) 80 | if err != nil { 81 | return nil, err 82 | } 83 | rawkey = rsa 84 | default: 85 | return nil, fmt.Errorf("Unsupported key type %q", block.Type) 86 | } 87 | 88 | return newVerifierFromKey(rawkey) 89 | } 90 | 91 | // ParseRSAPrivatePEMKey parses a PEM encoded private key and returns a Signer. 92 | func ParseRSAPrivatePEMKey(pemBytes []byte) (Signer, error) { 93 | block, _ := pem.Decode(pemBytes) 94 | if block == nil { 95 | return nil, errors.New("No key found or could not decode PEM key") 96 | } 97 | 98 | var rawkey interface{} 99 | switch block.Type { 100 | case "RSA PRIVATE KEY", "PRIVATE KEY": 101 | rsa, err := x509.ParsePKCS1PrivateKey(block.Bytes) 102 | if err != nil { 103 | return nil, err 104 | } 105 | rawkey = rsa 106 | default: 107 | return nil, fmt.Errorf("Unsupported key type %q", block.Type) 108 | } 109 | return newSignerFromKey(rawkey) 110 | } 111 | 112 | func newSignerFromKey(k interface{}) (Signer, error) { 113 | var sKey Signer 114 | switch t := k.(type) { 115 | case *rsa.PrivateKey: 116 | sKey = &rsaPrivKey{t} 117 | default: 118 | return nil, fmt.Errorf("Unsupported key type %T", k) 119 | } 120 | return sKey, nil 121 | } 122 | 123 | func newVerifierFromKey(k interface{}) (Verifier, error) { 124 | var vKey Verifier 125 | switch t := k.(type) { 126 | case *rsa.PublicKey: 127 | vKey = &rsaPubKey{t} 128 | default: 129 | return nil, fmt.Errorf("Unsupported key type %T", k) 130 | } 131 | return vKey, nil 132 | } 133 | 134 | // Sign signs data with rsa-sha256 135 | func (r *rsaPrivKey) Sign(data []byte) ([]byte, error) { 136 | return rsa.SignPKCS1v15(rand.Reader, r.PrivateKey, crypto.SHA1, data) 137 | } 138 | 139 | // Verify verifies the message using a rsa-sha256 signature 140 | func (r *rsaPubKey) Verify(message []byte, sig []byte) error { 141 | return rsa.VerifyPKCS1v15(r.PublicKey, crypto.SHA1, message, sig) 142 | } 143 | -------------------------------------------------------------------------------- /crypto_test.go: -------------------------------------------------------------------------------- 1 | package gold 2 | 3 | import ( 4 | "crypto/sha1" 5 | "encoding/base64" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/assert" 9 | ) 10 | 11 | func TestSignaturesRSA(t *testing.T) { 12 | privKey := []byte(`-----BEGIN RSA PRIVATE KEY----- 13 | MIICXgIBAAKBgQDCFENGw33yGihy92pDjZQhl0C36rPJj+CvfSC8+q28hxA161QF 14 | NUd13wuCTUcq0Qd2qsBe/2hFyc2DCJJg0h1L78+6Z4UMR7EOcpfdUE9Hf3m/hs+F 15 | UR45uBJeDK1HSFHD8bHKD6kv8FPGfJTotc+2xjJwoYi+1hqp1fIekaxsyQIDAQAB 16 | AoGBAJR8ZkCUvx5kzv+utdl7T5MnordT1TvoXXJGXK7ZZ+UuvMNUCdN2QPc4sBiA 17 | QWvLw1cSKt5DsKZ8UETpYPy8pPYnnDEz2dDYiaew9+xEpubyeW2oH4Zx71wqBtOK 18 | kqwrXa/pzdpiucRRjk6vE6YY7EBBs/g7uanVpGibOVAEsqH1AkEA7DkjVH28WDUg 19 | f1nqvfn2Kj6CT7nIcE3jGJsZZ7zlZmBmHFDONMLUrXR/Zm3pR5m0tCmBqa5RK95u 20 | 412jt1dPIwJBANJT3v8pnkth48bQo/fKel6uEYyboRtA5/uHuHkZ6FQF7OUkGogc 21 | mSJluOdc5t6hI1VsLn0QZEjQZMEOWr+wKSMCQQCC4kXJEsHAve77oP6HtG/IiEn7 22 | kpyUXRNvFsDE0czpJJBvL/aRFUJxuRK91jhjC68sA7NsKMGg5OXb5I5Jj36xAkEA 23 | gIT7aFOYBFwGgQAQkWNKLvySgKbAZRTeLBacpHMuQdl1DfdntvAyqpAZ0lY0RKmW 24 | G6aFKaqQfOXKCyWoUiVknQJAXrlgySFci/2ueKlIE1QqIiLSZ8V8OlpFLRnb1pzI 25 | 7U1yQXnTAEFYM560yJlzUpOb1V4cScGd365tiSMvxLOvTA== 26 | -----END RSA PRIVATE KEY-----`) 27 | 28 | pubKey := []byte(`-----BEGIN RSA PUBLIC KEY----- 29 | MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDCFENGw33yGihy92pDjZQhl0C3 30 | 6rPJj+CvfSC8+q28hxA161QFNUd13wuCTUcq0Qd2qsBe/2hFyc2DCJJg0h1L78+6 31 | Z4UMR7EOcpfdUE9Hf3m/hs+FUR45uBJeDK1HSFHD8bHKD6kv8FPGfJTotc+2xjJw 32 | oYi+1hqp1fIekaxsyQIDAQAB 33 | -----END RSA PUBLIC KEY-----`) 34 | 35 | pubT := "RSAPublicKey" 36 | pubN := "c2144346c37df21a2872f76a438d94219740b7eab3c98fe0af7d20bcfaadbc871035eb5405354775df0b824d472ad10776aac05eff6845c9cd83089260d21d4befcfba67850c47b10e7297dd504f477f79bf86cf85511e39b8125e0cad474851c3f1b1ca0fa92ff053c67c94e8b5cfb6c63270a188bed61aa9d5f21e91ac6cc9" 37 | pubE := "65537" 38 | 39 | h := `WebID-RSA source="https://deiu.me/Private/", username="https://deiu.me/profile#me", nonce="MTQzODc4MzA5NXxtS1dYcVd4bGRjVXQ2bFVEMXk2NE5KMDU1TFB3Nk9qM2FmMWduMk4tdl9tWDdvZXBtdUJSa1ZMRHE4WWZ1dUE0RlNGeDl0OGt6SGZnbkpZbW5CWE96TUxRamJ6a3xCC-Ik7gERpCBc__l2OK0DxVxyIiLTDVZ7rLIib2MNSQ==", sig="qiTKnXaXgMfGEA2LLCqhFWiB+6T9gXvLR6nO2dCvk71nBoK3MiwLxbsF83uKT81ur9SucDJ2fmjLKPbP9o7NrkYrM45rkPJsXHjbAzHDw2DftKLez5DF70HtDa1rEaUEF1mLrNMGfL4VYea5z15lNNNiDKaJpCwhgeHNB1x2qNY="` 40 | _toSign := `https://deiu.me/Private/https://deiu.me/profile#meMTQzODc4MzA5NXxtS1dYcVd4bGRjVXQ2bFVEMXk2NE5KMDU1TFB3Nk9qM2FmMWduMk4tdl9tWDdvZXBtdUJSa1ZMRHE4WWZ1dUE0RlNGeDl0OGt6SGZnbkpZbW5CWE96TUxRamJ6a3xCC-Ik7gERpCBc__l2OK0DxVxyIiLTDVZ7rLIib2MNSQ==` 41 | _sig := `qiTKnXaXgMfGEA2LLCqhFWiB+6T9gXvLR6nO2dCvk71nBoK3MiwLxbsF83uKT81ur9SucDJ2fmjLKPbP9o7NrkYrM45rkPJsXHjbAzHDw2DftKLez5DF70HtDa1rEaUEF1mLrNMGfL4VYea5z15lNNNiDKaJpCwhgeHNB1x2qNY=` 42 | p, err := ParseDigestAuthorizationHeader(h) 43 | assert.NoError(t, err) 44 | 45 | assert.Equal(t, _sig, p.Signature) 46 | 47 | parserPem, perr := ParseRSAPublicPEMKey(pubKey) 48 | assert.NoError(t, perr) 49 | 50 | parser, perr := ParseRSAPublicKeyNE(pubT, pubN, pubE) 51 | assert.NoError(t, perr) 52 | 53 | signer, err := ParseRSAPrivatePEMKey(privKey) 54 | assert.NoError(t, err) 55 | 56 | toSign := p.Source + p.Username + p.Nonce 57 | assert.Equal(t, _toSign, toSign) 58 | 59 | claim := sha1.Sum([]byte(toSign)) 60 | signed, err := signer.Sign(claim[:]) 61 | assert.NoError(t, err) 62 | b64Sig := base64.StdEncoding.EncodeToString(signed) 63 | assert.Equal(t, p.Signature, b64Sig) 64 | 65 | // println(p.Source, p.Username, p.Nonce, p.Signature) 66 | sig, err := base64.StdEncoding.DecodeString(p.Signature) 67 | assert.NoError(t, err) 68 | 69 | err = parser.Verify(claim[:], sig) 70 | assert.NoError(t, err) 71 | 72 | sig, err = base64.StdEncoding.DecodeString(_sig) 73 | assert.NoError(t, err) 74 | 75 | err = parserPem.Verify(claim[:], sig) 76 | assert.NoError(t, err) 77 | 78 | err = parser.Verify(claim[:], sig) 79 | assert.NoError(t, err) 80 | } 81 | 82 | func TestSignAndVerify(t *testing.T) { 83 | privKey := []byte(`-----BEGIN RSA PRIVATE KEY----- 84 | MIICXgIBAAKBgQDCFENGw33yGihy92pDjZQhl0C36rPJj+CvfSC8+q28hxA161QF 85 | NUd13wuCTUcq0Qd2qsBe/2hFyc2DCJJg0h1L78+6Z4UMR7EOcpfdUE9Hf3m/hs+F 86 | UR45uBJeDK1HSFHD8bHKD6kv8FPGfJTotc+2xjJwoYi+1hqp1fIekaxsyQIDAQAB 87 | AoGBAJR8ZkCUvx5kzv+utdl7T5MnordT1TvoXXJGXK7ZZ+UuvMNUCdN2QPc4sBiA 88 | QWvLw1cSKt5DsKZ8UETpYPy8pPYnnDEz2dDYiaew9+xEpubyeW2oH4Zx71wqBtOK 89 | kqwrXa/pzdpiucRRjk6vE6YY7EBBs/g7uanVpGibOVAEsqH1AkEA7DkjVH28WDUg 90 | f1nqvfn2Kj6CT7nIcE3jGJsZZ7zlZmBmHFDONMLUrXR/Zm3pR5m0tCmBqa5RK95u 91 | 412jt1dPIwJBANJT3v8pnkth48bQo/fKel6uEYyboRtA5/uHuHkZ6FQF7OUkGogc 92 | mSJluOdc5t6hI1VsLn0QZEjQZMEOWr+wKSMCQQCC4kXJEsHAve77oP6HtG/IiEn7 93 | kpyUXRNvFsDE0czpJJBvL/aRFUJxuRK91jhjC68sA7NsKMGg5OXb5I5Jj36xAkEA 94 | gIT7aFOYBFwGgQAQkWNKLvySgKbAZRTeLBacpHMuQdl1DfdntvAyqpAZ0lY0RKmW 95 | G6aFKaqQfOXKCyWoUiVknQJAXrlgySFci/2ueKlIE1QqIiLSZ8V8OlpFLRnb1pzI 96 | 7U1yQXnTAEFYM560yJlzUpOb1V4cScGd365tiSMvxLOvTA== 97 | -----END RSA PRIVATE KEY-----`) 98 | pubKey := []byte(`-----BEGIN PUBLIC KEY----- 99 | MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDCFENGw33yGihy92pDjZQhl0C3 100 | 6rPJj+CvfSC8+q28hxA161QFNUd13wuCTUcq0Qd2qsBe/2hFyc2DCJJg0h1L78+6 101 | Z4UMR7EOcpfdUE9Hf3m/hs+FUR45uBJeDK1HSFHD8bHKD6kv8FPGfJTotc+2xjJw 102 | oYi+1hqp1fIekaxsyQIDAQAB 103 | -----END PUBLIC KEY-----`) 104 | 105 | toSign := "some string" 106 | claim := sha1.Sum([]byte(toSign)) 107 | 108 | signer, err := ParseRSAPrivatePEMKey(privKey) 109 | assert.NoError(t, err) 110 | 111 | signed, err := signer.Sign(claim[:]) 112 | assert.NoError(t, err) 113 | 114 | sig := base64.URLEncoding.EncodeToString(signed) 115 | assert.NotEmpty(t, sig) 116 | 117 | parser, perr := ParseRSAPublicPEMKey(pubKey) 118 | assert.NoError(t, perr) 119 | 120 | err = parser.Verify(claim[:], signed) 121 | assert.NoError(t, err) 122 | 123 | // check with ParsePublicRSAKey 124 | pubT := "RSAPublicKey" 125 | pubN := "c2144346c37df21a2872f76a438d94219740b7eab3c98fe0af7d20bcfaadbc871035eb5405354775df0b824d472ad10776aac05eff6845c9cd83089260d21d4befcfba67850c47b10e7297dd504f477f79bf86cf85511e39b8125e0cad474851c3f1b1ca0fa92ff053c67c94e8b5cfb6c63270a188bed61aa9d5f21e91ac6cc9" 126 | pubE := "65537" 127 | 128 | parser, err = ParseRSAPublicKeyNE(pubT, pubN, pubE) 129 | assert.NoError(t, perr) 130 | 131 | err = parser.Verify(claim[:], signed) 132 | assert.NoError(t, err) 133 | 134 | // check with parse rsa.PublicKey 135 | signer, err = ParseRSAPrivateKey(user1k) 136 | assert.NoError(t, err) 137 | 138 | signed, err = signer.Sign(claim[:]) 139 | assert.NoError(t, err) 140 | 141 | sig = base64.StdEncoding.EncodeToString(signed) 142 | assert.NotEmpty(t, sig) 143 | 144 | parser, perr = ParseRSAPublicKey(user1p) 145 | assert.NoError(t, perr) 146 | 147 | err = parser.Verify(claim[:], signed) 148 | assert.NoError(t, err) 149 | } 150 | -------------------------------------------------------------------------------- /gold.conf-example: -------------------------------------------------------------------------------- 1 | { 2 | "ListenHTTP": ":8000", 3 | 4 | "ListenHTTPS": ":4443", 5 | 6 | "TLSCert": "/Users/user/certs/cert.pem", 7 | 8 | "TLSKey": "/Users/user/keys/key.pem", 9 | 10 | "DataRoot": "/Users/user/gold-data/", 11 | 12 | "Vhosts": true, 13 | 14 | "Insecure": false, 15 | 16 | "Debug": false, 17 | 18 | "CookieAge": 8736, 19 | 20 | "TokenAge": 5, 21 | 22 | "METASuffix": ".meta", 23 | 24 | "ACLSuffix": ".acl", 25 | 26 | "DataApp": "tabulator", 27 | 28 | "DirApp": "http://linkeddata.github.io/warp/#list/", 29 | 30 | "SignUpApp": "https://solid.github.io/solid-signup/?domain=", 31 | 32 | "DirIndex": ["index.html", "index.htm"], 33 | 34 | "DiskLimit": 100000000, 35 | 36 | "SMTPConfig": { 37 | "Name": "Administrator", 38 | "Addr": "admin@test.org", 39 | "User": "username", 40 | "Pass": "password", 41 | "Host": "mail.test.org", 42 | "Port": 25, 43 | "SSL": true, 44 | "Insecure": false 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /graph.go: -------------------------------------------------------------------------------- 1 | package gold 2 | 3 | import ( 4 | "bytes" 5 | "crypto/tls" 6 | "encoding/json" 7 | "fmt" 8 | "io" 9 | "io/ioutil" 10 | "log" 11 | "net/http" 12 | "net/url" 13 | "os" 14 | 15 | jsonld "github.com/linkeddata/gojsonld" 16 | crdf "github.com/presbrey/goraptor" 17 | ) 18 | 19 | // AnyGraph defines methods common to Graph types 20 | type AnyGraph interface { 21 | Len() int 22 | URI() string 23 | Parse(io.Reader, string) 24 | Serialize(string) (string, error) 25 | 26 | JSONPatch(io.Reader) error 27 | SPARQLUpdate(*SPARQLUpdate) (int, error) 28 | IterTriples() chan *Triple 29 | 30 | ReadFile(string) 31 | WriteFile(*os.File, string) error 32 | } 33 | 34 | var ( 35 | httpClient = &http.Client{ 36 | Transport: &http.Transport{ 37 | TLSClientConfig: &tls.Config{ 38 | InsecureSkipVerify: true, 39 | }, 40 | }, 41 | } 42 | ) 43 | 44 | // Graph structure 45 | type Graph struct { 46 | triples map[*Triple]bool 47 | 48 | uri string 49 | term Term 50 | } 51 | 52 | // NewGraph creates a Graph object 53 | func NewGraph(uri string) *Graph { 54 | if uri[:5] != "http:" && uri[:6] != "https:" { 55 | panic(uri) 56 | } 57 | 58 | return &Graph{ 59 | triples: make(map[*Triple]bool), 60 | uri: uri, 61 | term: NewResource(uri), 62 | } 63 | } 64 | 65 | // Len returns the length of the graph as number of triples in the graph 66 | func (g *Graph) Len() int { 67 | return len(g.triples) 68 | } 69 | 70 | // Term returns a Graph Term object 71 | func (g *Graph) Term() Term { 72 | return g.term 73 | } 74 | 75 | // URI returns a Graph URI object 76 | func (g *Graph) URI() string { 77 | return g.uri 78 | } 79 | 80 | func term2term(term crdf.Term) Term { 81 | switch term := term.(type) { 82 | case *crdf.Blank: 83 | return NewBlankNode(term.String()) 84 | case *crdf.Literal: 85 | if len(term.Datatype) > 0 { 86 | return NewLiteralWithLanguageAndDatatype(term.Value, term.Lang, NewResource(term.Datatype)) 87 | } 88 | return NewLiteral(term.Value) 89 | case *crdf.Uri: 90 | return NewResource(term.String()) 91 | } 92 | return nil 93 | } 94 | 95 | func jterm2term(term jsonld.Term) Term { 96 | switch term := term.(type) { 97 | case *jsonld.BlankNode: 98 | return NewBlankNode(term.RawValue()) 99 | case *jsonld.Literal: 100 | if term.Datatype != nil && len(term.Datatype.String()) > 0 { 101 | return NewLiteralWithLanguageAndDatatype(term.Value, term.Language, NewResource(term.Datatype.RawValue())) 102 | } 103 | return NewLiteral(term.Value) 104 | case *jsonld.Resource: 105 | return NewResource(term.RawValue()) 106 | } 107 | return nil 108 | } 109 | 110 | // One returns one triple based on a triple pattern of S, P, O objects 111 | func (g *Graph) One(s Term, p Term, o Term) *Triple { 112 | for triple := range g.IterTriples() { 113 | if isNilOrEquals(s, triple.Subject) && isNilOrEquals(p, triple.Predicate) && isNilOrEquals(o, triple.Object) { 114 | return triple 115 | } 116 | } 117 | return nil 118 | } 119 | 120 | // IterTriples iterates through all the triples in a graph 121 | func (g *Graph) IterTriples() (ch chan *Triple) { 122 | ch = make(chan *Triple) 123 | go func() { 124 | for triple := range g.triples { 125 | ch <- triple 126 | } 127 | close(ch) 128 | }() 129 | return ch 130 | } 131 | 132 | // Add is used to add a Triple object to the graph 133 | func (g *Graph) Add(t *Triple) { 134 | g.triples[t] = true 135 | } 136 | 137 | // AddTriple is used to add a triple made of individual S, P, O objects 138 | func (g *Graph) AddTriple(s Term, p Term, o Term) { 139 | g.triples[NewTriple(s, p, o)] = true 140 | } 141 | 142 | // Remove is used to remove a Triple object 143 | func (g *Graph) Remove(t *Triple) { 144 | delete(g.triples, t) 145 | } 146 | 147 | // All is used to return all triples that match a given pattern of S, P, O objects 148 | func (g *Graph) All(s Term, p Term, o Term) []*Triple { 149 | var triples []*Triple 150 | for triple := range g.IterTriples() { 151 | if s == nil && p == nil && o == nil { 152 | continue 153 | } 154 | 155 | if isNilOrEquals(s, triple.Subject) && isNilOrEquals(p, triple.Predicate) && isNilOrEquals(o, triple.Object) { 156 | triples = append(triples, triple) 157 | } 158 | } 159 | return triples 160 | } 161 | 162 | // AddStatement adds a Statement object 163 | func (g *Graph) AddStatement(st *crdf.Statement) { 164 | g.AddTriple(term2term(st.Subject), term2term(st.Predicate), term2term(st.Object)) 165 | } 166 | 167 | // Parse is used to parse RDF data from a reader, using the provided mime type 168 | func (g *Graph) Parse(reader io.Reader, mime string) { 169 | parserName := mimeParser[mime] 170 | if len(parserName) == 0 { 171 | parserName = "guess" 172 | } 173 | 174 | if parserName == "jsonld" { 175 | buf := new(bytes.Buffer) 176 | if _, err := buf.ReadFrom(reader); err != nil { 177 | log.Println(err) 178 | return 179 | } 180 | 181 | jsonData, err := jsonld.ReadJSON(buf.Bytes()) 182 | if err != nil { 183 | log.Println(err) 184 | return 185 | } 186 | 187 | options := &jsonld.Options{} 188 | options.Base = "" 189 | options.ProduceGeneralizedRdf = false 190 | dataSet, err := jsonld.ToRDF(jsonData, options) 191 | if err != nil { 192 | log.Println(err) 193 | return 194 | } 195 | 196 | for t := range dataSet.IterTriples() { 197 | g.AddTriple(jterm2term(t.Subject), jterm2term(t.Predicate), jterm2term(t.Object)) 198 | } 199 | 200 | return 201 | } 202 | 203 | parser := crdf.NewParser(parserName) 204 | parser.SetLogHandler(func(level int, message string) { 205 | log.Println(message) 206 | }) 207 | defer parser.Free() 208 | 209 | for s := range parser.Parse(reader, g.uri) { 210 | g.AddStatement(s) 211 | } 212 | } 213 | 214 | // ParseBase is used to parse RDF data from a reader, using the provided mime type and a base URI 215 | func (g *Graph) ParseBase(reader io.Reader, mime string, baseURI string) { 216 | if len(baseURI) < 1 { 217 | baseURI = g.uri 218 | } 219 | parserName := mimeParser[mime] 220 | if len(parserName) == 0 { 221 | parserName = "guess" 222 | } 223 | parser := crdf.NewParser(parserName) 224 | defer parser.Free() 225 | out := parser.Parse(reader, baseURI) 226 | for s := range out { 227 | g.AddStatement(s) 228 | } 229 | } 230 | 231 | // ReadFile is used to read RDF data from a file into the graph 232 | func (g *Graph) ReadFile(filename string) { 233 | stat, err := os.Stat(filename) 234 | if os.IsNotExist(err) { 235 | return 236 | } 237 | if stat.IsDir() { 238 | return 239 | } 240 | if !stat.IsDir() && err != nil { 241 | log.Println(err) 242 | return 243 | } 244 | f, err := os.OpenFile(filename, os.O_RDONLY, 0) 245 | defer f.Close() 246 | if err != nil { 247 | log.Println(err) 248 | return 249 | } 250 | g.Parse(f, "text/turtle") 251 | } 252 | 253 | // AppendFile is used to append RDF from a file, using a base URI 254 | func (g *Graph) AppendFile(filename string, baseURI string) { 255 | _, err := os.Stat(filename) 256 | if os.IsNotExist(err) { 257 | return 258 | } else if err != nil { 259 | log.Println(err) 260 | return 261 | } 262 | f, err := os.OpenFile(filename, os.O_RDONLY, 0) 263 | defer f.Close() 264 | if err != nil { 265 | log.Println(err) 266 | return 267 | } 268 | g.ParseBase(f, "text/turtle", baseURI) 269 | } 270 | 271 | // LoadURI is used to load RDF data from a specific URI 272 | func (g *Graph) LoadURI(uri string) (err error) { 273 | doc := defrag(uri) 274 | q, err := http.NewRequest("GET", doc, nil) 275 | if err != nil { 276 | return 277 | } 278 | q.Header.Set("Accept", "text/turtle,text/n3,application/rdf+xml") 279 | r, err := httpClient.Do(q) 280 | if err != nil { 281 | return 282 | } 283 | if r != nil { 284 | defer r.Body.Close() 285 | if r.StatusCode == 200 { 286 | g.ParseBase(r.Body, r.Header.Get("Content-Type"), doc) 287 | } else { 288 | err = fmt.Errorf("Could not fetch graph from %s - HTTP %d", uri, r.StatusCode) 289 | } 290 | } 291 | return 292 | } 293 | 294 | func term2C(t Term) crdf.Term { 295 | switch t := t.(type) { 296 | case *BlankNode: 297 | node := crdf.Blank(t.ID) 298 | return &node 299 | case *Resource: 300 | node := crdf.Uri(t.URI) 301 | return &node 302 | case *Literal: 303 | dt := "" 304 | if t.Datatype != nil { 305 | dt = t.Datatype.(*Resource).URI 306 | } 307 | node := crdf.Literal{ 308 | Value: t.Value, 309 | Datatype: dt, 310 | Lang: t.Language, 311 | } 312 | return &node 313 | } 314 | return nil 315 | } 316 | 317 | func (g *Graph) serializeJSONLd() ([]byte, error) { 318 | r := []map[string]interface{}{} 319 | for elt := range g.IterTriples() { 320 | one := map[string]interface{}{ 321 | "@id": elt.Subject.(*Resource).URI, 322 | } 323 | switch t := elt.Object.(type) { 324 | case *Resource: 325 | one[elt.Predicate.(*Resource).URI] = []map[string]string{ 326 | { 327 | "@id": t.URI, 328 | }, 329 | } 330 | break 331 | case *Literal: 332 | v := map[string]string{ 333 | "@value": t.Value, 334 | } 335 | if t.Datatype != nil && len(t.Datatype.String()) > 0 { 336 | v["@type"] = t.Datatype.String() 337 | } 338 | if len(t.Language) > 0 { 339 | v["@language"] = t.Language 340 | } 341 | one[elt.Predicate.(*Resource).URI] = []map[string]string{v} 342 | } 343 | r = append(r, one) 344 | } 345 | return json.Marshal(r) 346 | } 347 | 348 | // Serialize is used to serialize a graph based on a given mime type 349 | func (g *Graph) Serialize(mime string) (string, error) { 350 | if mime == "application/ld+json" { 351 | b, err := g.serializeJSONLd() 352 | return string(b), err 353 | } 354 | 355 | serializerName := mimeSerializer[mime] 356 | if len(serializerName) == 0 { 357 | serializerName = "turtle" 358 | } 359 | serializer := crdf.NewSerializer(serializerName) 360 | defer serializer.Free() 361 | 362 | ch := make(chan *crdf.Statement, 1024) 363 | go func() { 364 | for triple := range g.IterTriples() { 365 | ch <- &crdf.Statement{ 366 | Subject: term2C(triple.Subject), 367 | Predicate: term2C(triple.Predicate), 368 | Object: term2C(triple.Object), 369 | } 370 | } 371 | close(ch) 372 | }() 373 | return serializer.Serialize(ch, g.uri) 374 | } 375 | 376 | // WriteFile is used to dump RDF from a Graph into a file 377 | func (g *Graph) WriteFile(file *os.File, mime string) error { 378 | serializerName := mimeSerializer[mime] 379 | if len(serializerName) == 0 { 380 | serializerName = "turtle" 381 | } 382 | serializer := crdf.NewSerializer(serializerName) 383 | defer serializer.Free() 384 | err := serializer.SetFile(file, g.uri) 385 | if err != nil { 386 | return err 387 | } 388 | ch := make(chan *crdf.Statement, 1024) 389 | go func() { 390 | for triple := range g.IterTriples() { 391 | ch <- &crdf.Statement{ 392 | Subject: term2C(triple.Subject), 393 | Predicate: term2C(triple.Predicate), 394 | Object: term2C(triple.Object), 395 | } 396 | } 397 | close(ch) 398 | }() 399 | serializer.AddN(ch) 400 | return nil 401 | } 402 | 403 | type jsonPatch map[string]map[string][]struct { 404 | Value string `json:"value"` 405 | Type string `json:"type"` 406 | } 407 | 408 | // JSONPatch is used to perform a PATCH operation on a Graph using data from the reader 409 | func (g *Graph) JSONPatch(r io.Reader) error { 410 | v := make(jsonPatch) 411 | data, err := ioutil.ReadAll(r) 412 | if err != nil { 413 | return err 414 | } 415 | err = json.Unmarshal(data, &v) 416 | if err != nil { 417 | return err 418 | } 419 | base, _ := url.Parse(g.uri) 420 | for s, sv := range v { 421 | su, _ := base.Parse(s) 422 | for p, pv := range sv { 423 | pu, _ := base.Parse(p) 424 | subject := NewResource(su.String()) 425 | predicate := NewResource(pu.String()) 426 | for _, triple := range g.All(subject, predicate, nil) { 427 | g.Remove(triple) 428 | } 429 | for _, o := range pv { 430 | switch o.Type { 431 | case "uri": 432 | g.AddTriple(subject, predicate, NewResource(o.Value)) 433 | case "literal": 434 | g.AddTriple(subject, predicate, NewLiteral(o.Value)) 435 | } 436 | } 437 | } 438 | } 439 | return nil 440 | } 441 | 442 | // isNilOrEquals is a helper function returns true if first term is nil, otherwise checks equality 443 | func isNilOrEquals(t1 Term, t2 Term) bool { 444 | if t1 == nil { 445 | return true 446 | } 447 | 448 | return t2.Equal(t1) 449 | } 450 | -------------------------------------------------------------------------------- /graph_test.go: -------------------------------------------------------------------------------- 1 | package gold 2 | 3 | import ( 4 | "strings" 5 | "testing" 6 | 7 | jsonld "github.com/linkeddata/gojsonld" 8 | "github.com/stretchr/testify/assert" 9 | ) 10 | 11 | func TestJSONTerm2Term(t *testing.T) { 12 | term := jsonld.NewResource("http://test.org/") 13 | res1 := jterm2term(term) 14 | res2 := NewResource("http://test.org/") 15 | assert.True(t, res2.Equal(res1)) 16 | 17 | term = jsonld.NewLiteralWithDatatype("text", jsonld.NewResource("http://www.w3.org/2001/XMLSchema#hexBinary")) 18 | res1 = jterm2term(term) 19 | res2 = NewLiteralWithDatatype("text", NewResource("http://www.w3.org/2001/XMLSchema#hexBinary")) 20 | assert.True(t, res2.Equal(res1)) 21 | } 22 | 23 | func TestParseJSONLD(t *testing.T) { 24 | r := strings.NewReader(`{ "@id": "http://greggkellogg.net/foaf#me", "http://xmlns.com/foaf/0.1/name": "Gregg Kellogg" }`) 25 | g := NewGraph("https://test.org/") 26 | g.Parse(r, "application/ld+json") 27 | assert.Equal(t, 1, g.Len()) 28 | } 29 | 30 | func TestSerializeJSONLD(t *testing.T) { 31 | g := NewGraph("https://test.org/") 32 | g.AddTriple(NewResource("a"), NewResource("b"), NewResource("c")) 33 | assert.Equal(t, 1, g.Len()) 34 | toJSON, _ := g.Serialize("application/ld+json") 35 | assert.Equal(t, `[{"@id":"a","b":[{"@id":"c"}]}]`, toJSON) 36 | } 37 | 38 | func TestGraphPatch(t *testing.T) { 39 | var ( 40 | buf string 41 | err error 42 | graph = NewGraph("https://test/") 43 | ) 44 | 45 | graph.JSONPatch(strings.NewReader(`{"a":{"b":[{"type":"uri","value":"c"}]}}`)) 46 | buf, err = graph.Serialize("text/turtle") 47 | assert.Nil(t, err) 48 | assert.Equal(t, buf, "@prefix rdf: .\n\n\n .\n\n") 49 | 50 | graph.JSONPatch(strings.NewReader(`{"a":{"b":[{"type":"uri","value":"c2"}]}}`)) 51 | buf, err = graph.Serialize("text/turtle") 52 | assert.Nil(t, err) 53 | assert.Equal(t, buf, "@prefix rdf: .\n\n\n .\n\n") 54 | 55 | graph.JSONPatch(strings.NewReader(`{"a":{"b2":[{"type":"uri","value":"c2"}]}}`)) 56 | buf, err = graph.Serialize("text/turtle") 57 | assert.Nil(t, err) 58 | assert.Equal(t, buf, "@prefix rdf: .\n\n\n ;\n .\n\n") 59 | } 60 | 61 | func TestGraphOne(t *testing.T) { 62 | g := NewGraph("http://test/") 63 | 64 | g.AddTriple(NewResource("a"), NewResource("b"), NewResource("c")) 65 | assert.Equal(t, g.One(NewResource("a"), nil, nil).String(), " .") 66 | assert.Equal(t, g.One(NewResource("a"), NewResource("b"), nil).String(), " .") 67 | 68 | g.AddTriple(NewResource("a"), NewResource("b"), NewResource("d")) 69 | assert.Equal(t, g.One(NewResource("a"), NewResource("b"), NewResource("d")).String(), " .") 70 | assert.Equal(t, g.One(nil, NewResource("b"), NewResource("d")).String(), " .") 71 | 72 | g.AddTriple(NewResource("g"), NewResource("b2"), NewLiteral("e")) 73 | assert.Equal(t, g.One(nil, NewResource("b2"), nil).String(), " \"e\" .") 74 | assert.Equal(t, g.One(nil, nil, NewLiteral("e")).String(), " \"e\" .") 75 | 76 | assert.Nil(t, g.One(NewResource("x"), nil, nil)) 77 | assert.Nil(t, g.One(nil, NewResource("x"), nil)) 78 | assert.Nil(t, g.One(nil, nil, NewResource("x"))) 79 | } 80 | 81 | func TestGraphAll(t *testing.T) { 82 | g := NewGraph("http://test/") 83 | g.AddTriple(NewResource("a"), NewResource("b"), NewResource("c")) 84 | g.AddTriple(NewResource("a"), NewResource("b"), NewResource("d")) 85 | g.AddTriple(NewResource("a"), NewResource("f"), NewLiteral("h")) 86 | g.AddTriple(NewResource("g"), NewResource("b2"), NewResource("e")) 87 | g.AddTriple(NewResource("g"), NewResource("b2"), NewResource("c")) 88 | 89 | assert.Equal(t, 0, len(g.All(nil, nil, nil))) 90 | assert.Equal(t, 3, len(g.All(NewResource("a"), nil, nil))) 91 | assert.Equal(t, 2, len(g.All(nil, NewResource("b"), nil))) 92 | assert.Equal(t, 1, len(g.All(nil, nil, NewResource("d")))) 93 | assert.Equal(t, 2, len(g.All(nil, nil, NewResource("c")))) 94 | } 95 | -------------------------------------------------------------------------------- /init.go: -------------------------------------------------------------------------------- 1 | package gold 2 | 3 | import ( 4 | "log" 5 | ) 6 | 7 | func init() { 8 | log.SetFlags(log.Flags() | log.Lshortfile) 9 | } 10 | -------------------------------------------------------------------------------- /ldp.go: -------------------------------------------------------------------------------- 1 | package gold 2 | 3 | import ( 4 | "bytes" 5 | "crypto/md5" 6 | "crypto/rand" 7 | "encoding/hex" 8 | "fmt" 9 | "io" 10 | "io/ioutil" 11 | "os" 12 | "strings" 13 | ) 14 | 15 | type linkheader struct { 16 | uri string 17 | rel string 18 | } 19 | 20 | // Linkheaders holds the list of Link headers 21 | type Linkheaders struct { 22 | headers []*linkheader 23 | } 24 | 25 | type preferheader struct { 26 | omit []string 27 | include []string 28 | } 29 | 30 | // Preferheaders holds the list of Prefer headers 31 | type Preferheaders struct { 32 | headers []*preferheader 33 | } 34 | 35 | // ParsePreferHeader parses the LDP specific Prefer header 36 | func ParsePreferHeader(header string) *Preferheaders { 37 | ret := new(Preferheaders) 38 | 39 | for _, v := range strings.Split(header, ",") { 40 | item := new(preferheader) 41 | v = strings.TrimSpace(v) 42 | if strings.HasPrefix(v, "return=representation") { 43 | for _, s := range strings.Split(v, ";") { 44 | s = strings.TrimSpace(s) 45 | if strings.HasPrefix(s, "omit") { 46 | s = strings.TrimLeft(s, "omit=") 47 | s = strings.TrimLeft(s, "\"") 48 | s = strings.TrimRight(s, "\"") 49 | for _, u := range strings.Split(s, " ") { 50 | item.omit = append(item.omit, u) 51 | } 52 | } 53 | if strings.HasPrefix(s, "include") { 54 | s = strings.TrimLeft(s, "include=") 55 | s = strings.TrimLeft(s, "\"") 56 | s = strings.TrimRight(s, "\"") 57 | for _, u := range strings.Split(s, " ") { 58 | item.include = append(item.include, u) 59 | } 60 | } 61 | } 62 | ret.headers = append(ret.headers, item) 63 | } 64 | } 65 | 66 | return ret 67 | } 68 | 69 | // Omits returns the types of resources to omit when listing an LDPC 70 | func (p *Preferheaders) Omits() []string { 71 | var ret []string 72 | for _, v := range p.headers { 73 | for _, u := range v.omit { 74 | ret = append(ret, u) 75 | } 76 | } 77 | return ret 78 | } 79 | 80 | // Includes returns the types of resources to include when listing an LDPC 81 | func (p *Preferheaders) Includes() []string { 82 | var ret []string 83 | for _, v := range p.headers { 84 | for _, u := range v.include { 85 | ret = append(ret, u) 86 | } 87 | } 88 | return ret 89 | } 90 | 91 | // ParseLinkHeader is a generic Link header parser 92 | func ParseLinkHeader(header string) *Linkheaders { 93 | ret := new(Linkheaders) 94 | 95 | for _, v := range strings.Split(header, ", ") { 96 | item := new(linkheader) 97 | for _, s := range strings.Split(v, ";") { 98 | s = strings.TrimSpace(s) 99 | if strings.HasPrefix(s, "<") && strings.HasSuffix(s, ">") { 100 | s = strings.TrimLeft(s, "<") 101 | s = strings.TrimRight(s, ">") 102 | item.uri = s 103 | } else if strings.Index(s, "rel=") >= 0 { 104 | s = strings.TrimLeft(s, "rel=") 105 | 106 | if strings.HasPrefix(s, "\"") || strings.HasPrefix(s, "'") { 107 | s = s[1:] 108 | } 109 | if strings.HasSuffix(s, "\"") || strings.HasSuffix(s, "'") { 110 | s = s[:len(s)-1] 111 | } 112 | item.rel = s 113 | } 114 | } 115 | ret.headers = append(ret.headers, item) 116 | } 117 | return ret 118 | } 119 | 120 | // MatchRel attempts to match a Link header based on the rel value 121 | func (l *Linkheaders) MatchRel(rel string) string { 122 | for _, v := range l.headers { 123 | if v.rel == rel { 124 | return v.uri 125 | } 126 | } 127 | return "" 128 | } 129 | 130 | // MatchURI attempts to match a Link header based on the href value 131 | func (l *Linkheaders) MatchURI(uri string) bool { 132 | for _, v := range l.headers { 133 | if v.uri == uri { 134 | return true 135 | } 136 | } 137 | return false 138 | } 139 | 140 | // NewUUID generates a new UUID string 141 | func NewUUID() string { 142 | uuid := make([]byte, 16) 143 | io.ReadFull(rand.Reader, uuid) 144 | uuid[8] = uuid[8]&^0xc0 | 0x80 145 | uuid[6] = uuid[6]&^0xf0 | 0x40 146 | return hex.EncodeToString(uuid) 147 | } 148 | 149 | // NewETag generates ETag 150 | func NewETag(path string) (string, error) { 151 | var ( 152 | hash []byte 153 | md5s string 154 | err error 155 | ) 156 | stat, err := os.Stat(path) 157 | if err != nil { 158 | return "", err 159 | } 160 | if stat.IsDir() { 161 | if files, err := ioutil.ReadDir(path); err == nil { 162 | if len(files) == 0 { 163 | md5s += stat.ModTime().String() 164 | } 165 | for _, file := range files { 166 | md5s += file.ModTime().String() + fmt.Sprintf("%d", file.Size()) 167 | } 168 | } 169 | } else { 170 | md5s += stat.ModTime().String() + fmt.Sprintf("%d", stat.Size()) 171 | } 172 | h := md5.New() 173 | io.Copy(h, bytes.NewBufferString(md5s)) 174 | hash = h.Sum([]byte("")) 175 | 176 | return hex.EncodeToString(hash), err 177 | } 178 | -------------------------------------------------------------------------------- /ldp_test.go: -------------------------------------------------------------------------------- 1 | package gold 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestLinkHeaderParser(t *testing.T) { 10 | l := ParseLinkHeader("") 11 | assert.Equal(t, "", l.MatchRel("acl")) 12 | 13 | l = ParseLinkHeader("; rel='type'") 14 | assert.NotEmpty(t, l.headers) 15 | assert.True(t, l.MatchURI("http://www.w3.org/ns/ldp#Container")) 16 | assert.False(t, l.MatchURI("http://www.w3.org/ns/ldp#Resource")) 17 | assert.Equal(t, "http://www.w3.org/ns/ldp#Container", l.MatchRel("type")) 18 | 19 | l = ParseLinkHeader("; rel=\"type\", ; rel=\"type\"") 20 | assert.NotEmpty(t, l.headers) 21 | assert.True(t, l.MatchURI("http://www.w3.org/ns/ldp#Container")) 22 | assert.True(t, l.MatchURI("http://www.w3.org/ns/ldp#Resource")) 23 | } 24 | 25 | func TestPreferHeaderParser(t *testing.T) { 26 | l := ParsePreferHeader("return=representation; omit=\"http://www.w3.org/ns/ldp#PreferMembership http://www.w3.org/ns/ldp#PreferContainment\"") 27 | assert.NotEmpty(t, l.headers) 28 | assert.Equal(t, 2, len(l.Omits())) 29 | for i, uri := range l.Omits() { 30 | if i == 0 { 31 | assert.Equal(t, "http://www.w3.org/ns/ldp#PreferMembership", uri) 32 | } else { 33 | assert.Equal(t, "http://www.w3.org/ns/ldp#PreferContainment", uri) 34 | } 35 | } 36 | 37 | l = ParsePreferHeader("return=representation; include=\"http://www.w3.org/ns/ldp#PreferMembership http://www.w3.org/ns/ldp#PreferContainment\"") 38 | assert.NotEmpty(t, l.headers) 39 | assert.Equal(t, 2, len(l.Includes())) 40 | for i, uri := range l.Includes() { 41 | if i == 0 { 42 | assert.Equal(t, "http://www.w3.org/ns/ldp#PreferMembership", uri) 43 | } else { 44 | assert.Equal(t, "http://www.w3.org/ns/ldp#PreferContainment", uri) 45 | } 46 | } 47 | } 48 | 49 | func TestNewUUID(t *testing.T) { 50 | uuid := NewUUID() 51 | assert.Equal(t, 32, len(uuid)) 52 | } 53 | -------------------------------------------------------------------------------- /locks.go: -------------------------------------------------------------------------------- 1 | package gold 2 | 3 | import ( 4 | "sync" 5 | ) 6 | 7 | var ( 8 | locksL = new(sync.Mutex) 9 | locks = map[string]*sync.Mutex{} 10 | ) 11 | 12 | func lock(key string) func() { 13 | mu, ex := locks[key] 14 | if !ex { // TTAS 15 | locksL.Lock() 16 | mu, ex = locks[key] 17 | if !ex { 18 | locks[key] = new(sync.Mutex) 19 | } 20 | locksL.Unlock() 21 | mu = locks[key] 22 | } 23 | mu.Lock() 24 | return func() { 25 | mu.Unlock() 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /mime.go: -------------------------------------------------------------------------------- 1 | package gold 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "mime" 7 | "path/filepath" 8 | 9 | "github.com/gabriel-vasile/mimetype" 10 | crdf "github.com/presbrey/goraptor" 11 | 12 | "regexp" 13 | "sync" 14 | ) 15 | 16 | var mimeParser = map[string]string{ 17 | "application/ld+json": "jsonld", 18 | "application/json": "internal", 19 | "application/sparql-update": "internal", 20 | } 21 | 22 | var mimeSerializer = map[string]string{ 23 | "application/ld+json": "internal", 24 | "text/html": "internal", 25 | } 26 | 27 | var mimeRdfExt = map[string]string{ 28 | ".ttl": "text/turtle", 29 | ".n3": "text/n3", 30 | ".rdf": "application/rdf+xml", 31 | ".jsonld": "application/ld+json", 32 | } 33 | 34 | var rdfExtensions = []string{ 35 | ".ttl", 36 | ".n3", 37 | ".rdf", 38 | ".jsonld", 39 | } 40 | 41 | var ( 42 | serializerMimes = []string{} 43 | validMimeType = regexp.MustCompile(`^\w+/\w+$`) 44 | mutex = &sync.Mutex{} 45 | ) 46 | 47 | func init() { 48 | // add missing extensions 49 | for k, v := range mimeRdfExt { 50 | mime.AddExtensionType(k, v) 51 | } 52 | 53 | for _, syntax := range crdf.ParserSyntax { 54 | switch syntax.MimeType { 55 | case "", "text/html": 56 | continue 57 | } 58 | mimeParser[syntax.MimeType] = syntax.Name 59 | } 60 | mimeParser["text/n3"] = mimeParser["text/turtle"] 61 | 62 | for name, syntax := range crdf.SerializerSyntax { 63 | switch name { 64 | case "json-triples": 65 | // only activate: json 66 | continue 67 | case "rdfxml-xmp", "rdfxml": 68 | // only activate: rdfxml-abbrev 69 | continue 70 | } 71 | mimeSerializer[syntax.MimeType] = syntax.Name 72 | } 73 | 74 | for mime := range mimeSerializer { 75 | switch mime { 76 | case "application/xhtml+xml": 77 | continue 78 | } 79 | serializerMimes = append(serializerMimes, mime) 80 | } 81 | } 82 | 83 | func GuessMimeType(path string) (string, error) { 84 | mimeType := "text/plain" 85 | 86 | guessedType, _, err := mimetype.DetectFile(path) 87 | if err != nil { 88 | fmt.Printf("Unknown mimeType from file: %s ==> %s", path, err) 89 | return mimeType, err 90 | } 91 | 92 | if guessedType != "" && validMimeType.MatchString(guessedType) { 93 | mimeType = guessedType 94 | } 95 | 96 | return mimeType, nil 97 | } 98 | 99 | func LookupExt(ctype string) string { 100 | for k, v := range mimeRdfExt { 101 | if v == ctype { 102 | return k 103 | } 104 | } 105 | return "" 106 | } 107 | 108 | func LookUpCtype(ext string) string { 109 | return mimeRdfExt[ext] 110 | } 111 | 112 | func AddRDFExtension(ext string) { 113 | rdfExtensions = append(rdfExtensions, ext) 114 | } 115 | 116 | func IsRdfExtension(ext string) bool { 117 | for _, v := range rdfExtensions { 118 | if v == ext { 119 | return true 120 | } 121 | } 122 | return false 123 | } 124 | 125 | func MimeLookup(path string) (string, string, bool) { 126 | var mimeType string 127 | maybeRDF := false 128 | ext := filepath.Ext(path) 129 | if len(ext) > 0 { 130 | if IsRdfExtension(ext) { 131 | maybeRDF = true 132 | mimeType = LookUpCtype(ext) 133 | } else { 134 | mimeType = mime.TypeByExtension(ext) 135 | if len(mimeType) > 0 { 136 | if len(LookupExt(ext)) > 0 { 137 | maybeRDF = true 138 | } 139 | } 140 | } 141 | } 142 | return mimeType, ext, maybeRDF 143 | } 144 | 145 | // MapPathToExtension returns the path with the proper extension that matches the given content type, 146 | // even if the resource (path) contains a different extension 147 | // Only works with Go 1.5+ 148 | //@@TODO should switch to a more comprehensive list of mime-to-ext (instead of using go's internal list) 149 | func MapPathToExtension(path string, ctype string) (string, error) { 150 | if len(path) == 0 { 151 | return "", errors.New("MapPathToExt -- missing path or ctype value") 152 | } 153 | if path[len(path)-1:] == "/" { 154 | return path, nil 155 | } 156 | 157 | fileCType, ext, _ := MimeLookup(path) 158 | if len(fileCType) > 0 { 159 | fileCType, _, _ = mime.ParseMediaType(fileCType) 160 | if len(ctype) > 0 { 161 | if fileCType != ctype { 162 | // append the extension corresponding to Content-Type header 163 | newExt, err := mime.ExtensionsByType(ctype) 164 | if err != nil { 165 | return "", err 166 | } 167 | if len(newExt) > 0 { 168 | ext = newExt[0] 169 | } 170 | path += "$" + ext 171 | } 172 | } 173 | } else { 174 | if len(ext) > 0 { 175 | if len(ctype) > 0 { 176 | newExt, err := mime.ExtensionsByType(ctype) 177 | if err != nil { 178 | return "", err 179 | } 180 | if len(newExt) > 0 { 181 | match := false 182 | for _, e := range newExt { 183 | if e == ext { 184 | match = true 185 | break 186 | } 187 | } 188 | if !match { 189 | // could not find matching extension 190 | if !IsRdfExtension(newExt[0]) { 191 | path += "$" + newExt[0] 192 | } 193 | } 194 | } 195 | } 196 | } else { 197 | // !fileCtype, !ext, ctype 198 | if len(ctype) > 0 { 199 | // maybe it's an RDF resource 200 | if ext = LookupExt(ctype); len(ext) > 0 { 201 | path += ext 202 | } else { 203 | newExt, err := mime.ExtensionsByType(ctype) 204 | if err != nil { 205 | return "", err 206 | } 207 | if len(newExt) > 0 { 208 | path += newExt[0] 209 | } 210 | } 211 | } else { 212 | return "", errors.New("Cannot infer mime type from from empty file") 213 | } 214 | } 215 | } 216 | 217 | return path, nil 218 | } 219 | -------------------------------------------------------------------------------- /mime_test.go: -------------------------------------------------------------------------------- 1 | package gold 2 | 3 | import ( 4 | // "os" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | var ( 11 | mimeParserExpect = map[string]string{ 12 | // "application/json": "internal", 13 | "application/sparql-update": "internal", 14 | 15 | "application/ld+json": "jsonld", 16 | "application/rdf+xml": "rdfxml", 17 | "application/rss": "rss-tag-soup", 18 | "application/x-trig": "trig", 19 | "text/n3": "turtle", 20 | "text/turtle": "turtle", 21 | "text/x-nquads": "nquads", 22 | // "application/n-triples": "ntriples", 23 | } 24 | mimeSerializerExpect = map[string]string{ 25 | "application/ld+json": "internal", 26 | "text/html": "internal", 27 | 28 | "application/atom+xml": "atom", 29 | "application/json": "json", 30 | "application/rdf+xml": "rdfxml-abbrev", 31 | "application/rss+xml": "rss-1.0", 32 | "application/xhtml+xml": "html", 33 | "text/turtle": "turtle", 34 | "text/x-graphviz": "dot", 35 | "text/x-nquads": "nquads", 36 | // "application/n-triples": "ntriples", 37 | } 38 | ) 39 | 40 | func TestMimeParserExpect(t *testing.T) { 41 | for k, v := range mimeParserExpect { 42 | assert.Equal(t, v, mimeParser[k]) 43 | } 44 | } 45 | 46 | func TestMimeSerializerExpect(t *testing.T) { 47 | for k, v := range mimeSerializerExpect { 48 | assert.Equal(t, v, mimeSerializer[k]) 49 | } 50 | } 51 | 52 | func TestMapPathToExtension(t *testing.T) { 53 | // empty nil empty + error msg 54 | path := "" 55 | ctype := "" 56 | res, err := MapPathToExtension(path, ctype) 57 | assert.Error(t, err) 58 | assert.Empty(t, res) 59 | 60 | // /space/ nil /space/ 61 | path = "/space/" 62 | ctype = "" 63 | res, err = MapPathToExtension(path, ctype) 64 | assert.NoError(t, err) 65 | assert.Equal(t, path, res) 66 | 67 | // /space/ text/html /space/ 68 | path = "/space/" 69 | ctype = "text/html" 70 | res, err = MapPathToExtension(path, ctype) 71 | assert.NoError(t, err) 72 | assert.Equal(t, path, res) 73 | 74 | // /space/foo nil empty + error msg 75 | path = "/space/foo" 76 | ctype = "" 77 | res, err = MapPathToExtension(path, ctype) 78 | assert.Error(t, err) 79 | assert.Empty(t, res) 80 | 81 | // /space/foo.html nil /space/foo.html 82 | path = "/space/foo.html" 83 | ctype = "" 84 | res, err = MapPathToExtension(path, ctype) 85 | assert.NoError(t, err) 86 | assert.Equal(t, path, res) 87 | 88 | // /space/foo.html text/html /space/foo.html 89 | path = "/space/foo.html" 90 | ctype = "text/html" 91 | res, err = MapPathToExtension(path, ctype) 92 | assert.NoError(t, err) 93 | assert.Equal(t, path, res) 94 | 95 | // /space/foo.ttl nil /space/foo.ttl 96 | path = "/space/foo.ttl" 97 | ctype = "text/turtle" 98 | res, err = MapPathToExtension(path, ctype) 99 | assert.NoError(t, err) 100 | assert.Equal(t, path, res) 101 | 102 | // /space/foo.html text/turtle /space/foo.html$.ttl 103 | path = "/space/foo.html" 104 | ctype = "text/turtle" 105 | res, err = MapPathToExtension(path, ctype) 106 | assert.NoError(t, err) 107 | assert.Equal(t, path+"$.ttl", res) 108 | 109 | // /space/foo text/turtle /space/foo.ttl 110 | path = "/space/foo" 111 | ctype = "text/turtle" 112 | res, err = MapPathToExtension(path, ctype) 113 | assert.NoError(t, err) 114 | assert.Equal(t, path+".ttl", res) 115 | 116 | // /space/foo.acl text/turtle /space/foo.acl 117 | path = "/space/foo" + config.ACLSuffix 118 | ctype = "text/turtle" 119 | res, err = MapPathToExtension(path, ctype) 120 | assert.NoError(t, err) 121 | assert.Equal(t, path, res) 122 | 123 | // /space/foo.meta text/turtle /space/foo.acl 124 | path = "/space/foo" + config.MetaSuffix 125 | ctype = "text/turtle" 126 | res, err = MapPathToExtension(path, ctype) 127 | assert.NoError(t, err) 128 | assert.Equal(t, path, res) 129 | 130 | // /space/foo nil /space/foo.jpg$.htm 131 | path = "/space/foo" 132 | ctype = "text/html" 133 | res, err = MapPathToExtension(path, "") 134 | assert.Error(t, err) 135 | assert.Empty(t, res) 136 | 137 | // /space/foo.jpg text/html /space/foo.jpg$.htm 138 | path = "/space/foo.jpg" 139 | ctype = "text/html" 140 | res, err = MapPathToExtension(path, ctype) 141 | assert.NoError(t, err) 142 | assert.Contains(t, res, path+"$.htm") 143 | 144 | // /space/foo.exe text/html /space/foo.exe$.htm 145 | path = "/space/foo.exe" 146 | ctype = "text/html" 147 | res, err = MapPathToExtension(path, ctype) 148 | assert.NoError(t, err) 149 | assert.Contains(t, res, path+"$.htm") 150 | 151 | // /space/foo.ttl.acl text/html /space/foo.ttl.acl$.htm 152 | path = "/space/foo.ttl" + config.ACLSuffix 153 | ctype = "text/html" 154 | res, err = MapPathToExtension(path, ctype) 155 | assert.NoError(t, err) 156 | assert.Contains(t, res, path+"$.htm") 157 | 158 | // /space/foo.b4r text/html /space/foo.b4r$.htm 159 | path = "/space/foo.bar" 160 | ctype = "text/html" 161 | res, err = MapPathToExtension(path, ctype) 162 | assert.NoError(t, err) 163 | assert.Contains(t, res, path+"$.htm") 164 | } 165 | 166 | func TestLookUpCtype(t *testing.T) { 167 | cases := []struct { 168 | in, want string 169 | }{ 170 | {".ttl", "text/turtle"}, 171 | {".n3", "text/n3"}, 172 | {".rdf", "application/rdf+xml"}, 173 | {".jsonld", "application/ld+json"}, 174 | {".unrecognized_ext", ""}, 175 | } 176 | for _, c := range cases { 177 | assert.Equal(t, c.want, LookUpCtype(c.in)) 178 | } 179 | } 180 | -------------------------------------------------------------------------------- /pathinfo.go: -------------------------------------------------------------------------------- 1 | package gold 2 | 3 | import ( 4 | "errors" 5 | "net" 6 | "net/url" 7 | "os" 8 | _path "path" 9 | "path/filepath" 10 | "strings" 11 | "time" 12 | ) 13 | 14 | type pathInfo struct { 15 | Obj *url.URL 16 | URI string 17 | Base string 18 | Path string 19 | Root string 20 | File string 21 | FileType string 22 | ParentURI string 23 | AclURI string 24 | AclFile string 25 | MetaURI string 26 | MetaFile string 27 | Extension string 28 | MaybeRDF bool 29 | IsDir bool 30 | Exists bool 31 | ModTime time.Time 32 | Size int64 33 | } 34 | 35 | func (req *httpRequest) pathInfo(path string) (*pathInfo, error) { 36 | res := &pathInfo{} 37 | 38 | if len(path) == 0 { 39 | return nil, errors.New("missing resource path") 40 | } 41 | 42 | // hack - if source URI contains "one%2b+%2btwo" then it is 43 | // normally decoded to "one+ +two", but Go parses it to 44 | // "one+++two", so we replace the plus with a blank space 45 | // strings.Replace(path, "+", "%20", -1) 46 | 47 | p, err := url.Parse(path) 48 | if err != nil { 49 | return nil, err 50 | } 51 | 52 | res.Base = p.Scheme + "://" + p.Host 53 | res.Root = req.Server.Config.DataRoot 54 | // include host and port if running in vhosts mode 55 | host, port, _ := net.SplitHostPort(p.Host) 56 | if len(host) == 0 { 57 | host = p.Host 58 | } 59 | if len(port) > 0 { 60 | host += ":" + port 61 | } 62 | if req.Server.Config.Vhosts { 63 | res.Root = req.Server.Config.DataRoot + host + "/" 64 | res.Base = p.Scheme + "://" + host 65 | } 66 | 67 | // p.Path = p.String()[len(p.Scheme+"://"+p.Host):] 68 | if strings.HasPrefix(p.Path, "/") && len(p.Path) > 0 { 69 | p.Path = strings.TrimLeft(p.Path, "/") 70 | } 71 | 72 | if len(p.Path) == 0 { 73 | res.URI = p.String() + "/" 74 | } else { 75 | res.URI = p.String() 76 | } 77 | res.Obj = p 78 | res.File = p.Path 79 | res.Path = p.Path 80 | 81 | if req.Server.Config.Vhosts { 82 | res.File = res.Root + p.Path 83 | } else if len(req.Server.Config.DataRoot) > 0 { 84 | res.File = req.Server.Config.DataRoot + p.Path 85 | } 86 | 87 | res.Exists = true 88 | res.IsDir = false 89 | // check if file exits first 90 | if stat, err := os.Stat(res.File); os.IsNotExist(err) { 91 | res.Exists = false 92 | } else { 93 | res.ModTime = stat.ModTime() 94 | res.Size = stat.Size() 95 | // Add missing trailing slashes for dirs 96 | if stat.IsDir() { 97 | res.IsDir = true 98 | if !strings.HasSuffix(res.Path, "/") && len(res.Path) > 1 { 99 | res.Path += "/" 100 | res.File += "/" 101 | res.URI += "/" 102 | } 103 | } else { 104 | res.FileType, res.Extension, res.MaybeRDF = MimeLookup(res.File) 105 | if len(res.FileType) == 0 { 106 | res.FileType, err = GuessMimeType(res.File) 107 | if err != nil { 108 | req.Server.debug.Println(err) 109 | } 110 | } 111 | } 112 | } 113 | 114 | if len(res.Extension) == 0 { 115 | res.Extension = _path.Ext(res.File) 116 | } 117 | 118 | if strings.HasSuffix(res.Path, "/") { 119 | if filepath.Dir(filepath.Dir(res.Path)) == "." { 120 | res.ParentURI = res.Base + "/" 121 | } else { 122 | res.ParentURI = res.Base + "/" + filepath.Dir(filepath.Dir(res.Path)) + "/" 123 | } 124 | } else { 125 | res.ParentURI = res.Base + "/" + filepath.Dir(res.Path) + "/" 126 | } 127 | 128 | if strings.HasSuffix(res.Path, req.Server.Config.ACLSuffix) { 129 | res.AclURI = res.URI 130 | res.AclFile = res.File 131 | res.MetaURI = res.URI 132 | res.MetaFile = res.File 133 | } else if strings.HasSuffix(res.Path, req.Server.Config.MetaSuffix) { 134 | res.AclURI = res.URI + req.Server.Config.ACLSuffix 135 | res.AclFile = res.File + req.Server.Config.ACLSuffix 136 | res.MetaURI = res.URI 137 | res.MetaFile = res.File 138 | } else { 139 | res.AclURI = res.URI + req.Server.Config.ACLSuffix 140 | res.AclFile = res.File + req.Server.Config.ACLSuffix 141 | res.MetaURI = res.URI + req.Server.Config.MetaSuffix 142 | res.MetaFile = res.File + req.Server.Config.MetaSuffix 143 | } 144 | 145 | return res, nil 146 | } 147 | -------------------------------------------------------------------------------- /pathinfo_test.go: -------------------------------------------------------------------------------- 1 | package gold 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestPathInfoWithoutTrailingSlash(t *testing.T) { 10 | sroot := serverDefaultRoot() 11 | req := &httpRequest{nil, handler, "", "", "", false} 12 | p, err := req.pathInfo(testServer.URL) 13 | assert.Nil(t, err) 14 | assert.Equal(t, testServer.URL+"/", p.URI) 15 | assert.Equal(t, testServer.URL, p.Base) 16 | assert.Equal(t, "", p.Path) 17 | assert.Equal(t, sroot, p.File) 18 | assert.Equal(t, testServer.URL+"/"+config.ACLSuffix, p.AclURI) 19 | assert.Equal(t, sroot+config.ACLSuffix, p.AclFile) 20 | assert.Equal(t, testServer.URL+"/"+config.MetaSuffix, p.MetaURI) 21 | assert.Equal(t, sroot+config.MetaSuffix, p.MetaFile) 22 | assert.Empty(t, p.Extension) 23 | assert.True(t, p.Exists) 24 | } 25 | 26 | func TestPathInfoWithTrailingSlash(t *testing.T) { 27 | sroot := serverDefaultRoot() 28 | req := &httpRequest{nil, handler, "", "", "", false} 29 | 30 | p, err := req.pathInfo(testServer.URL + "/") 31 | assert.Nil(t, err) 32 | assert.Equal(t, testServer.URL, p.Base) 33 | assert.Equal(t, testServer.URL+"/", p.URI) 34 | assert.Equal(t, "", p.Path) 35 | assert.Equal(t, sroot, p.File) 36 | assert.Equal(t, testServer.URL+"/"+config.ACLSuffix, p.AclURI) 37 | assert.Equal(t, sroot+config.ACLSuffix, p.AclFile) 38 | assert.Equal(t, testServer.URL+"/"+config.MetaSuffix, p.MetaURI) 39 | assert.Equal(t, sroot+config.MetaSuffix, p.MetaFile) 40 | assert.Empty(t, p.Extension) 41 | assert.True(t, p.Exists) 42 | } 43 | 44 | func TestPathInfoWithPath(t *testing.T) { 45 | path := testServer.URL + "/_test/" 46 | sroot := serverDefaultRoot() 47 | req := &httpRequest{nil, handler, "", "", "", false} 48 | 49 | p, err := req.pathInfo(path) 50 | assert.Nil(t, err) 51 | assert.Equal(t, path, p.URI) 52 | assert.Equal(t, testServer.URL, p.Base) 53 | assert.Equal(t, "_test/", p.Path) 54 | assert.Equal(t, sroot+"_test/", p.File) 55 | assert.Equal(t, path+config.ACLSuffix, p.AclURI) 56 | assert.Equal(t, sroot+"_test/"+config.ACLSuffix, p.AclFile) 57 | assert.Equal(t, path+config.MetaSuffix, p.MetaURI) 58 | assert.Equal(t, sroot+"_test/"+config.MetaSuffix, p.MetaFile) 59 | assert.Empty(t, p.Extension) 60 | assert.True(t, p.Exists) 61 | } 62 | 63 | func TestPathInfoWithPathAndChildDir(t *testing.T) { 64 | path := testServer.URL + "/_test/" 65 | sroot := serverDefaultRoot() 66 | req := &httpRequest{nil, handler, "", "", "", false} 67 | 68 | p, err := req.pathInfo(path + "dir/") 69 | assert.Nil(t, err) 70 | assert.Equal(t, path+"dir/", p.URI) 71 | assert.Equal(t, testServer.URL, p.Base) 72 | assert.Equal(t, "_test/dir/", p.Path) 73 | assert.Equal(t, path, p.ParentURI) 74 | assert.Equal(t, sroot+"_test/dir/", p.File) 75 | assert.Equal(t, path+"dir/"+config.ACLSuffix, p.AclURI) 76 | assert.Equal(t, sroot+"_test/dir/"+config.ACLSuffix, p.AclFile) 77 | assert.Equal(t, path+"dir/"+config.MetaSuffix, p.MetaURI) 78 | assert.Equal(t, sroot+"_test/dir/"+config.MetaSuffix, p.MetaFile) 79 | assert.Empty(t, p.Extension) 80 | assert.False(t, p.Exists) 81 | } 82 | 83 | func TestPathInfoWithPathAndChildFile(t *testing.T) { 84 | path := testServer.URL + "/_test/" 85 | sroot := serverDefaultRoot() 86 | req := &httpRequest{nil, handler, "", "", "", false} 87 | 88 | p, err := req.pathInfo(path + "abc") 89 | assert.Nil(t, err) 90 | assert.Equal(t, path+"abc", p.URI) 91 | assert.Equal(t, testServer.URL, p.Base) 92 | assert.Equal(t, "_test/abc", p.Path) 93 | assert.Equal(t, path, p.ParentURI) 94 | assert.Equal(t, sroot+"_test/abc", p.File) 95 | assert.Equal(t, path+"abc"+config.ACLSuffix, p.AclURI) 96 | assert.Equal(t, sroot+"_test/abc"+config.ACLSuffix, p.AclFile) 97 | assert.Equal(t, path+"abc"+config.MetaSuffix, p.MetaURI) 98 | assert.Equal(t, sroot+"_test/abc"+config.MetaSuffix, p.MetaFile) 99 | assert.Empty(t, p.Extension) 100 | assert.False(t, p.Exists) 101 | } 102 | 103 | func TestPathInfoWithPathAndACLSuffix(t *testing.T) { 104 | path := testServer.URL + "/_test/" 105 | sroot := serverDefaultRoot() 106 | req := &httpRequest{nil, handler, "", "", "", false} 107 | 108 | p, err := req.pathInfo(path + config.ACLSuffix) 109 | assert.Nil(t, err) 110 | assert.Equal(t, path+config.ACLSuffix, p.URI) 111 | assert.Equal(t, testServer.URL, p.Base) 112 | assert.Equal(t, "_test/"+config.ACLSuffix, p.Path) 113 | assert.Equal(t, sroot+"_test/"+config.ACLSuffix, p.File) 114 | assert.Equal(t, path+config.ACLSuffix, p.AclURI) 115 | assert.Equal(t, sroot+"_test/"+config.ACLSuffix, p.AclFile) 116 | assert.Equal(t, path+config.ACLSuffix, p.MetaURI) 117 | assert.Equal(t, sroot+"_test/"+config.ACLSuffix, p.MetaFile) 118 | assert.Equal(t, config.ACLSuffix, p.Extension) 119 | assert.False(t, p.Exists) 120 | } 121 | 122 | func TestPathInfoWithPathAndMetaSuffix(t *testing.T) { 123 | path := testServer.URL + "/_test/" 124 | sroot := serverDefaultRoot() 125 | req := &httpRequest{nil, handler, "", "", "", false} 126 | 127 | p, err := req.pathInfo(path + config.MetaSuffix) 128 | assert.Nil(t, err) 129 | assert.Equal(t, path+config.MetaSuffix, p.URI) 130 | assert.Equal(t, testServer.URL, p.Base) 131 | assert.Equal(t, "_test/"+config.MetaSuffix, p.Path) 132 | assert.Equal(t, sroot+"_test/"+config.MetaSuffix, p.File) 133 | assert.Equal(t, path+config.MetaSuffix+config.ACLSuffix, p.AclURI) 134 | assert.Equal(t, sroot+"_test/"+config.MetaSuffix+config.ACLSuffix, p.AclFile) 135 | assert.Equal(t, path+config.MetaSuffix, p.MetaURI) 136 | assert.Equal(t, sroot+"_test/"+config.MetaSuffix, p.MetaFile) 137 | assert.Equal(t, config.MetaSuffix, p.Extension) 138 | assert.False(t, p.Exists) 139 | } 140 | -------------------------------------------------------------------------------- /pkg/apps/apps.go: -------------------------------------------------------------------------------- 1 | // Package apps provides functions for rendering application templates 2 | // TODO: implement strategy pattern 3 | package apps 4 | 5 | import( 6 | "strings" 7 | "html/template" 8 | "log" 9 | "fmt" 10 | "io/ioutil" 11 | 12 | "github.com/linkeddata/gold/pkg/routes" 13 | ) 14 | 15 | var( 16 | apps = map[string]string { 17 | "DataApp": "templates/tabulator.html", 18 | "404": routes.NotFound(), 19 | } 20 | templates = template.New("") 21 | ) 22 | 23 | // DataApp renders Data application template 24 | func DataApp()(string, error) { 25 | app, err := render("DataApp", template.URL(routes.Popup())) 26 | if err != nil { 27 | return "", fmt.Errorf("Failed to render DataApp: %v", err) 28 | } 29 | return app, nil 30 | } 31 | 32 | // NotFound returns 404 page 33 | func NotFound()(string, error) { 34 | app, err := render("404", nil) 35 | if err != nil { 36 | return "", fmt.Errorf("Failed to render NotFound: %v", err) 37 | } 38 | return app, nil 39 | } 40 | 41 | func render(name string, data interface{})(string, error) { 42 | var writer strings.Builder 43 | err := templates.ExecuteTemplate(&writer, name, data) 44 | if err != nil { 45 | return "", err 46 | } 47 | return writer.String(), nil 48 | } 49 | 50 | func init() { 51 | for app, path := range(apps) { 52 | tmpFile, err := ioutil.ReadFile(path) 53 | if err != nil { 54 | log.Panicf("Failed to read template file %s: %v", path, err) 55 | } 56 | 57 | _, err = templates.New(app).Parse(string(tmpFile)) 58 | if err != nil { 59 | log.Panicf("Failed to parse template for %s: %v", app, err) 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /pkg/routes/routes.go: -------------------------------------------------------------------------------- 1 | // Package routes provides helpers for routing 2 | package routes 3 | 4 | func NotFound() string { 5 | return "statics/404.html" 6 | } 7 | 8 | func Popup() string { 9 | return "statics/popup.html" 10 | } 11 | -------------------------------------------------------------------------------- /proxy.go: -------------------------------------------------------------------------------- 1 | package gold 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/elazarl/goproxy" 7 | ) 8 | 9 | var ( 10 | proxy = goproxy.NewProxyHttpServer() 11 | ) 12 | 13 | func init() { 14 | proxy.OnResponse().DoFunc(func(r *http.Response, ctx *goproxy.ProxyCtx) *http.Response { 15 | if r == nil { 16 | return r 17 | } 18 | r.Header.Set("Access-Control-Allow-Credentials", "true") 19 | r.Header.Set("Access-Control-Expose-Headers", "User, Location, Link, Vary, Last-Modified, WWW-Authenticate, Content-Length, Content-Type, Accept-Patch, Accept-Post, Allow, Updates-Via, Ms-Author-Via") 20 | r.Header.Set("Access-Control-Max-Age", "60") 21 | // Drop connection to allow for HTTP/2 <-> HTTP/1.1 compatibility 22 | r.Header.Del("Connection") 23 | return r 24 | }) 25 | } 26 | -------------------------------------------------------------------------------- /proxy_test.go: -------------------------------------------------------------------------------- 1 | package gold 2 | 3 | import ( 4 | "io/ioutil" 5 | "net/http" 6 | "strings" 7 | "testing" 8 | 9 | "github.com/stretchr/testify/assert" 10 | ) 11 | 12 | func TestProxyNoAuth(t *testing.T) { 13 | request, err := http.NewRequest("GET", testServer.URL+"/"+ProxyPath+"?uri="+testServer.URL+"/_test/", nil) 14 | assert.NoError(t, err) 15 | request.Header.Add("Origin", "example.org") 16 | response, err := httpClient.Do(request) 17 | assert.NoError(t, err) 18 | assert.Equal(t, 200, response.StatusCode) 19 | assert.Contains(t, response.Header.Get("Content-Type"), "text/turtle") 20 | assert.Equal(t, "example.org", response.Header.Get("Access-Control-Allow-Origin")) 21 | body, err := ioutil.ReadAll(response.Body) 22 | assert.NoError(t, err) 23 | response.Body.Close() 24 | assert.Contains(t, string(body), "") 25 | } 26 | 27 | func TestProxyQueryOPTION(t *testing.T) { 28 | request, err := http.NewRequest("OPTIONS", testServer.URL+"/"+QueryPath, nil) 29 | assert.NoError(t, err) 30 | request.Header.Add("Origin", "example.org") 31 | request.Header.Add("Content-Type", "test/tql") 32 | response, err := httpClient.Do(request) 33 | assert.NoError(t, err) 34 | assert.Equal(t, 200, response.StatusCode) 35 | assert.Equal(t, "example.org", response.Header.Get("Access-Control-Allow-Origin")) 36 | assert.True(t, strings.Contains(response.Header.Get("Access-Control-Expose-Headers"), "Content-Type")) 37 | } 38 | -------------------------------------------------------------------------------- /push.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # works with a file called VERSION in the current directory, 4 | # the contents of which should be a semantic version number 5 | # such as "1.2.3" 6 | # 7 | # Remember to enable tag following: 8 | # `git config --global push.followTags true` 9 | 10 | # this script will display the current version, automatically 11 | # suggest a "minor" version update, and ask for input to use 12 | # the suggestion, or a newly entered value. 13 | 14 | # once the new version number is determined, the script will 15 | # pull a list of changes from git history, prepend this to 16 | # a file called CHANGES (under the title of the new version 17 | # number) and create a GIT tag. 18 | 19 | if [ -f VERSION ]; then 20 | BASE_STRING=`cat VERSION` 21 | BASE_LIST=(`echo $BASE_STRING | tr '.' ' '`) 22 | V_MAJOR=${BASE_LIST[0]} 23 | V_MINOR=${BASE_LIST[1]} 24 | V_PATCH=${BASE_LIST[2]} 25 | echo "Current version : $BASE_STRING" 26 | V_MINOR=$((V_MINOR + 1)) 27 | V_PATCH=0 28 | SUGGESTED_VERSION="$V_MAJOR.$V_MINOR.$V_PATCH" 29 | read -p "Enter a version number [$SUGGESTED_VERSION]: " INPUT_STRING 30 | if [ "$INPUT_STRING" = "" ]; then 31 | INPUT_STRING=$SUGGESTED_VERSION 32 | fi 33 | echo "Will set new version to be $INPUT_STRING" 34 | echo $INPUT_STRING > VERSION 35 | echo "Version $INPUT_STRING:" > tmpfile 36 | git log --pretty=format:" - %s" "v$BASE_STRING"...HEAD >> tmpfile 37 | echo "" >> tmpfile 38 | echo "" >> tmpfile 39 | cat CHANGES >> tmpfile 40 | mv tmpfile CHANGES 41 | git add CHANGES VERSION 42 | git commit -m "Version bump to $INPUT_STRING" 43 | git tag -a -m "Tagging version $INPUT_STRING" "v$INPUT_STRING" 44 | git push --follow-tags 45 | else 46 | echo "Could not find a VERSION file" 47 | read -p "Do you want to create a version file and start from scratch? [y]" RESPONSE 48 | if [ "$RESPONSE" = "" ]; then RESPONSE="y"; fi 49 | if [ "$RESPONSE" = "Y" ]; then RESPONSE="y"; fi 50 | if [ "$RESPONSE" = "Yes" ]; then RESPONSE="y"; fi 51 | if [ "$RESPONSE" = "yes" ]; then RESPONSE="y"; fi 52 | if [ "$RESPONSE" = "YES" ]; then RESPONSE="y"; fi 53 | if [ "$RESPONSE" = "y" ]; then 54 | echo "0.1.0" > VERSION 55 | echo "Version 0.1.0" > CHANGES 56 | git log --pretty=format:" - %s" >> CHANGES 57 | echo "" >> CHANGES 58 | echo "" >> CHANGES 59 | git add VERSION CHANGES 60 | git commit -m "Added VERSION and CHANGES files, Version bump to v0.1.0" 61 | git tag -a -m "Tagging version 0.1.0" "v0.1.0" 62 | git push --follow-tags 63 | fi 64 | 65 | fi 66 | -------------------------------------------------------------------------------- /rdf.go: -------------------------------------------------------------------------------- 1 | package gold 2 | 3 | import ( 4 | "strings" 5 | ) 6 | 7 | var ( 8 | ns = struct { 9 | rdf, rdfs, acl, cert, foaf, stat, ldp, dct, space, st NS 10 | }{ 11 | rdf: NewNS("http://www.w3.org/1999/02/22-rdf-syntax-ns#"), 12 | rdfs: NewNS("http://www.w3.org/2000/01/rdf-schema#"), 13 | acl: NewNS("http://www.w3.org/ns/auth/acl#"), 14 | cert: NewNS("http://www.w3.org/ns/auth/cert#"), 15 | foaf: NewNS("http://xmlns.com/foaf/0.1/"), 16 | stat: NewNS("http://www.w3.org/ns/posix/stat#"), 17 | ldp: NewNS("http://www.w3.org/ns/ldp#"), 18 | dct: NewNS("http://purl.org/dc/terms/"), 19 | space: NewNS("http://www.w3.org/ns/pim/space#"), 20 | st: NewNS("http://www.w3.org/ns/solid/terms#"), 21 | } 22 | ) 23 | 24 | // NS is a generic namespace type 25 | type NS string 26 | 27 | // NewNS is used to set a new namespace 28 | func NewNS(base string) (ns NS) { 29 | return NS(base) 30 | } 31 | 32 | // Get is used to return the prefix for a namespace 33 | func (ns NS) Get(name string) (term Term) { 34 | return NewResource(string(ns) + name) 35 | } 36 | 37 | func brack(s string) string { 38 | if len(s) > 0 && s[0] == '<' { 39 | return s 40 | } 41 | if len(s) > 0 && s[len(s)-1] == '>' { 42 | return s 43 | } 44 | return "<" + s + ">" 45 | } 46 | 47 | func debrack(s string) string { 48 | if len(s) < 2 { 49 | return s 50 | } 51 | if s[0] != '<' { 52 | return s 53 | } 54 | if s[len(s)-1] != '>' { 55 | return s 56 | } 57 | return s[1 : len(s)-1] 58 | } 59 | 60 | func defrag(s string) string { 61 | lst := strings.Split(s, "#") 62 | if len(lst) != 2 { 63 | return s 64 | } 65 | return lst[0] 66 | } 67 | 68 | func unquote(s string) string { 69 | if len(s) < 2 { 70 | return s 71 | } 72 | if s[0] != '"' { 73 | return s 74 | } 75 | if s[len(s)-1] != '"' { 76 | return s 77 | } 78 | return s[1 : len(s)-1] 79 | } 80 | 81 | // frag = lambda x: x[x.find('#')==-1 and len(x) or x.find('#'):len(x)-(x[-1]=='>')] 82 | // unfrag = lambda x: '#' in x and (x[:x.find('#')==-1 and len(x) or x.find('#')] + (x[0]=='<' and '>' or '')) or x 83 | // cpfrag = lambda x,y: unfrag(y)[-1] == '>' and unfrag(y)[:-1]+frag(x)+'>' or unfrag(y)+frag(x) 84 | -------------------------------------------------------------------------------- /rdf_test.go: -------------------------------------------------------------------------------- 1 | package gold 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestRDFBrack(t *testing.T) { 10 | assert.Equal(t, "", brack("test")) 11 | assert.Equal(t, "", brack("test>")) 13 | } 14 | 15 | func TestRDFDebrack(t *testing.T) { 16 | assert.Equal(t, "a", debrack("a")) 17 | assert.Equal(t, "test", debrack("")) 18 | assert.Equal(t, "", debrack("test>")) 20 | } 21 | 22 | func TestDefrag(t *testing.T) { 23 | assert.Equal(t, "test", defrag("test")) 24 | assert.Equal(t, "test", defrag("test#me")) 25 | } 26 | -------------------------------------------------------------------------------- /server/daemon.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "crypto/tls" 5 | "flag" 6 | "log" 7 | "net" 8 | "net/http" 9 | "net/http/fcgi" 10 | "os" 11 | "strconv" 12 | "strings" 13 | 14 | "github.com/linkeddata/gold" 15 | ) 16 | 17 | var ( 18 | conf = flag.String("conf", "", "use this configuration file") 19 | 20 | httpA = flag.String("http", ":80", "HTTP listener address (redirects to HTTPS)") 21 | httpsA = flag.String("https", ":443", "HTTPS listener address") 22 | insecure = flag.Bool("insecure", false, "provide insecure/plain HTTP access (only)") 23 | nohttp = flag.Bool("nohttp", false, "disable HTTP redirects to HTTPS?") 24 | hsts = flag.Bool("enabbleHSTS", true, "enable strict transport security (HSTS)?") 25 | enableWebIDTLS = flag.Bool("enabbleWebIDTLS", true, "enable WebID-TLS authentication?") 26 | 27 | cookieT = flag.Int64("cookieAge", 24, "lifetime for cookies (in hours)") 28 | debug = flag.Bool("debug", false, "output extra logging?") 29 | root = flag.String("root", ".", "path to file storage root") 30 | app = flag.String("app", "tabulator", "default viewer app for HTML clients") 31 | tlsCert = flag.String("tlsCertFile", "", "TLS certificate eg. cert.pem") 32 | tlsKey = flag.String("tlsKeyFile", "", "TLS certificate eg. key.pem") 33 | vhosts = flag.Bool("vhosts", false, "run in virtual hosts mode?") 34 | bolt = flag.String("boltPath", "", "path to the location of the Bolt db file (uses /tmp/bolt.db by default)") 35 | 36 | metaSuffix = flag.String("metaSuffix", ",meta", "default suffix for meta files") 37 | aclSuffix = flag.String("aclSuffix", ",acl", "default suffix for ACL files") 38 | 39 | proxy = flag.String("proxy", "", "URL of the proxy service used for WebID-TLS delegation") 40 | local = flag.Bool("proxyLocal", true, "set to false to disable proxying of resource from local network") 41 | 42 | tokenT = flag.Int64("tokenAge", 5, "recovery token lifetime (in minutes)") 43 | 44 | salt = flag.String("salt", "", "used for storing hashed user passwords") 45 | 46 | agent = flag.String("agent", "", "WebID of the agent used for delegated authentication") 47 | 48 | emailName = flag.String("emailName", "", "remote SMTP server account name") 49 | emailAddr = flag.String("emailAddr", "", "remote SMTP server email address") 50 | emailUser = flag.String("emailUser", "", "remote SMTP server username") 51 | emailPass = flag.String("emailPass", "", "remote SMTP server password") 52 | emailServ = flag.String("emailServ", "", "remote SMTP server address / domain") 53 | emailPort = flag.String("emailPort", "", "remote SMTP port number") 54 | emailForceSSL = flag.Bool("emailForceSSL", false, "force SSL/TLS connection for remote SMTP server?") 55 | emailInsecure = flag.Bool("emailInsecure", false, "allow connections to insecure remote SMTP servers (self-signed certs)?") 56 | 57 | httpsPort string 58 | ) 59 | 60 | func init() { 61 | flag.Parse() 62 | } 63 | 64 | func redir(w http.ResponseWriter, req *http.Request) { 65 | host, _, _ := net.SplitHostPort(req.Host) 66 | if host == "" { 67 | host = req.Host 68 | } 69 | next := "https://" + host 70 | if httpsPort != "443" { 71 | next += ":" + httpsPort 72 | } 73 | http.Redirect(w, req, next+req.RequestURI, http.StatusMovedPermanently) 74 | } 75 | 76 | func main() { 77 | // Try to recover in case of panics 78 | defer func() { 79 | if rec := recover(); rec != nil { 80 | log.Println("\nRecovered from panic: ", rec) 81 | } 82 | }() 83 | 84 | serverRoot, err := os.Getwd() 85 | if err != nil { 86 | println("[Server] Error starting server:", err) 87 | os.Exit(1) 88 | } 89 | 90 | if *root == "." { 91 | *root = "" 92 | } 93 | 94 | if strings.HasPrefix(*root, serverRoot) || strings.HasPrefix(*root, "/") { 95 | serverRoot = *root 96 | } else { 97 | serverRoot = serverRoot + "/" + *root 98 | } 99 | if !strings.HasSuffix(serverRoot, "/") { 100 | serverRoot += "/" 101 | } 102 | 103 | config := gold.NewServerConfig() 104 | confLoaded := false 105 | if len(*conf) > 0 { 106 | err = config.LoadJSONFile(*conf) 107 | if err == nil { 108 | confLoaded = true 109 | } else { 110 | log.Println(err) 111 | } 112 | } 113 | if !confLoaded { 114 | config.ListenHTTP = *httpA 115 | config.ListenHTTPS = *httpsA 116 | config.TLSCert = *tlsCert 117 | config.TLSKey = *tlsKey 118 | config.WebIDTLS = *enableWebIDTLS 119 | config.Salt = *salt 120 | config.CookieAge = *cookieT 121 | config.TokenAge = *tokenT 122 | config.Debug = *debug 123 | config.DataRoot = serverRoot 124 | config.BoltPath = *bolt 125 | config.Vhosts = *vhosts 126 | config.Insecure = *insecure 127 | config.NoHTTP = *nohttp 128 | config.HSTS = *hsts 129 | config.MetaSuffix = *metaSuffix 130 | config.ACLSuffix = *aclSuffix 131 | config.Agent = *agent 132 | config.ProxyTemplate = *proxy 133 | config.ProxyLocal = *local 134 | if len(*emailName) > 0 && len(*emailAddr) > 0 && len(*emailUser) > 0 && 135 | len(*emailPass) > 0 && len(*emailServ) > 0 && len(*emailPort) > 0 { 136 | ep, _ := strconv.Atoi(*emailPort) 137 | config.SMTPConfig = gold.EmailConfig{ 138 | Name: *emailName, 139 | Addr: *emailAddr, 140 | User: *emailUser, 141 | Pass: *emailPass, 142 | Host: *emailServ, 143 | Port: ep, 144 | ForceSSL: *emailForceSSL, 145 | Insecure: *emailInsecure, 146 | } 147 | } 148 | } 149 | _, httpsPort, _ = net.SplitHostPort(config.ListenHTTPS) 150 | 151 | handler := gold.NewServer(config) 152 | 153 | // Start Bolt 154 | err = handler.StartBolt() 155 | if err != nil { 156 | log.Fatalln(err) 157 | } 158 | defer handler.BoltDB.Close() 159 | 160 | if os.Getenv("FCGI_ROLE") != "" { 161 | err = fcgi.Serve(nil, handler) 162 | if err != nil { 163 | log.Fatalln(err) 164 | } 165 | return 166 | } 167 | 168 | // Serve files from /statics/ 169 | if os.Getenv("SERVE_STATIC_FILES") == "true" { 170 | fs := http.FileServer(http.Dir("statics")) 171 | http.Handle("/statics/", http.StripPrefix("/statics/", fs)) 172 | } 173 | 174 | // Main handler 175 | http.Handle("/", handler) 176 | 177 | if config.Insecure { 178 | err = http.ListenAndServe(config.ListenHTTP, handler) 179 | if err != nil { 180 | log.Fatalln(err) 181 | } 182 | return 183 | } 184 | 185 | if !config.NoHTTP { 186 | go func() { 187 | err = http.ListenAndServe(config.ListenHTTP, http.HandlerFunc(redir)) 188 | if err != nil { 189 | log.Fatalln(err) 190 | } 191 | }() 192 | } 193 | 194 | var ( 195 | srv = &http.Server{Addr: config.ListenHTTPS} 196 | tcpL net.Listener 197 | tlsL net.Listener 198 | ) 199 | 200 | 201 | tlsConfig := NewTLSConfig(config.WebIDTLS) 202 | 203 | tlsConfig.Certificates = make([]tls.Certificate, 1) 204 | if len(config.TLSCert) == 0 && len(config.TLSKey) == 0 { 205 | tlsConfig.Certificates[0], err = tls.X509KeyPair(tlsTestCert, tlsTestKey) 206 | } else { 207 | tlsConfig.Certificates[0], err = tls.LoadX509KeyPair(config.TLSCert, config.TLSKey) 208 | } 209 | if err == nil { 210 | tcpL, err = net.Listen("tcp", config.ListenHTTPS) 211 | } 212 | if err == nil { 213 | tlsL = tls.NewListener(tcpL, tlsConfig) 214 | err = srv.Serve(tlsL) 215 | } 216 | if err != nil { 217 | log.Fatal(err) 218 | } 219 | } 220 | -------------------------------------------------------------------------------- /server/tls.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "crypto/tls" 5 | ) 6 | 7 | var ( 8 | tlsTestCert = []byte(`-----BEGIN CERTIFICATE----- 9 | MIIB4TCCAUygAwIBAgIBADALBgkqhkiG9w0BAQUwEjEQMA4GA1UEChMHQWNtZSBD 10 | bzAeFw0xNDAxMzAyMzUyMTlaFw0yNDAxMjgyMzUyMTlaMBIxEDAOBgNVBAoTB0Fj 11 | bWUgQ28wgZ8wDQYJKoZIhvcNAQEBBQADgY0AMIGJAoGBAMs8NmXX55GqvTRcIE2K 12 | 8ZoElA7xRuiIYPXFl6Zlt/xCYUzcxEEz2pKOX3jgYEzx4wG0hQ5bcNQMJWPftZ7K 13 | 6QBvDRWs8wVgrbeN8o9LelPDrPl40Zk96howpgek/nPd5AUt6y0/hV4CNVt07y+D 14 | 13BxZSEj1E8ZTwCwhQ9uGltPAgMBAAGjSzBJMA4GA1UdDwEB/wQEAwIAoDATBgNV 15 | HSUEDDAKBggrBgEFBQcDATAMBgNVHRMBAf8EAjAAMBQGA1UdEQQNMAuCCWxvY2Fs 16 | aG9zdDALBgkqhkiG9w0BAQUDgYEAawZEY85RZAKrROH3t1xuGLI+MIWmiFH5Z/aQ 17 | 3kA/v5YHLlygjbgxedgFEe9TodiMk9M7kUTmAM6vS2qYf+apAj2QHFFyR8xc/BZ2 18 | YHpBjeARoeg1ctbzCWeISB4BN7hOAQOojKcgaqbP49S5WG+ONfF6GuRE3oBJPJZf 19 | 1bRSET8= 20 | -----END CERTIFICATE-----`) 21 | tlsTestKey = []byte(`-----BEGIN RSA PRIVATE KEY----- 22 | MIICXQIBAAKBgQDLPDZl1+eRqr00XCBNivGaBJQO8UboiGD1xZemZbf8QmFM3MRB 23 | M9qSjl944GBM8eMBtIUOW3DUDCVj37WeyukAbw0VrPMFYK23jfKPS3pTw6z5eNGZ 24 | PeoaMKYHpP5z3eQFLestP4VeAjVbdO8vg9dwcWUhI9RPGU8AsIUPbhpbTwIDAQAB 25 | AoGAc00U25CzCvxf3V3K4dNLIIMqcJPIE9KTl7vjPn8E87PBOfchzJAbl/v4BD7f 26 | w6eTj3sX5b5Q86x0ZgYcJxudNiLJK8XrrYqpe9yMoQ4PsN2mL77VtxwiiDrINnW+ 27 | eWX5eavIXFd1d6cNbudPy/vS4MpOAMid/g/m53tH8V/ZPUkCQQD7DGcW5ra05dK7 28 | qpcj+TRQACe2VSgo78Li9DoifoU9vdx3pWWNxthdGfUlMXuAyl29sFXsxVE/ve47 29 | k7jf/YSTAkEAzz5j+F28XwRkC+2HEQFTk+CBDsV3iNFNcRFeQiaYXwI6OCmQQXDA 30 | pdmcjFqUzcKh7Wtx3G/Fz8hyifzr4/Xf1QJBAJgSjEP4H8b2zK93h7R32bN4VJYD 31 | gZ9ClYhLLwgEIgwjfXBQlXLLd/b1qWUNU2XRr/Ue4v3ZDP2SvMQEGOI+PNcCQQCF 32 | j3PmEKLhqXbAqSeusegnGTyTRHew2RLLl6Hjh/QS5uCWaVLqmbvOJtxZJ9dWc+Tf 33 | masboX0eV9RZUYLEuySxAkBLfEizykRCZ1CYkIUtKsq6HOtj+ELPBVtVPMCx3O10 34 | LMEOXuCrAMT/nApK629bgSlTU6P9PZd+05yRbHt4Ds1S 35 | -----END RSA PRIVATE KEY-----`) 36 | ) 37 | 38 | func NewTLSConfig(enableWebIDTLS bool) *tls.Config { 39 | tlsConfig := &tls.Config{ 40 | MinVersion: tls.VersionTLS12, 41 | PreferServerCipherSuites: true, 42 | CurvePreferences: []tls.CurveID{tls.CurveP521, tls.CurveP384, tls.CurveP256}, 43 | CipherSuites: []uint16{ 44 | tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, 45 | tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, 46 | tls.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA, 47 | tls.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA, 48 | tls.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA, 49 | tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA, 50 | }, 51 | NextProtos: []string{"h2"}, 52 | } 53 | if enableWebIDTLS { 54 | tlsConfig.ClientAuth = tls.RequestClientCert 55 | } 56 | return tlsConfig 57 | } 58 | -------------------------------------------------------------------------------- /smtp.go: -------------------------------------------------------------------------------- 1 | package gold 2 | 3 | import ( 4 | "crypto/tls" 5 | "fmt" 6 | "net/mail" 7 | "net/smtp" 8 | "strconv" 9 | "strings" 10 | ) 11 | 12 | type EmailStruct struct { 13 | To string 14 | ToName string 15 | From string 16 | FromName string 17 | Subject string 18 | Body string 19 | } 20 | 21 | // EmailConfig holds configuration values for remote SMTP servers 22 | type EmailConfig struct { 23 | // Name of the remote SMTP server account, i.e. Server admin 24 | Name string 25 | // Addr is the remote SMTP server email address, i.e. admin@server.org 26 | Addr string 27 | // User is the remote SMTP server username, i.e. admin 28 | User string 29 | // Pass is the remote SMTP server password 30 | Pass string 31 | // Host is the remote SMTP server IP address or domain 32 | Host string 33 | // Port is the remote SMTP server port number 34 | Port int 35 | // ForceSSL forces SSL/TLS connection instead of StartTLS 36 | ForceSSL bool 37 | // Insecure allows connections to insecure remote SMTP servers (self-signed certs) 38 | Insecure bool 39 | } 40 | 41 | func NewEmailStruct() *EmailStruct { 42 | return &EmailStruct{} 43 | } 44 | 45 | func (s *Server) sendWelcomeMail(params map[string]string) { 46 | email := NewEmailStruct() 47 | email.To = params["{{.To}}"] 48 | email.ToName = params["{{.Name}}"] 49 | email.From = params["{{.From}}"] 50 | email.FromName = "Notifications Service" 51 | email.Subject = "Welcome to " + params["{{.Host}}"] + "!" 52 | email.Body = parseMailTemplate("welcomeMail", params) 53 | 54 | s.sendMail(email) 55 | } 56 | 57 | func (s *Server) sendRecoveryMail(params map[string]string) { 58 | email := NewEmailStruct() 59 | email.To = params["{{.To}}"] 60 | email.ToName = params["{{.Name}}"] 61 | email.From = params["{{.From}}"] 62 | email.FromName = "Account Recovery" 63 | email.Subject = "Recovery instructions for your account on " + params["{{.Host}}"] 64 | email.Body = parseMailTemplate("accountRecovery", params) 65 | 66 | s.sendMail(email) 67 | } 68 | 69 | // should be run in a go routine 70 | func (s *Server) sendMail(email *EmailStruct) { 71 | if &s.Config.SMTPConfig == nil { 72 | s.debug.Println("Missing smtp server configuration") 73 | } 74 | smtpCfg := &s.Config.SMTPConfig 75 | 76 | auth := smtp.PlainAuth("", 77 | smtpCfg.User, 78 | smtpCfg.Pass, 79 | smtpCfg.Host, 80 | ) 81 | 82 | // Setup headers 83 | src := mail.Address{Name: email.FromName, Address: email.From} 84 | dst := mail.Address{Name: email.ToName, Address: email.To} 85 | headers := make(map[string]string) 86 | headers["From"] = src.String() 87 | headers["To"] = dst.String() 88 | headers["Subject"] = email.Subject 89 | headers["MIME-Version"] = "1.0" 90 | headers["Content-Type"] = "text/html; charset=\"utf-8\"" 91 | 92 | message := "" 93 | for k, v := range headers { 94 | message += fmt.Sprintf("%s: %s\r\n", k, v) 95 | } 96 | message += "\r\n" + email.Body 97 | 98 | if len(smtpCfg.Host) > 0 && smtpCfg.Port > 0 && auth != nil { 99 | smtpServer := smtpCfg.Host + ":" + strconv.Itoa(smtpCfg.Port) 100 | var err error 101 | // force upgrade to full SSL/TLS connection 102 | if smtpCfg.ForceSSL { 103 | err = s.sendSecureMail(src, dst, []byte(message), smtpCfg) 104 | } else { 105 | err = smtp.SendMail(smtpServer, auth, smtpCfg.Addr, []string{email.To}, []byte(message)) 106 | } 107 | if err != nil { 108 | s.debug.Println("Error sending recovery email to " + email.To + ": " + err.Error()) 109 | } else { 110 | s.debug.Println("Successfully sent recovery email to " + email.To) 111 | } 112 | } else { 113 | s.debug.Println("Missing smtp server and/or port") 114 | } 115 | } 116 | 117 | func (s *Server) sendSecureMail(from mail.Address, to mail.Address, msg []byte, cfg *EmailConfig) (err error) { 118 | // Connect to the SMTP Server 119 | serverName := cfg.Host + ":" + strconv.Itoa(cfg.Port) 120 | auth := smtp.PlainAuth("", cfg.User, cfg.Pass, cfg.Host) 121 | 122 | // TLS config 123 | tlsconfig := &tls.Config{ 124 | InsecureSkipVerify: s.Config.SMTPConfig.Insecure, 125 | ServerName: cfg.Host, 126 | } 127 | 128 | // Here is the key, you need to call tls.Dial instead of smtp.Dial 129 | // for smtp servers running on 465 that require an ssl connection 130 | // from the very beginning (no starttls) 131 | conn, err := tls.Dial("tcp", serverName, tlsconfig) 132 | if err != nil { 133 | s.debug.Println(err.Error()) 134 | return 135 | } 136 | defer conn.Close() 137 | 138 | c, err := smtp.NewClient(conn, cfg.Host) 139 | if err != nil { 140 | s.debug.Println(err.Error()) 141 | return 142 | } 143 | 144 | // Auth 145 | if err = c.Auth(auth); err != nil { 146 | s.debug.Println(err.Error()) 147 | return 148 | } 149 | 150 | // To && From 151 | if err = c.Mail(from.Address); err != nil { 152 | s.debug.Println(err.Error()) 153 | return 154 | } 155 | 156 | if err = c.Rcpt(to.Address); err != nil { 157 | s.debug.Println(err.Error()) 158 | return 159 | } 160 | 161 | // Data 162 | w, err := c.Data() 163 | if err != nil { 164 | s.debug.Println(err.Error()) 165 | return 166 | } 167 | 168 | _, err = w.Write(msg) 169 | if err != nil { 170 | s.debug.Println(err.Error()) 171 | return 172 | } 173 | 174 | err = w.Close() 175 | if err != nil { 176 | s.debug.Println(err.Error()) 177 | return 178 | } 179 | 180 | c.Quit() 181 | return nil 182 | } 183 | 184 | func sendWelcomeEmail() error { 185 | return nil 186 | } 187 | 188 | func parseMailTemplate(tpl string, vals map[string]string) string { 189 | body := SMTPTemplates[tpl] 190 | 191 | for oVal, nVal := range vals { 192 | body = strings.Replace(body, oVal, nVal, -1) 193 | } 194 | return body 195 | } 196 | -------------------------------------------------------------------------------- /smtp_test.go: -------------------------------------------------------------------------------- 1 | package gold 2 | 3 | import ( 4 | "bytes" 5 | "crypto/tls" 6 | "errors" 7 | "fmt" 8 | "io" 9 | "net" 10 | "strings" 11 | "testing" 12 | "time" 13 | 14 | "github.com/stretchr/testify/assert" 15 | ) 16 | 17 | var ( 18 | tlsConfig = &tls.Config{ 19 | NextProtos: []string{"http/1.1"}, 20 | } 21 | tlsTestCert = []byte(`-----BEGIN CERTIFICATE----- 22 | MIIB4TCCAUygAwIBAgIBADALBgkqhkiG9w0BAQUwEjEQMA4GA1UEChMHQWNtZSBD 23 | bzAeFw0xNDAxMzAyMzUyMTlaFw0yNDAxMjgyMzUyMTlaMBIxEDAOBgNVBAoTB0Fj 24 | bWUgQ28wgZ8wDQYJKoZIhvcNAQEBBQADgY0AMIGJAoGBAMs8NmXX55GqvTRcIE2K 25 | 8ZoElA7xRuiIYPXFl6Zlt/xCYUzcxEEz2pKOX3jgYEzx4wG0hQ5bcNQMJWPftZ7K 26 | 6QBvDRWs8wVgrbeN8o9LelPDrPl40Zk96howpgek/nPd5AUt6y0/hV4CNVt07y+D 27 | 13BxZSEj1E8ZTwCwhQ9uGltPAgMBAAGjSzBJMA4GA1UdDwEB/wQEAwIAoDATBgNV 28 | HSUEDDAKBggrBgEFBQcDATAMBgNVHRMBAf8EAjAAMBQGA1UdEQQNMAuCCWxvY2Fs 29 | aG9zdDALBgkqhkiG9w0BAQUDgYEAawZEY85RZAKrROH3t1xuGLI+MIWmiFH5Z/aQ 30 | 3kA/v5YHLlygjbgxedgFEe9TodiMk9M7kUTmAM6vS2qYf+apAj2QHFFyR8xc/BZ2 31 | YHpBjeARoeg1ctbzCWeISB4BN7hOAQOojKcgaqbP49S5WG+ONfF6GuRE3oBJPJZf 32 | 1bRSET8= 33 | -----END CERTIFICATE-----`) 34 | tlsTestKey = []byte(`-----BEGIN RSA PRIVATE KEY----- 35 | MIICXQIBAAKBgQDLPDZl1+eRqr00XCBNivGaBJQO8UboiGD1xZemZbf8QmFM3MRB 36 | M9qSjl944GBM8eMBtIUOW3DUDCVj37WeyukAbw0VrPMFYK23jfKPS3pTw6z5eNGZ 37 | PeoaMKYHpP5z3eQFLestP4VeAjVbdO8vg9dwcWUhI9RPGU8AsIUPbhpbTwIDAQAB 38 | AoGAc00U25CzCvxf3V3K4dNLIIMqcJPIE9KTl7vjPn8E87PBOfchzJAbl/v4BD7f 39 | w6eTj3sX5b5Q86x0ZgYcJxudNiLJK8XrrYqpe9yMoQ4PsN2mL77VtxwiiDrINnW+ 40 | eWX5eavIXFd1d6cNbudPy/vS4MpOAMid/g/m53tH8V/ZPUkCQQD7DGcW5ra05dK7 41 | qpcj+TRQACe2VSgo78Li9DoifoU9vdx3pWWNxthdGfUlMXuAyl29sFXsxVE/ve47 42 | k7jf/YSTAkEAzz5j+F28XwRkC+2HEQFTk+CBDsV3iNFNcRFeQiaYXwI6OCmQQXDA 43 | pdmcjFqUzcKh7Wtx3G/Fz8hyifzr4/Xf1QJBAJgSjEP4H8b2zK93h7R32bN4VJYD 44 | gZ9ClYhLLwgEIgwjfXBQlXLLd/b1qWUNU2XRr/Ue4v3ZDP2SvMQEGOI+PNcCQQCF 45 | j3PmEKLhqXbAqSeusegnGTyTRHew2RLLl6Hjh/QS5uCWaVLqmbvOJtxZJ9dWc+Tf 46 | masboX0eV9RZUYLEuySxAkBLfEizykRCZ1CYkIUtKsq6HOtj+ELPBVtVPMCx3O10 47 | LMEOXuCrAMT/nApK629bgSlTU6P9PZd+05yRbHt4Ds1S 48 | -----END RSA PRIVATE KEY-----`) 49 | ) 50 | 51 | type smtpServer struct { 52 | //array of commands 53 | Commands map[string]func(payload []byte) ([]byte, error) 54 | //function to be called, when no corresponding command is found 55 | CatchAll func(payload []byte) ([]byte, error) 56 | //error reporting function 57 | ErrorReporter func(err error) []byte 58 | //separator between command and payload 59 | Separator []byte 60 | } 61 | 62 | // Listen creates a server that listens for incoming SMTP connections 63 | func (srv *smtpServer) Listen(network, address string, secure bool) error { 64 | var l net.Listener 65 | var err error 66 | if secure { 67 | tlsConfig.Certificates = make([]tls.Certificate, 1) 68 | tlsConfig.Certificates[0], err = tls.X509KeyPair(tlsTestCert, tlsTestKey) 69 | if err != nil { 70 | return err 71 | } 72 | var tcpL net.Listener 73 | tcpL, err = net.Listen("tcp", address) 74 | if err != nil { 75 | return err 76 | } 77 | l = tls.NewListener(tcpL, tlsConfig) 78 | defer l.Close() 79 | } else { 80 | l, err = net.Listen(network, address) 81 | if err != nil { 82 | return err 83 | } 84 | defer l.Close() 85 | } 86 | for { 87 | conn, err := l.Accept() 88 | if err != nil { 89 | return err 90 | } 91 | go func(c net.Conn) { 92 | defer func() { 93 | e := recover() 94 | if e != nil { 95 | errorRecovered := e.(string) 96 | if errorRecovered != "" { 97 | fmt.Println(errorRecovered) 98 | c.Write(srv.ErrorReporter(errors.New(errorRecovered))) 99 | c.Close() 100 | } 101 | } 102 | }() 103 | c.Write([]byte("Commands available: \n")) 104 | for command := range srv.Commands { 105 | c.Write([]byte("# ")) 106 | c.Write([]byte(command)) 107 | c.Write([]byte(" [payload] \n")) 108 | } 109 | c.Write([]byte("What to do? ...\n")) 110 | 111 | //reading 112 | buf := make([]byte, 0, 4096) 113 | tmp := make([]byte, 256) 114 | for { 115 | n, err := conn.Read(tmp) 116 | if err != nil { 117 | if err != io.EOF { 118 | panic(err.Error()) 119 | } 120 | break 121 | } 122 | buf = append(buf, tmp[:n]...) 123 | } 124 | 125 | parsed := bytes.Split(buf, srv.Separator) 126 | command := strings.ToLower(string(parsed[0])) 127 | payload := bytes.Join(parsed[1:], srv.Separator) 128 | 129 | action, ok := srv.Commands[command] 130 | if !ok { 131 | action = srv.CatchAll 132 | } 133 | 134 | answer, err := action(payload) 135 | if err != nil { 136 | panic(err.Error()) 137 | } else { 138 | c.Write(answer) 139 | c.Close() 140 | } 141 | }(conn) 142 | } 143 | } 144 | 145 | func catchAllFunc(payload []byte) ([]byte, error) { 146 | return []byte(fmt.Sprintf("Catch all command with payload of %v", string(payload))), nil 147 | } 148 | 149 | func helloFunc(payload []byte) ([]byte, error) { 150 | return []byte(fmt.Sprintf("Hello command with payload of %v", string(payload))), nil 151 | } 152 | 153 | func badFunc(payload []byte) ([]byte, error) { 154 | return []byte(fmt.Sprintf("Bad command with payload of %v", string(payload))), errors.New("Ups, sorry!") 155 | } 156 | 157 | func errorReporter(err error) []byte { 158 | return []byte("Error processing the command: " + err.Error()) 159 | } 160 | 161 | func TestStartFakeSMTPServer(t *testing.T) { 162 | t.Parallel() 163 | var commands = make(map[string](func(payload []byte) ([]byte, error))) 164 | commands["hello"] = helloFunc 165 | commands["bad"] = badFunc 166 | separator := []byte(" ") 167 | go func() { 168 | serv := smtpServer{commands, catchAllFunc, errorReporter, separator} 169 | err := serv.Listen("tcp", ":3000", false) 170 | assert.NoError(t, err) 171 | }() 172 | } 173 | 174 | func TestStartFakeSecureSMTPServer(t *testing.T) { 175 | t.Parallel() 176 | var commands = make(map[string](func(payload []byte) ([]byte, error))) 177 | commands["hello"] = helloFunc 178 | commands["bad"] = badFunc 179 | separator := []byte(" ") 180 | go func() { 181 | serv := smtpServer{commands, catchAllFunc, errorReporter, separator} 182 | err := serv.Listen("tcp", ":3030", true) 183 | assert.NoError(t, err) 184 | }() 185 | } 186 | 187 | func TestFakeSMTPDial(t *testing.T) { 188 | t.Parallel() 189 | // give the server some time to start 190 | time.Sleep(200 * time.Millisecond) 191 | _, err := net.Dial("tcp", "localhost:3000") 192 | assert.NoError(t, err) 193 | } 194 | 195 | //func TestFakeSMTPSecureDial(t *testing.T) { 196 | // t.Parallel() 197 | // // give the server some time to start 198 | // time.Sleep(200 * time.Millisecond) 199 | // tlsconfig := &tls.Config{ 200 | // InsecureSkipVerify: true, 201 | // ServerName: "localhost", 202 | // } 203 | // _, err := tls.Dial("tcp", "localhost:3030", tlsconfig) 204 | // assert.NoError(t, err) 205 | //} 206 | -------------------------------------------------------------------------------- /sparqlupdate.go: -------------------------------------------------------------------------------- 1 | package gold 2 | 3 | import ( 4 | "bytes" 5 | "errors" 6 | "io" 7 | "io/ioutil" 8 | "strings" 9 | "text/scanner" 10 | ) 11 | 12 | // SPARQLUpdateQuery contains a verb, the body of the query and the graph 13 | type SPARQLUpdateQuery struct { 14 | verb string 15 | body string 16 | 17 | graph AnyGraph 18 | } 19 | 20 | // SPARQLUpdate contains the base URI and a list of queries 21 | type SPARQLUpdate struct { 22 | baseURI string 23 | queries []SPARQLUpdateQuery 24 | } 25 | 26 | // NewSPARQLUpdate creates a new SPARQL object 27 | func NewSPARQLUpdate(baseURI string) *SPARQLUpdate { 28 | return &SPARQLUpdate{ 29 | baseURI: baseURI, 30 | queries: []SPARQLUpdateQuery{}, 31 | } 32 | } 33 | 34 | // Parse parses a SPARQL query from the reader 35 | func (sparql *SPARQLUpdate) Parse(src io.Reader) error { 36 | b, _ := ioutil.ReadAll(src) 37 | s := new(scanner.Scanner).Init(bytes.NewReader(b)) 38 | s.Mode = scanner.ScanIdents | scanner.ScanStrings 39 | 40 | start := 0 41 | level := 0 42 | verb := "" 43 | tok := s.Scan() 44 | for tok != scanner.EOF { 45 | switch tok { 46 | case -2: 47 | if level == 0 { 48 | if len(verb) > 0 { 49 | verb += " " 50 | } 51 | verb += s.TokenText() 52 | } 53 | 54 | case 123: // { 55 | if level == 0 { 56 | start = s.Position.Offset 57 | } 58 | level++ 59 | 60 | case 125: // } 61 | level-- 62 | if level == 0 { 63 | query := SPARQLUpdateQuery{ 64 | body: string(b[start+1 : s.Position.Offset]), 65 | graph: NewGraph(sparql.baseURI), 66 | verb: verb, 67 | } 68 | query.graph.Parse(strings.NewReader(query.body), "text/turtle") 69 | sparql.queries = append(sparql.queries, query) 70 | } 71 | 72 | case 59: // ; 73 | if level == 0 { 74 | verb = "" 75 | } 76 | } 77 | 78 | tok = s.Scan() 79 | } 80 | 81 | return nil 82 | } 83 | 84 | // SPARQLUpdate is used to update a graph from a SPARQL query 85 | // Ugly, needs to be improved 86 | func (g *Graph) SPARQLUpdate(sparql *SPARQLUpdate) (int, error) { 87 | for _, query := range sparql.queries { 88 | if query.verb == "DELETE" || query.verb == "DELETE DATA" { 89 | for pattern := range query.graph.IterTriples() { 90 | found := false 91 | for _, triple := range g.All(pattern.Subject, pattern.Predicate, nil) { 92 | switch triple.Object.(type) { 93 | case *BlankNode: 94 | return 500, errors.New("bnodes are not supported!") 95 | default: 96 | if pattern.Object.Equal(triple.Object) { 97 | g.Remove(triple) 98 | found = true 99 | } 100 | } 101 | } 102 | if !found { 103 | return 409, errors.New("no matching triple found in graph!") 104 | } 105 | } 106 | } 107 | } 108 | for _, query := range sparql.queries { 109 | if query.verb == "INSERT" || query.verb == "INSERT DATA" { 110 | for triple := range query.graph.IterTriples() { 111 | g.Add(triple) 112 | } 113 | } 114 | } 115 | return 200, nil 116 | } 117 | -------------------------------------------------------------------------------- /sparqlupdate_test.go: -------------------------------------------------------------------------------- 1 | package gold 2 | 3 | import ( 4 | "strings" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func TestSPARQLParseInsert(t *testing.T) { 11 | sparql := NewSPARQLUpdate("https://test/") 12 | sparql.Parse(strings.NewReader("INSERT { . }")) 13 | assert.Equal(t, len(sparql.queries), 1) 14 | if len(sparql.queries) > 0 { 15 | assert.Equal(t, sparql.queries[0].verb, "INSERT") 16 | assert.Equal(t, sparql.queries[0].body, " . ") 17 | } 18 | 19 | sparql = NewSPARQLUpdate("https://test/") 20 | sparql.Parse(strings.NewReader("INSERT { . }")) 21 | assert.Equal(t, len(sparql.queries), 1) 22 | if len(sparql.queries) > 0 { 23 | assert.Equal(t, sparql.queries[0].verb, "INSERT") 24 | assert.Equal(t, sparql.queries[0].body, " . ") 25 | } 26 | } 27 | 28 | func TestSPARQLParseInsertDeleteUri(t *testing.T) { 29 | sparql := NewSPARQLUpdate("https://test/") 30 | sparql.Parse(strings.NewReader("INSERT DATA { . }; DELETE DATA { . }")) 31 | assert.Equal(t, len(sparql.queries), 2) 32 | if len(sparql.queries) > 1 { 33 | assert.Equal(t, sparql.queries[0].verb, "INSERT DATA") 34 | assert.Equal(t, sparql.queries[0].body, " . ") 35 | assert.Equal(t, sparql.queries[1].verb, "DELETE DATA") 36 | assert.Equal(t, sparql.queries[1].body, " . ") 37 | } 38 | } 39 | 40 | func TestSPARQLParseInsertDeleteLiteral(t *testing.T) { 41 | sparql := NewSPARQLUpdate("https://test/") 42 | sparql.Parse(strings.NewReader("INSERT DATA { \"};{\" . }; DELETE DATA { \"};{\" . }")) 43 | assert.Equal(t, len(sparql.queries), 2) 44 | if len(sparql.queries) > 1 { 45 | assert.Equal(t, sparql.queries[0].verb, "INSERT DATA") 46 | assert.Equal(t, sparql.queries[0].body, " \"};{\" . ") 47 | assert.Equal(t, sparql.queries[1].verb, "DELETE DATA") 48 | assert.Equal(t, sparql.queries[1].body, " \"};{\" . ") 49 | } 50 | } 51 | 52 | func TestSPARQLInsertLiteralWithDataType(t *testing.T) { 53 | sparql := NewSPARQLUpdate("https://test/") 54 | err := sparql.Parse(strings.NewReader("INSERT DATA { \"123\"^^ . }")) 55 | assert.NoError(t, err) 56 | assert.Equal(t, len(sparql.queries), 1) 57 | assert.Equal(t, "INSERT DATA", sparql.queries[0].verb) 58 | assert.Equal(t, " \"123\"^^ . ", sparql.queries[0].body) 59 | graph := NewGraph("https://test/") 60 | code, err := graph.SPARQLUpdate(sparql) 61 | assert.Equal(t, 200, code) 62 | assert.NoError(t, err) 63 | assert.Equal(t, 1, graph.Len()) 64 | } 65 | 66 | func TestSPARQLUpdateBnodePresent(t *testing.T) { 67 | graph := NewGraph("https://test/") 68 | sparql := NewSPARQLUpdate("https://test/") 69 | err := sparql.Parse(strings.NewReader("INSERT DATA { [ ] . }")) 70 | assert.NoError(t, err) 71 | code, err := graph.SPARQLUpdate(sparql) 72 | assert.Equal(t, 200, code) 73 | assert.NoError(t, err) 74 | assert.Equal(t, 2, graph.Len()) 75 | 76 | err = sparql.Parse(strings.NewReader("DELETE DATA { [ ] . }")) 77 | assert.NoError(t, err) 78 | code, err = graph.SPARQLUpdate(sparql) 79 | assert.Equal(t, 500, code) 80 | assert.Error(t, err) 81 | } 82 | 83 | func TestSPARQLUpdateTripleNotPresent(t *testing.T) { 84 | graph := NewGraph("https://test/") 85 | sparql := NewSPARQLUpdate("https://test/") 86 | err := sparql.Parse(strings.NewReader("INSERT DATA { . }")) 87 | assert.NoError(t, err) 88 | code, err := graph.SPARQLUpdate(sparql) 89 | assert.Equal(t, 200, code) 90 | assert.NoError(t, err) 91 | assert.Equal(t, 1, graph.Len()) 92 | 93 | err = sparql.Parse(strings.NewReader("DELETE DATA { . }")) 94 | assert.NoError(t, err) 95 | code, err = graph.SPARQLUpdate(sparql) 96 | assert.Equal(t, 409, code) 97 | assert.Error(t, err) 98 | } 99 | 100 | func TestSPARQLUpdateMultipleTriples(t *testing.T) { 101 | graph := NewGraph("https://test/") 102 | sparql := NewSPARQLUpdate("https://test/") 103 | err := sparql.Parse(strings.NewReader("INSERT DATA { . }; INSERT DATA { . }")) 104 | assert.NoError(t, err) 105 | code, err := graph.SPARQLUpdate(sparql) 106 | assert.Equal(t, 200, code) 107 | assert.NoError(t, err) 108 | assert.Equal(t, 2, graph.Len()) 109 | 110 | sparql = NewSPARQLUpdate("https://test/") 111 | err = sparql.Parse(strings.NewReader("DELETE DATA { . }; DELETE DATA { . }; INSERT DATA { . }")) 112 | assert.NoError(t, err) 113 | code, err = graph.SPARQLUpdate(sparql) 114 | assert.Equal(t, 200, code) 115 | assert.NoError(t, err) 116 | assert.Equal(t, 1, graph.Len()) 117 | } 118 | -------------------------------------------------------------------------------- /spkac.go: -------------------------------------------------------------------------------- 1 | package gold 2 | 3 | import ( 4 | "bytes" 5 | // "crypto/ecdsa" 6 | // "crypto/elliptic" 7 | "crypto/rand" 8 | "crypto/rsa" 9 | "crypto/sha1" 10 | "crypto/tls" 11 | "crypto/x509" 12 | "crypto/x509/pkix" 13 | "encoding/asn1" 14 | "encoding/base64" 15 | "encoding/pem" 16 | "errors" 17 | "math/big" 18 | ) 19 | 20 | type pkacInfo struct { 21 | Raw asn1.RawContent 22 | PublicKey publicKeyInfo 23 | Challenge string 24 | } 25 | 26 | type spkacInfo struct { 27 | Raw asn1.RawContent 28 | Pkac pkacInfo 29 | Algorithm pkix.AlgorithmIdentifier 30 | Signature asn1.BitString 31 | } 32 | 33 | type rsaPublicKey struct { 34 | N *big.Int 35 | E int 36 | } 37 | 38 | type publicKeyInfo struct { 39 | Raw asn1.RawContent 40 | Algorithm pkix.AlgorithmIdentifier 41 | PublicKey asn1.BitString 42 | } 43 | 44 | var ( 45 | oidPublicKeyRSA = asn1.ObjectIdentifier{1, 2, 840, 113549, 1, 1, 1} 46 | ) 47 | 48 | func parsePublicKey(algo x509.PublicKeyAlgorithm, keyData *publicKeyInfo) (interface{}, error) { 49 | asn1Data := keyData.PublicKey.RightAlign() 50 | switch algo { 51 | case x509.RSA: 52 | p := new(rsaPublicKey) 53 | _, err := asn1.Unmarshal(asn1Data, p) 54 | if err != nil { 55 | return nil, err 56 | } 57 | 58 | if p.N.Sign() <= 0 { 59 | return nil, errors.New("x509: RSA modulus is not a positive number") 60 | } 61 | if p.E <= 0 { 62 | return nil, errors.New("x509: RSA public exponent is not a positive number") 63 | } 64 | 65 | pub := &rsa.PublicKey{ 66 | E: p.E, 67 | N: p.N, 68 | } 69 | return pub, nil 70 | default: 71 | // DSA and EC not supported everywhere 72 | return nil, nil 73 | } 74 | } 75 | 76 | func getPublicKeyAlgorithmFromOID(oid asn1.ObjectIdentifier) x509.PublicKeyAlgorithm { 77 | if oid.Equal(oidPublicKeyRSA) { 78 | return x509.RSA 79 | } 80 | return x509.UnknownPublicKeyAlgorithm 81 | } 82 | 83 | // ParseSPKAC returns the public key from a KEYGEN's SPKAC request 84 | func ParseSPKAC(spkacBase64 string) (pub interface{}, err error) { 85 | var info spkacInfo 86 | derBytes, err := base64.StdEncoding.DecodeString(spkacBase64) 87 | if err != nil { 88 | return nil, err 89 | } 90 | 91 | if _, err = asn1.Unmarshal(derBytes, &info); err != nil { 92 | return 93 | } 94 | 95 | algo := getPublicKeyAlgorithmFromOID(info.Pkac.PublicKey.Algorithm.Algorithm) 96 | if algo == x509.UnknownPublicKeyAlgorithm { 97 | return nil, errors.New("x509: unknown public key algorithm") 98 | } 99 | 100 | pub, err = parsePublicKey(algo, &info.Pkac.PublicKey) 101 | if err != nil { 102 | return 103 | } 104 | 105 | return 106 | } 107 | 108 | // NewSPKACx509 creates a new x509 self-signed cert based on the SPKAC value 109 | func NewSPKACx509(uri string, name string, spkacBase64 string) ([]byte, error) { 110 | public, err := ParseSPKAC(spkacBase64) 111 | if err != nil { 112 | return nil, err 113 | } 114 | pubKey := public.(*rsa.PublicKey) 115 | rsaPub, err := x509.MarshalPKIXPublicKey(pubKey) 116 | if err != nil { 117 | return nil, err 118 | } 119 | h := sha1.New() 120 | pubSha1 := h.Sum(rsaPub)[:20] 121 | 122 | priv, err := rsa.GenerateKey(rand.Reader, 1024) 123 | if err != nil { 124 | return nil, err 125 | } 126 | 127 | template := x509.Certificate{ 128 | SerialNumber: new(big.Int).SetInt64(42), 129 | Subject: pkix.Name{ 130 | CommonName: name, 131 | Organization: []string{"WebID"}, 132 | // Country: []string{"US"}, 133 | }, 134 | NotBefore: notBefore, 135 | NotAfter: notAfter, 136 | 137 | SubjectKeyId: pubSha1, 138 | 139 | BasicConstraintsValid: true, 140 | } 141 | // add WebID in the subjectAltName field 142 | var rawValues []asn1.RawValue 143 | rawValues = append(rawValues, asn1.RawValue{Class: 2, Tag: 6, Bytes: []byte(uri)}) 144 | values, err := asn1.Marshal(rawValues) 145 | if err != nil { 146 | return nil, err 147 | } 148 | template.ExtraExtensions = []pkix.Extension{{Id: subjectAltName, Value: values}} 149 | template.Extensions = template.ExtraExtensions 150 | certDerBytes, err := x509.CreateCertificate(rand.Reader, &template, &template, public, priv) 151 | 152 | return certDerBytes, nil 153 | } 154 | 155 | // NewRSAcert creates a new RSA x509 self-signed certificate 156 | func NewRSAcert(uri string, name string, priv *rsa.PrivateKey) (*tls.Certificate, error) { 157 | uri = "URI: " + uri 158 | template := x509.Certificate{ 159 | SerialNumber: new(big.Int).SetInt64(42), 160 | Subject: pkix.Name{ 161 | CommonName: name, 162 | Organization: []string{"WebID"}, 163 | // Country: []string{"US"}, 164 | }, 165 | NotBefore: notBefore, 166 | NotAfter: notAfter, 167 | 168 | BasicConstraintsValid: true, 169 | KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature, 170 | ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth}, 171 | } 172 | rawValues := []asn1.RawValue{ 173 | {Class: 2, Tag: 6, Bytes: []byte(uri)}, 174 | } 175 | values, err := asn1.Marshal(rawValues) 176 | if err != nil { 177 | return nil, err 178 | } 179 | template.ExtraExtensions = []pkix.Extension{{Id: subjectAltName, Value: values}} 180 | 181 | keyPEM := bytes.NewBuffer(nil) 182 | err = pem.Encode(keyPEM, &pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(priv)}) 183 | if err != nil { 184 | return nil, err 185 | } 186 | 187 | derBytes, err := x509.CreateCertificate(rand.Reader, &template, &template, &priv.PublicKey, priv) 188 | if err != nil { 189 | return nil, err 190 | } 191 | certPEM := bytes.NewBuffer(nil) 192 | err = pem.Encode(certPEM, &pem.Block{Type: "CERTIFICATE", Bytes: derBytes}) 193 | if err != nil { 194 | return nil, err 195 | } 196 | 197 | cert, err := tls.X509KeyPair(certPEM.Bytes(), keyPEM.Bytes()) 198 | if err != nil { 199 | return nil, err 200 | } 201 | 202 | return &cert, nil 203 | } 204 | -------------------------------------------------------------------------------- /spkac_test.go: -------------------------------------------------------------------------------- 1 | package gold 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | // Generated using: 10 | // openssl spkac -key privkey.pem -challenge hello -out spkac.cnf 11 | var spkacRSABase64 = `MIICRTCCAS0wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDK/2gvbZk5wajwkX6wwhCrG39NetMycseg8nFgN23MKSLbjU/JspvCrk6jlHNs8d1+FcFyU/AHqYYiY60mSMymDetnho/iqW5sThziyOaVmQ7I7JM6Lqr1tD3376VTvq/1KKrIJrnyCEuxeysflFpS+uTY5X5YV5n8AUPQhjr0aJXnIAI0SryLd0KeSGb+p7uxlmKG7Q8mxl1wel3WXEFr1oVLa61BHfbO8IhrAV8bUBsc0tWX/OSZc611exX1XZ/f3ujxRaL96xraN7AS7/zNI024r4261jPnVTpdFwf2CcnfU7rwCjgcezfBDcIVOUliyUfh1QTRZEYS4LUUVHAHAgMBAAEWBWhlbGxvMA0GCSqGSIb3DQEBBAUAA4IBAQCIBcbE+nw/vpjLvdl7EVnX4TWpKxDej92MOafyaOjNmy/iVhto57Lr+jBhm0A1oHpmGXLarkQPSLcXndZJFm/WSdHZ5pids+fEpe9yyMhgYYkVqqNbnGQmgSrmRZjIbzF6J69SaYXqJ1jQAZ4RrxRsgimfUfGw3C59yytdqkqllg2ojZe158vRlO/X6ysyCevchT9InDAWXE8YM/LBaI6jSlAz1BUFw0phpnAWTpULjMoP45QelY26gfNT1oDD+7PXAiEeo101kba67UcKXr8/7Z05iUONvkE+X1nNLynpvSskz7hha0pjtR+ipDVL9vIQxBFZ1xwrbbOj1fmIKzaE` 12 | 13 | func TestParseSPKAC(t *testing.T) { 14 | _, err := ParseSPKAC(spkacRSABase64) 15 | assert.NoError(t, err) 16 | } 17 | 18 | // func TestCreateCertificateFromSPKAC(t *testing.T) { 19 | // uri := "https://example.org/person/card#me" 20 | // name := "User Test" 21 | 22 | // newSpkac, err := NewSPKACx509(uri, name, spkacRSABase64) 23 | // assert.NoError(t, err) 24 | 25 | // webid, err := WebIDFromCert(newSpkac) 26 | // assert.NoError(t, err) 27 | // assert.Equal(t, uri, webid) 28 | // } 29 | -------------------------------------------------------------------------------- /statics/404.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 |

404 - oh noes, there's nothing here

7 | 8 | 9 | -------------------------------------------------------------------------------- /templates.go: -------------------------------------------------------------------------------- 1 | package gold 2 | 3 | var ( 4 | // Apps contains a list of default apps that get server instead of RDF 5 | Apps = map[string]string{ 6 | "newCert": ` 7 | 8 | 9 |
10 |

Issue new certificate

11 | Name: 12 | WebID: 13 | 14 | 15 |
16 | 17 | `, 18 | "accountRecovery": ` 19 | 20 | 21 |

Recover access to your account

22 |
23 | What is your WebID? 24 |
25 | 26 | 27 |
28 | 29 | `, 30 | "401": ` 31 | 32 | 33 | 34 | 35 |

401 - Unauthorized! You need to authenticate to access this resource.

36 |
37 |

Login

38 | WebID: 39 |
40 | 41 |
42 | Password: 43 |
44 | 45 |
46 | 47 |
48 |

Forgot your password?

49 |
50 |

Do you need a WebID? You can sign up for one at databox.me.

51 | 52 | `, 53 | "403": ` 54 | 55 | 56 | 57 | 58 |

403 - oh noes, access denied!

59 |

Please visit the recovery page in case you have lost access to your credentials.

60 | 61 | `, 62 | } 63 | // SMTPTemplates contains a list of templates for sending emails 64 | SMTPTemplates = map[string]string{ 65 | "accountRecovery": `

Hello,

66 | 67 |

We have a received a request to recover you account, originating from {{.IP}}. Please ignore this email if you did not send this request.

68 | 69 |

Click the following link to recover your account: {{.Link}}

70 | 71 |

This email was generated automatically. No one will respond if you reply to it.

72 | 73 |

Sincerely, 74 |

{{.Host}} team

75 | `, 76 | "welcomeMail": `

Hi there {{.Name}}!

77 |
78 |

It looks like you have successfully created your Solid account on {{.Host}}. Congratulations!

79 | 80 |

Your WebID (identifier) is: {{.WebID}}.

81 | 82 |

You can start browsing your files here: {{.Account}}.

83 | 84 |

We would like to reassure you that we will not use your email address for any other purpose than allowing you to authenticate and/or recover your account credentials.

85 | 86 |

Best,

87 |

{{.Host}} team

88 | `, 89 | } 90 | ) 91 | 92 | func NewPassTemplate(token string, err string) string { 93 | template := ` 94 | 95 | 96 |
97 |

Please provide a new password

98 |

` + err + `

99 | Password: 100 |
101 | 102 |
103 | Password (type again to verify): 104 |
105 | 106 |
107 | 108 |
109 | 110 | ` 111 | return template 112 | } 113 | 114 | func LoginTemplate(redir, origin, webid string) string { 115 | template := ` 116 | 117 | 118 |
119 |

Login

120 | WebID: 121 |
122 | 123 |
124 | Password: 125 |
126 | 127 |
128 | 129 |
130 |

Forgot your password?

131 |
132 |

Do you need a WebID? You can sign up for one at databox.me.

133 | 134 | ` 135 | 136 | return template 137 | } 138 | 139 | func UnauthorizedTemplate(redirTo, webid string) string { 140 | template := ` 141 | 142 | 143 | 144 | 145 |

401 - Unauthorized! You need to authenticate to access this resource.

146 |
147 |

Login

148 | WebID: 149 |
150 | 151 |
152 | Password: 153 |
154 | 155 |
156 | 157 |
158 |

Forgot your password?

159 |
160 |

Do you need a WebID? You can sign up for one at databox.me.

161 | 162 | ` 163 | 164 | return template 165 | } 166 | 167 | func LogoutTemplate(webid string) string { 168 | template := ` 169 | 170 | 171 | 172 | 173 |

You are logged in as ` + webid + `.

174 |

Click here to logout

175 | 176 | ` 177 | return template 178 | } 179 | 180 | func TokensTemplate(tokens string) string { 181 | template := ` 182 | 183 | 184 | 185 | 186 | ` + tokens + ` 187 | 188 | ` 189 | return template 190 | } 191 | -------------------------------------------------------------------------------- /templates/404.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 |

404 - oh noes, there's nothing here

7 | 8 | 9 | -------------------------------------------------------------------------------- /templates/tabulator.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 10 | 11 | 32 | 33 | 34 |
35 |
36 |
37 | 38 | 39 | -------------------------------------------------------------------------------- /term.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2012 Kier Davis 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and 5 | associated documentation files (the "Software"), to deal in the Software without restriction, 6 | including without limitation the rights to use, copy, modify, merge, publish, distribute, 7 | sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all copies or substantial 11 | portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT 14 | NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 15 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES 16 | OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 17 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 18 | */ 19 | 20 | package gold 21 | 22 | import ( 23 | "fmt" 24 | "math/rand" 25 | "strings" 26 | ) 27 | 28 | // A Term is the value of a subject, predicate or object i.e. a IRI reference, blank node or 29 | // literal. 30 | type Term interface { 31 | // Method String should return the NTriples representation of this term. 32 | String() string 33 | 34 | // Method Equal should return whether this term is equal to another. 35 | Equal(Term) bool 36 | } 37 | 38 | // Resource is an URI / IRI reference. 39 | type Resource struct { 40 | URI string 41 | } 42 | 43 | // NewResource returns a new resource object. 44 | func NewResource(uri string) (term Term) { 45 | return Term(&Resource{URI: uri}) 46 | } 47 | 48 | // String returns the NTriples representation of this resource. 49 | func (term Resource) String() (str string) { 50 | return fmt.Sprintf("<%s>", term.URI) 51 | } 52 | 53 | // Equal returns whether this resource is equal to another. 54 | func (term Resource) Equal(other Term) bool { 55 | if spec, ok := other.(*Resource); ok { 56 | return term.URI == spec.URI 57 | } 58 | 59 | return false 60 | } 61 | 62 | // Literal is a textual value, with an associated language or datatype. 63 | type Literal struct { 64 | Value string 65 | Language string 66 | Datatype Term 67 | } 68 | 69 | // NewLiteral returns a new literal with the given value. 70 | func NewLiteral(value string) (term Term) { 71 | return Term(&Literal{Value: value}) 72 | } 73 | 74 | // NewLiteralWithLanguage returns a new literal with the given value and language. 75 | func NewLiteralWithLanguage(value string, language string) (term Term) { 76 | return Term(&Literal{Value: value, Language: language}) 77 | } 78 | 79 | // NewLiteralWithDatatype returns a new literal with the given value and datatype. 80 | func NewLiteralWithDatatype(value string, datatype Term) (term Term) { 81 | return Term(&Literal{Value: value, Datatype: datatype}) 82 | } 83 | 84 | // NewLiteralWithLanguageAndDatatype returns a new literal with the given value, language 85 | // and datatype. Technically a literal cannot have both a language and a datatype, but this function 86 | // is provided to allow creation of literal in a context where this check has already been made, 87 | // such as in a parser. 88 | func NewLiteralWithLanguageAndDatatype(value string, language string, datatype Term) (term Term) { 89 | return Term(&Literal{Value: value, Language: language, Datatype: datatype}) 90 | } 91 | 92 | // String returns the NTriples representation of this literal. 93 | func (term Literal) String() (str string) { 94 | str = term.Value 95 | str = strings.Replace(str, "\\", "\\\\", -1) 96 | str = strings.Replace(str, "\"", "\\\"", -1) 97 | str = strings.Replace(str, "\n", "\\n", -1) 98 | str = strings.Replace(str, "\r", "\\r", -1) 99 | str = strings.Replace(str, "\t", "\\t", -1) 100 | 101 | str = fmt.Sprintf("\"%s\"", str) 102 | 103 | if term.Language != "" { 104 | str += "@" + term.Language 105 | } else if term.Datatype != nil { 106 | str += "^^" + term.Datatype.String() 107 | } 108 | 109 | return str 110 | } 111 | 112 | // Equal returns whether this literal is equivalent to another. 113 | func (term Literal) Equal(other Term) bool { 114 | spec, ok := other.(*Literal) 115 | if !ok { 116 | return false 117 | } 118 | 119 | if term.Value != spec.Value { 120 | return false 121 | } 122 | 123 | if term.Language != spec.Language { 124 | return false 125 | } 126 | 127 | if (term.Datatype == nil && spec.Datatype != nil) || (term.Datatype != nil && spec.Datatype == nil) { 128 | return false 129 | } 130 | 131 | if term.Datatype != nil && spec.Datatype != nil && !term.Datatype.Equal(spec.Datatype) { 132 | return false 133 | } 134 | 135 | return true 136 | } 137 | 138 | // BlankNode is an RDF blank node i.e. an unqualified URI/IRI. 139 | type BlankNode struct { 140 | ID string 141 | } 142 | 143 | // NewBlankNode returns a new blank node with the given ID. 144 | func NewBlankNode(id string) (term Term) { 145 | return Term(&BlankNode{ID: id}) 146 | } 147 | 148 | // NewAnonNode returns a new blank node with a pseudo-randomly generated ID. 149 | func NewAnonNode() (term Term) { 150 | return Term(&BlankNode{ID: fmt.Sprintf("anon%016x", rand.Int63())}) 151 | } 152 | 153 | // String returns the NTriples representation of the blank node. 154 | func (term BlankNode) String() (str string) { 155 | return "_:" + term.ID 156 | } 157 | 158 | // Equal returns whether this blank node is equivalent to another. 159 | func (term BlankNode) Equal(other Term) bool { 160 | if spec, ok := other.(*BlankNode); ok { 161 | return term.ID == spec.ID 162 | } 163 | 164 | return false 165 | } 166 | -------------------------------------------------------------------------------- /term_test.go: -------------------------------------------------------------------------------- 1 | package gold 2 | 3 | import ( 4 | "github.com/stretchr/testify/assert" 5 | "strings" 6 | "testing" 7 | ) 8 | 9 | func TestLiteralEqual(t *testing.T) { 10 | var t1 Literal 11 | t1.Value = "test1" 12 | t1.Language = "en" 13 | 14 | assert.True(t, t1.Equal(NewLiteralWithLanguage("test1", "en"))) 15 | assert.False(t, t1.Equal(NewLiteralWithLanguage("test2", "en"))) 16 | 17 | assert.True(t, t1.Equal(NewLiteralWithLanguage("test1", "en"))) 18 | assert.False(t, t1.Equal(NewLiteralWithLanguage("test1", "fr"))) 19 | 20 | t1.Language = "" 21 | t1.Datatype = NewResource("http://www.w3.org/2001/XMLSchema#string") 22 | assert.False(t, t1.Equal(NewLiteral("test1"))) 23 | assert.True(t, t1.Equal(NewLiteralWithDatatype("test1", NewResource("http://www.w3.org/2001/XMLSchema#string")))) 24 | assert.False(t, t1.Equal(NewLiteralWithDatatype("test1", NewResource("http://www.w3.org/2001/XMLSchema#int")))) 25 | } 26 | 27 | func TestNewLiteralWithLanguage(t *testing.T) { 28 | s := NewLiteralWithLanguage("test", "en") 29 | assert.Equal(t, "\"test\"@en", s.String()) 30 | } 31 | 32 | func TestNewLiteralWithDatatype(t *testing.T) { 33 | s := NewLiteralWithDatatype("test", NewResource("http://www.w3.org/2001/XMLSchema#string")) 34 | assert.Equal(t, "\"test\"^^", s.String()) 35 | } 36 | 37 | func TestNewLiteralWithLanguageAndDatatype(t *testing.T) { 38 | s := NewLiteralWithLanguageAndDatatype("test", "en", NewResource("http://www.w3.org/2001/XMLSchema#string")) 39 | assert.Equal(t, "\"test\"@en", s.String()) 40 | 41 | s = NewLiteralWithLanguageAndDatatype("test", "", NewResource("http://www.w3.org/2001/XMLSchema#string")) 42 | assert.Equal(t, "\"test\"^^", s.String()) 43 | } 44 | 45 | func TestNewBlankNode(t *testing.T) { 46 | id := NewBlankNode("n1") 47 | assert.Equal(t, "_:n1", id.String()) 48 | } 49 | 50 | func TestNewAnonNode(t *testing.T) { 51 | id := NewAnonNode() 52 | assert.True(t, strings.Contains(id.String(), "_:anon")) 53 | } 54 | 55 | func TestBNodeEqual(t *testing.T) { 56 | var id1 BlankNode 57 | id1.ID = "n1" 58 | id2 := NewBlankNode("n1") 59 | assert.True(t, id1.Equal(id2)) 60 | id3 := NewBlankNode("n2") 61 | assert.False(t, id1.Equal(id3)) 62 | } 63 | -------------------------------------------------------------------------------- /tests/img.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/linkeddata/gold/8723e97756b636b6191e281a08e83cdffe406e65/tests/img.jpg -------------------------------------------------------------------------------- /triple.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2012 Kier Davis 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and 5 | associated documentation files (the "Software"), to deal in the Software without restriction, 6 | including without limitation the rights to use, copy, modify, merge, publish, distribute, 7 | sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all copies or substantial 11 | portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT 14 | NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 15 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES 16 | OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 17 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 18 | */ 19 | 20 | package gold 21 | 22 | import ( 23 | "fmt" 24 | ) 25 | 26 | // Triple contains a subject, a predicate and an object term. 27 | type Triple struct { 28 | Subject Term 29 | Predicate Term 30 | Object Term 31 | } 32 | 33 | // NewTriple returns a new triple with the given subject, predicate and object. 34 | func NewTriple(subject Term, predicate Term, object Term) (triple *Triple) { 35 | return &Triple{ 36 | Subject: subject, 37 | Predicate: predicate, 38 | Object: object, 39 | } 40 | } 41 | 42 | // String returns the NTriples representation of this triple. 43 | func (triple Triple) String() (str string) { 44 | subjStr := "nil" 45 | if triple.Subject != nil { 46 | subjStr = triple.Subject.String() 47 | } 48 | 49 | predStr := "nil" 50 | if triple.Predicate != nil { 51 | predStr = triple.Predicate.String() 52 | } 53 | 54 | objStr := "nil" 55 | if triple.Object != nil { 56 | objStr = triple.Object.String() 57 | } 58 | 59 | return fmt.Sprintf("%s %s %s .", subjStr, predStr, objStr) 60 | } 61 | 62 | // Equal returns this triple is equivalent to the argument. 63 | func (triple Triple) Equal(other *Triple) bool { 64 | return triple.Subject.Equal(other.Subject) && 65 | triple.Predicate.Equal(other.Predicate) && 66 | triple.Object.Equal(other.Object) 67 | } 68 | -------------------------------------------------------------------------------- /triple_test.go: -------------------------------------------------------------------------------- 1 | package gold 2 | 3 | import ( 4 | "github.com/stretchr/testify/assert" 5 | "testing" 6 | ) 7 | 8 | func TestTripleEquals(t *testing.T) { 9 | one := NewTriple(NewResource("a"), NewResource("b"), NewResource("c")) 10 | assert.True(t, one.Equal(NewTriple(NewResource("a"), NewResource("b"), NewResource("c")))) 11 | } 12 | -------------------------------------------------------------------------------- /webid.go: -------------------------------------------------------------------------------- 1 | package gold 2 | 3 | import ( 4 | "crypto/rand" 5 | "crypto/rsa" 6 | "crypto/sha1" 7 | "crypto/x509" 8 | "encoding/asn1" 9 | "encoding/base64" 10 | "errors" 11 | "fmt" 12 | "os" 13 | _path "path" 14 | "strconv" 15 | "strings" 16 | "sync" 17 | "time" 18 | "unicode" 19 | ) 20 | 21 | const ( 22 | rsaBits = 2048 23 | ) 24 | 25 | type webidAccount struct { 26 | Root string 27 | BaseURI string 28 | Document string 29 | WebID string 30 | PrefURI string 31 | PubTypeIndex string 32 | PrivTypeIndex string 33 | Name string 34 | Email string 35 | Agent string 36 | ProxyURI string 37 | QueryURI string 38 | Img string 39 | } 40 | 41 | type workspace struct { 42 | Name string 43 | Label string 44 | Type string 45 | } 46 | 47 | var ( 48 | subjectAltName = []int{2, 5, 29, 17} 49 | 50 | notBefore = time.Date(2000, 1, 1, 0, 0, 0, 0, time.UTC) 51 | notAfter = time.Date(2049, 12, 31, 23, 59, 59, 0, time.UTC) 52 | 53 | workspaces = []workspace{ 54 | {Name: "Preferences", Label: "Preferences workspace", Type: ""}, 55 | {Name: "Applications", Label: "Applications workspace", Type: "PreferencesWorkspace"}, 56 | {Name: "Inbox", Label: "Inbox", Type: ""}, 57 | } 58 | 59 | // cache 60 | webidL = new(sync.Mutex) 61 | pkeyURI = map[string]string{} 62 | ) 63 | 64 | func pkeyTypeNE(pkey interface{}) (t, n, e string) { 65 | switch pkey := pkey.(type) { 66 | //TODO: case *dsa.PublicKey 67 | case *rsa.PublicKey: 68 | t = "RSAPublicKey" 69 | n = fmt.Sprintf("%x", pkey.N) 70 | e = fmt.Sprintf("%d", pkey.E) 71 | } 72 | return 73 | } 74 | 75 | // WebIDDigestAuth performs a digest authentication using WebID-RSA 76 | func WebIDDigestAuth(req *httpRequest) (string, error) { 77 | if len(req.Header.Get("Authorization")) == 0 { 78 | return "", nil 79 | } 80 | 81 | authH, err := ParseDigestAuthorizationHeader(req.Header.Get("Authorization")) 82 | if err != nil { 83 | return "", err 84 | } 85 | 86 | if len(authH.Source) == 0 || authH.Source != req.BaseURI() { 87 | return "", errors.New("Bad source URI for auth token: " + authH.Source + " -- possible MITM attack!") 88 | } 89 | 90 | claim := sha1.Sum([]byte(authH.Source + authH.Username + authH.Nonce)) 91 | signature, err := base64.StdEncoding.DecodeString(authH.Signature) 92 | if err != nil { 93 | return "", errors.New(err.Error() + " in " + authH.Signature) 94 | } 95 | 96 | if len(authH.Username) == 0 || len(claim) == 0 || len(signature) == 0 { 97 | return "", errors.New("No WebID and/or claim found in the Authorization header.\n" + req.Header.Get("Authorization")) 98 | } 99 | 100 | // fetch WebID to get pubKey 101 | if !strings.HasPrefix(authH.Username, "http") { 102 | return "", errors.New("Username is not a valid HTTP URI: " + authH.Username) 103 | } 104 | 105 | // Decrypt and validate nonce from secure token 106 | tValues, err := ValidateSecureToken("WWW-Authenticate", authH.Nonce, req.Server) 107 | if err != nil { 108 | return "", err 109 | } 110 | v, err := strconv.ParseInt(tValues["valid"], 10, 64) 111 | if err != nil { 112 | return "", err 113 | } 114 | if time.Now().Local().Unix() > v { 115 | return "", errors.New("Token expired for " + authH.Username) 116 | } 117 | if len(tValues["secret"]) == 0 { 118 | return "", errors.New("Missing secret from token (tempered with?)") 119 | } 120 | if tValues["secret"] != string(req.Server.cookieSalt) { 121 | return "", errors.New("Wrong secret value in client token!") 122 | } 123 | 124 | g := NewGraph(authH.Username) 125 | err = g.LoadURI(authH.Username) 126 | if err != nil { 127 | return "", err 128 | } 129 | 130 | req.debug.Println("Checking for public keys for user", authH.Username) 131 | for _, keyT := range g.All(NewResource(authH.Username), ns.cert.Get("key"), nil) { 132 | for range g.All(keyT.Object, ns.rdf.Get("type"), ns.cert.Get("RSAPublicKey")) { 133 | req.debug.Println("Found RSA key in user's profile", keyT.Object.String()) 134 | for _, pubP := range g.All(keyT.Object, ns.cert.Get("pem"), nil) { 135 | keyP := term2C(pubP.Object).String() 136 | req.debug.Println("Found matching public key in user's profile", keyP[:10], "...", keyP[len(keyP)-10:len(keyP)]) 137 | parser, err := ParseRSAPublicPEMKey([]byte(keyP)) 138 | if err == nil { 139 | err = parser.Verify(claim[:], signature) 140 | if err == nil { 141 | return authH.Username, nil 142 | } 143 | } 144 | req.debug.Println("Unable to verify signature with key", keyP[:10], "...", keyP[len(keyP)-10:len(keyP)], "-- reason:", err) 145 | } 146 | // also loop through modulus/exp 147 | for _, pubN := range g.All(keyT.Object, ns.cert.Get("modulus"), nil) { 148 | keyN := term2C(pubN.Object).String() 149 | for _, pubE := range g.All(keyT.Object, ns.cert.Get("exponent"), nil) { 150 | keyE := term2C(pubE.Object).String() 151 | req.debug.Println("Found matching modulus and exponent in user's profile", keyN[:10], "...", keyN[len(keyN)-10:len(keyN)]) 152 | parser, err := ParseRSAPublicKeyNE("RSAPublicKey", keyN, keyE) 153 | if err == nil { 154 | err = parser.Verify(claim[:], signature) 155 | if err == nil { 156 | return authH.Username, nil 157 | } 158 | } 159 | req.debug.Println("Unable to verify signature with key", keyN[:10], "...", keyN[len(keyN)-10:len(keyN)], "-- reason:", err) 160 | } 161 | } 162 | } 163 | } 164 | 165 | return "", err 166 | } 167 | 168 | // WebIDTLSAuth - performs WebID-TLS authentication 169 | func WebIDTLSAuth(req *httpRequest) (uri string, err error) { 170 | tls := req.TLS 171 | claim := "" 172 | uri = "" 173 | err = nil 174 | 175 | if tls == nil || !tls.HandshakeComplete { 176 | return "", errors.New("Not a TLS connection. TLS handshake failed") 177 | } 178 | 179 | if len(tls.PeerCertificates) < 1 { 180 | return "", errors.New("No client certificate found in the TLS request!") 181 | } 182 | 183 | for _, x := range tls.PeerCertificates[0].Extensions { 184 | if !x.Id.Equal(subjectAltName) { 185 | continue 186 | } 187 | if len(x.Value) < 5 { 188 | continue 189 | } 190 | 191 | v := asn1.RawValue{} 192 | _, err = asn1.Unmarshal(x.Value, &v) 193 | if err == nil { 194 | san := "" 195 | for _, r := range string(v.Bytes[2:]) { 196 | if rune(r) == 65533 { 197 | san += "," 198 | } else if unicode.IsGraphic(rune(r)) { 199 | san += string(r) 200 | } 201 | } 202 | for _, sanURI := range strings.Split(san, ",") { 203 | sanURI = strings.TrimSpace(sanURI) 204 | if len(sanURI) == 0 { 205 | continue 206 | } 207 | if strings.HasPrefix(sanURI, "URI:") { 208 | claim = strings.TrimSpace(sanURI[4:]) 209 | break 210 | } else if strings.HasPrefix(sanURI, "http") { 211 | claim = sanURI 212 | break 213 | } 214 | } 215 | } 216 | if len(claim) == 0 || claim[:4] != "http" { 217 | continue 218 | } 219 | 220 | pkey := tls.PeerCertificates[0].PublicKey 221 | t, n, e := pkeyTypeNE(pkey) 222 | if len(t) == 0 { 223 | continue 224 | } 225 | 226 | pkeyk := fmt.Sprint([]string{t, n, e}) 227 | webidL.Lock() 228 | uri = pkeyURI[pkeyk] 229 | webidL.Unlock() 230 | if len(uri) > 0 { 231 | return 232 | } 233 | 234 | // pkey from client contains WebID claim 235 | 236 | g := NewGraph(claim) 237 | err = g.LoadURI(claim) 238 | if err != nil { 239 | return "", err 240 | } 241 | 242 | for _, keyT := range g.All(NewResource(claim), ns.cert.Get("key"), nil) { 243 | // found pkey in the profile 244 | for range g.All(keyT.Object, ns.rdf.Get("type"), ns.cert.Get(t)) { 245 | for range g.All(keyT.Object, ns.cert.Get("modulus"), NewLiteral(n)) { 246 | goto matchModulus 247 | } 248 | for range g.All(keyT.Object, ns.cert.Get("modulus"), NewLiteralWithDatatype(n, NewResource("http://www.w3.org/2001/XMLSchema#hexBinary"))) { 249 | goto matchModulus 250 | } 251 | matchModulus: 252 | // found a matching modulus in the profile 253 | for range g.All(keyT.Object, ns.cert.Get("exponent"), NewLiteral(e)) { 254 | goto matchExponent 255 | } 256 | for range g.All(keyT.Object, ns.cert.Get("exponent"), NewLiteralWithDatatype(e, NewResource("http://www.w3.org/2001/XMLSchema#int"))) { 257 | goto matchExponent 258 | } 259 | matchExponent: 260 | // found a matching exponent in the profile 261 | req.debug.Println("Found matching public modulus and exponent in user's profile") 262 | uri = claim 263 | webidL.Lock() 264 | pkeyURI[pkeyk] = uri 265 | webidL.Unlock() 266 | return 267 | } 268 | // could not find a certificate in the profile 269 | } 270 | // could not find a certificate pkey in the profile 271 | } 272 | return 273 | } 274 | 275 | // WebIDFromCert returns subjectAltName string from x509 []byte 276 | func WebIDFromCert(cert []byte) (string, error) { 277 | parsed, err := x509.ParseCertificate(cert) 278 | if err != nil { 279 | return "", err 280 | } 281 | 282 | for _, x := range parsed.Extensions { 283 | if x.Id.Equal(subjectAltName) { 284 | v := asn1.RawValue{} 285 | _, err = asn1.Unmarshal(x.Value, &v) 286 | if err != nil { 287 | return "", err 288 | } 289 | return string(v.Bytes[2:]), nil 290 | } 291 | } 292 | return "", nil 293 | } 294 | 295 | // AddProfileKeys creates a WebID profile graph and corresponding keys 296 | func AddProfileKeys(uri string, g *Graph) (*Graph, *rsa.PrivateKey, *rsa.PublicKey, error) { 297 | priv, err := rsa.GenerateKey(rand.Reader, rsaBits) 298 | if err != nil { 299 | return nil, nil, nil, err 300 | } 301 | pub := &priv.PublicKey 302 | 303 | profileURI := strings.Split(uri, "#")[0] 304 | userTerm := NewResource(uri) 305 | keyTerm := NewResource(profileURI + "#key") 306 | 307 | g.AddTriple(userTerm, ns.cert.Get("key"), keyTerm) 308 | g.AddTriple(keyTerm, ns.rdf.Get("type"), ns.cert.Get("RSAPublicKey")) 309 | g.AddTriple(keyTerm, ns.dct.Get("title"), NewLiteral("Created "+time.Now().Format(time.RFC822))) 310 | g.AddTriple(keyTerm, ns.cert.Get("modulus"), NewLiteralWithDatatype(fmt.Sprintf("%x", pub.N), NewResource("http://www.w3.org/2001/XMLSchema#hexBinary"))) 311 | g.AddTriple(keyTerm, ns.cert.Get("exponent"), NewLiteralWithDatatype(fmt.Sprintf("%d", pub.E), NewResource("http://www.w3.org/2001/XMLSchema#int"))) 312 | 313 | return g, priv, pub, nil 314 | } 315 | 316 | // AddCertKeys adds the modulus and exponent values to the profile document 317 | func (req *httpRequest) AddCertKeys(uri string, mod string, exp string) error { 318 | uuid := NewUUID() 319 | uuid = uuid[:4] 320 | 321 | profileURI := strings.Split(uri, "#")[0] 322 | userTerm := NewResource(uri) 323 | keyTerm := NewResource(profileURI + "#key" + uuid) 324 | 325 | resource, _ := req.pathInfo(profileURI) 326 | 327 | g := NewGraph(profileURI) 328 | g.ReadFile(resource.File) 329 | g.AddTriple(userTerm, ns.cert.Get("key"), keyTerm) 330 | g.AddTriple(keyTerm, ns.rdf.Get("type"), ns.cert.Get("RSAPublicKey")) 331 | g.AddTriple(keyTerm, ns.rdfs.Get("label"), NewLiteral("Created "+time.Now().Format(time.RFC822)+" on "+resource.Obj.Host)) 332 | g.AddTriple(keyTerm, ns.cert.Get("modulus"), NewLiteralWithDatatype(mod, NewResource("http://www.w3.org/2001/XMLSchema#hexBinary"))) 333 | g.AddTriple(keyTerm, ns.cert.Get("exponent"), NewLiteralWithDatatype(exp, NewResource("http://www.w3.org/2001/XMLSchema#int"))) 334 | 335 | // open account acl file 336 | f, err := os.OpenFile(resource.File, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0644) 337 | if err != nil { 338 | return err 339 | } 340 | defer f.Close() 341 | 342 | // write account acl to disk 343 | err = g.WriteFile(f, "text/turtle") 344 | if err != nil { 345 | return err 346 | } 347 | 348 | return nil 349 | } 350 | 351 | // NewWebIDProfile creates a WebID profile graph based on account data 352 | func NewWebIDProfile(account webidAccount) *Graph { 353 | profileURI := strings.Split(account.WebID, "#")[0] 354 | userTerm := NewResource(account.WebID) 355 | profileTerm := NewResource(profileURI) 356 | 357 | g := NewGraph(profileURI) 358 | g.AddTriple(profileTerm, ns.rdf.Get("type"), ns.foaf.Get("PersonalProfileDocument")) 359 | g.AddTriple(profileTerm, ns.foaf.Get("maker"), userTerm) 360 | g.AddTriple(profileTerm, ns.foaf.Get("primaryTopic"), userTerm) 361 | 362 | g.AddTriple(userTerm, ns.rdf.Get("type"), ns.foaf.Get("Person")) 363 | if len(account.Name) > 0 { 364 | g.AddTriple(profileTerm, ns.dct.Get("title"), NewLiteral("WebID profile of "+account.Name)) 365 | g.AddTriple(userTerm, ns.foaf.Get("name"), NewLiteral(account.Name)) 366 | } 367 | if len(account.Img) > 0 { 368 | g.AddTriple(userTerm, ns.foaf.Get("img"), NewResource(account.Img)) 369 | } 370 | if len(account.Agent) > 0 { 371 | g.AddTriple(userTerm, ns.acl.Get("delegates"), NewResource(account.Agent)) 372 | } 373 | g.AddTriple(userTerm, ns.space.Get("storage"), NewResource(account.BaseURI+"/")) 374 | g.AddTriple(userTerm, ns.space.Get("preferencesFile"), NewResource(account.PrefURI)) 375 | g.AddTriple(userTerm, ns.st.Get("privateTypeIndex"), NewResource(account.PrivTypeIndex)) 376 | g.AddTriple(userTerm, ns.st.Get("publicTypeIndex"), NewResource(account.PubTypeIndex)) 377 | g.AddTriple(userTerm, ns.ldp.Get("inbox"), NewResource(account.BaseURI+"/Inbox/")) 378 | g.AddTriple(userTerm, ns.st.Get("timeline"), NewResource(account.BaseURI+"/Timeline/")) 379 | 380 | // add proxy and query endpoints 381 | if len(account.ProxyURI) > 0 { 382 | g.AddTriple(userTerm, ns.st.Get("proxyTemplate"), NewResource(account.ProxyURI)) 383 | } 384 | if len(account.QueryURI) > 0 { 385 | g.AddTriple(userTerm, ns.st.Get("queryEndpoint"), NewResource(account.QueryURI)) 386 | } 387 | 388 | return g 389 | } 390 | 391 | // LinkToWebID links the account URI (root container) to the WebID that owns the space 392 | func (req *httpRequest) LinkToWebID(account webidAccount) error { 393 | resource, _ := req.pathInfo(account.BaseURI + "/") 394 | 395 | g := NewGraph(resource.URI) 396 | g.AddTriple(NewResource(account.WebID), ns.st.Get("account"), NewResource(resource.URI)) 397 | 398 | // open account root meta file 399 | f, err := os.OpenFile(resource.MetaFile, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0644) 400 | if err != nil { 401 | return err 402 | } 403 | defer f.Close() 404 | 405 | // write account meta file to disk 406 | err = g.WriteFile(f, "text/turtle") 407 | if err != nil { 408 | return err 409 | } 410 | 411 | return nil 412 | } 413 | 414 | func (req *httpRequest) getAccountWebID() string { 415 | resource, err := req.pathInfo(req.BaseURI()) 416 | if err == nil { 417 | resource, _ = req.pathInfo(resource.Base) 418 | g := NewGraph(resource.MetaURI) 419 | g.ReadFile(resource.MetaFile) 420 | if g.Len() >= 1 { 421 | webid := g.One(nil, ns.st.Get("account"), NewResource(resource.MetaURI)) 422 | if webid != nil { 423 | return debrack(webid.Subject.String()) 424 | } 425 | } 426 | } 427 | 428 | return "" 429 | } 430 | 431 | // AddWorkspaces creates all the necessary workspaces corresponding to a new account 432 | func (req *httpRequest) AddWorkspaces(account webidAccount, g *Graph) error { 433 | pref := NewGraph(account.PrefURI) 434 | prefTerm := NewResource(account.PrefURI) 435 | pref.AddTriple(prefTerm, ns.rdf.Get("type"), ns.space.Get("ConfigurationFile")) 436 | pref.AddTriple(prefTerm, ns.dct.Get("title"), NewLiteral("Preferences file")) 437 | 438 | pref.AddTriple(NewResource(account.WebID), ns.space.Get("preferencesFile"), NewResource(account.PrefURI)) 439 | pref.AddTriple(NewResource(account.WebID), ns.rdf.Get("type"), ns.foaf.Get("Person")) 440 | 441 | for _, ws := range workspaces { 442 | resource, _ := req.pathInfo(account.BaseURI + "/" + ws.Name + "/") 443 | err := os.MkdirAll(resource.File, 0755) 444 | if err != nil { 445 | return err 446 | } 447 | 448 | // Write ACLs 449 | // No one but the user is allowed access by default 450 | aclTerm := NewResource(resource.AclURI + "#owner") 451 | wsTerm := NewResource(resource.URI) 452 | a := NewGraph(resource.AclURI) 453 | a.AddTriple(aclTerm, ns.rdf.Get("type"), ns.acl.Get("Authorization")) 454 | a.AddTriple(aclTerm, ns.acl.Get("accessTo"), wsTerm) 455 | a.AddTriple(aclTerm, ns.acl.Get("accessTo"), NewResource(resource.AclURI)) 456 | a.AddTriple(aclTerm, ns.acl.Get("agent"), NewResource(account.WebID)) 457 | if len(req.FormValue("email")) > 0 { 458 | a.AddTriple(aclTerm, ns.acl.Get("agent"), NewResource("mailto:"+account.Email)) 459 | } 460 | a.AddTriple(aclTerm, ns.acl.Get("defaultForNew"), wsTerm) 461 | a.AddTriple(aclTerm, ns.acl.Get("mode"), ns.acl.Get("Read")) 462 | a.AddTriple(aclTerm, ns.acl.Get("mode"), ns.acl.Get("Write")) 463 | a.AddTriple(aclTerm, ns.acl.Get("mode"), ns.acl.Get("Control")) 464 | if ws.Type == "PublicWorkspace" { 465 | readAllTerm := NewResource(resource.AclURI + "#readall") 466 | a.AddTriple(readAllTerm, ns.rdf.Get("type"), ns.acl.Get("Authorization")) 467 | a.AddTriple(readAllTerm, ns.acl.Get("accessTo"), wsTerm) 468 | a.AddTriple(readAllTerm, ns.acl.Get("agentClass"), ns.foaf.Get("Agent")) 469 | a.AddTriple(readAllTerm, ns.acl.Get("mode"), ns.acl.Get("Read")) 470 | } 471 | // Special case for Inbox (append only) 472 | if ws.Name == "Inbox" { 473 | appendAllTerm := NewResource(resource.AclURI + "#apendall") 474 | a.AddTriple(appendAllTerm, ns.rdf.Get("type"), ns.acl.Get("Authorization")) 475 | a.AddTriple(appendAllTerm, ns.acl.Get("accessTo"), wsTerm) 476 | a.AddTriple(appendAllTerm, ns.acl.Get("agentClass"), ns.foaf.Get("Agent")) 477 | a.AddTriple(appendAllTerm, ns.acl.Get("mode"), ns.acl.Get("Append")) 478 | } 479 | 480 | // open account acl file 481 | f, err := os.OpenFile(resource.AclFile, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0644) 482 | if err != nil { 483 | return err 484 | } 485 | defer f.Close() 486 | 487 | // write account acl to disk 488 | err = a.WriteFile(f, "text/turtle") 489 | if err != nil { 490 | return err 491 | } 492 | 493 | // Append workspace URL to the preferencesFile 494 | //if ws.Name != "Inbox" || ws.Name != "Timeline" { <- this assertion is always true ... 495 | pref.AddTriple(wsTerm, ns.rdf.Get("type"), ns.space.Get("Workspace")) 496 | if len(ws.Type) > 0 { 497 | pref.AddTriple(wsTerm, ns.rdf.Get("type"), ns.space.Get(ws.Type)) 498 | } 499 | pref.AddTriple(wsTerm, ns.dct.Get("title"), NewLiteral(ws.Label)) 500 | 501 | pref.AddTriple(NewResource(account.WebID), ns.space.Get("workspace"), wsTerm) 502 | //} 503 | } 504 | 505 | resource, _ := req.pathInfo(account.PrefURI) 506 | err := os.MkdirAll(_path.Dir(resource.File), 0755) 507 | if err != nil { 508 | return err 509 | } 510 | // open account acl file 511 | f, err := os.OpenFile(resource.File, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0644) 512 | if err != nil { 513 | return err 514 | } 515 | 516 | // write account acl to disk 517 | err = pref.WriteFile(f, "text/turtle") 518 | if err != nil { 519 | return err 520 | } 521 | f.Close() 522 | 523 | // write the typeIndex 524 | createTypeIndex(req, "ListedDocument", account.PubTypeIndex) 525 | createTypeIndex(req, "UnlistedDocument", account.PrivTypeIndex) 526 | 527 | return nil 528 | } 529 | 530 | func createTypeIndex(req *httpRequest, indexType, url string) error { 531 | typeIndex := NewGraph(url) 532 | typeIndex.AddTriple(NewResource(url), ns.rdf.Get("type"), ns.st.Get("TypeIndex")) 533 | typeIndex.AddTriple(NewResource(url), ns.rdf.Get("type"), ns.st.Get(indexType)) 534 | 535 | resource, _ := req.pathInfo(url) 536 | err := os.MkdirAll(_path.Dir(resource.File), 0755) 537 | if err != nil { 538 | return err 539 | } 540 | // open account acl file 541 | f, err := os.OpenFile(resource.File, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0644) 542 | if err != nil { 543 | return err 544 | } 545 | defer f.Close() 546 | 547 | // write account acl to disk 548 | err = typeIndex.WriteFile(f, "text/turtle") 549 | return err 550 | } 551 | -------------------------------------------------------------------------------- /webid_test.go: -------------------------------------------------------------------------------- 1 | package gold 2 | 3 | import ( 4 | "net/http" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func TestWebIDTLSauth(t *testing.T) { 11 | request, err := http.NewRequest("HEAD", testServer.URL, nil) 12 | assert.NoError(t, err) 13 | response, err := user1h.Do(request) 14 | assert.NoError(t, err) 15 | response.Body.Close() 16 | assert.Equal(t, 200, response.StatusCode) 17 | 18 | assert.Equal(t, user1, response.Header.Get("User")) 19 | } 20 | 21 | func TestAddProfileKeys(t *testing.T) { 22 | webid := testServer.URL + "/_test/user1#id" 23 | var account = webidAccount{ 24 | WebID: webid, 25 | } 26 | g := NewWebIDProfile(account) 27 | g, k, p, err := AddProfileKeys(webid, g) 28 | assert.NoError(t, err) 29 | assert.NotNil(t, k) 30 | assert.NotNil(t, p) 31 | assert.Equal(t, 15, g.Len()) 32 | } 33 | -------------------------------------------------------------------------------- /websocket.go: -------------------------------------------------------------------------------- 1 | package gold 2 | 3 | import ( 4 | "io" 5 | "log" 6 | "net/http" 7 | "strings" 8 | "sync" 9 | 10 | "golang.org/x/net/websocket" 11 | ) 12 | 13 | // type wsConn struct { 14 | // subbed bool 15 | // uuid string 16 | // } 17 | 18 | var ( 19 | websocketSubs = map[string]map[*websocket.Conn]string{} 20 | websocketSubsL = new(sync.RWMutex) 21 | ) 22 | 23 | func onDeleteURI(uri string) { 24 | websocketPublish(uri) 25 | } 26 | 27 | func onUpdateURI(uri string) { 28 | websocketPublish(uri) 29 | } 30 | 31 | // Handles each websocket connection 32 | func websocketHandler(ws *websocket.Conn) { 33 | // @@TODO switch to server logging 34 | // log.Println("opened via:", ws.RemoteAddr()) 35 | 36 | uris := map[string]bool{} 37 | message := "" 38 | for { 39 | err := websocket.Message.Receive(ws, &message) 40 | if err == io.EOF { 41 | break 42 | } 43 | if err != nil { 44 | log.Println(err) 45 | break 46 | } 47 | 48 | argv := strings.Split(message, " ") 49 | if len(argv) < 2 { 50 | argv = append(argv, "") 51 | } 52 | 53 | cmd, uri := argv[0], argv[1] 54 | switch cmd { 55 | 56 | case "ping": 57 | websocket.Message.Send(ws, "pong") 58 | 59 | case "sub": 60 | uris[uri] = true 61 | websocketSubsL.Lock() 62 | if _, ex := websocketSubs[uri]; !ex { 63 | websocketSubs[uri] = map[*websocket.Conn]string{} 64 | } 65 | websocketSubs[uri][ws] = NewUUID() 66 | websocketSubsL.Unlock() 67 | websocket.Message.Send(ws, "ack "+uri) 68 | 69 | case "unsub": 70 | websocketSubsL.Lock() 71 | uris[uri] = false 72 | if len(websocketSubs[uri][ws]) > 0 { 73 | delete(websocketSubs[uri], ws) 74 | } 75 | websocketSubsL.Unlock() 76 | websocket.Message.Send(ws, "removed "+uri) 77 | 78 | default: 79 | log.Println("invalid message:", message) 80 | } 81 | } 82 | 83 | websocketSubsL.Lock() 84 | for k := range uris { 85 | delete(websocketSubs[k], ws) 86 | } 87 | websocketSubsL.Unlock() 88 | // @@TODO switch to server logging 89 | // log.Println("closed via:", ws.RemoteAddr()) 90 | } 91 | 92 | func websocketPublish(uri string, uuid ...string) { 93 | websocketSubsL.RLock() 94 | subs := websocketSubs[uri] 95 | websocketSubsL.RUnlock() 96 | 97 | for k := range subs { 98 | uuidMatch := true 99 | // log.Println(uuid) 100 | if len(uuid) > 0 && subs[k] != uuid[0] { 101 | uuidMatch = false 102 | } 103 | if uuidMatch { 104 | err := websocket.Message.Send(k, "pub "+uri) 105 | if err != nil { 106 | log.Println(err) 107 | } 108 | } 109 | } 110 | } 111 | 112 | // Converts an HTTP request to a websocket server 113 | func websocketServe(w http.ResponseWriter, req *http.Request) { 114 | websocket.Handler(websocketHandler).ServeHTTP(w, req) 115 | } 116 | 117 | // Checks whether an HTTP request looks like websocket 118 | func websocketUpgrade(r *http.Request) bool { 119 | if r == nil { 120 | return false 121 | } 122 | if strings.ToLower(r.Header.Get("Connection")) != "upgrade" { 123 | return false 124 | } 125 | if strings.ToLower(r.Header.Get("Upgrade")) != "websocket" { 126 | return false 127 | } 128 | return true 129 | } 130 | -------------------------------------------------------------------------------- /websocket_test.go: -------------------------------------------------------------------------------- 1 | package gold 2 | 3 | import ( 4 | "crypto/tls" 5 | "io/ioutil" 6 | "net" 7 | "net/http" 8 | "net/http/httptest" 9 | "net/url" 10 | "os" 11 | "strings" 12 | "testing" 13 | "time" 14 | 15 | "github.com/stretchr/testify/assert" 16 | "golang.org/x/net/websocket" 17 | ) 18 | 19 | var ( 20 | wsOrigin, wsURI string 21 | testServerWs *httptest.Server 22 | wsCfg1 *websocket.Config 23 | ) 24 | 25 | func init() { 26 | wsOrigin = "http://localhost/" 27 | 28 | configWs := NewServerConfig() 29 | configWs.DataRoot += "_test/" 30 | configWs.Vhosts = false 31 | handlerWs := NewServer(configWs) 32 | 33 | testServerWs = httptest.NewUnstartedServer(handlerWs) 34 | testServerWs.TLS = new(tls.Config) 35 | testServerWs.TLS.InsecureSkipVerify = true 36 | testServerWs.TLS.ClientAuth = tls.RequestClientCert 37 | testServerWs.TLS.NextProtos = []string{"http/1.1"} 38 | testServerWs.StartTLS() 39 | 40 | p, _ := url.Parse(testServerWs.URL) 41 | host, port, _ := net.SplitHostPort(p.Host) 42 | 43 | wsURI = "wss://" + host + ":" + port 44 | wsCfg1, _ = websocket.NewConfig(wsURI, wsOrigin) 45 | } 46 | 47 | func TestWebSocketPing(t *testing.T) { 48 | config := tls.Config{ 49 | Certificates: []tls.Certificate{*user1cert}, 50 | InsecureSkipVerify: true, 51 | } 52 | wsCfg1.TlsConfig = &config 53 | ws, err := websocket.DialConfig(wsCfg1) 54 | assert.NoError(t, err) 55 | _, err = ws.Write([]byte("ping")) 56 | assert.NoError(t, err) 57 | 58 | msg := make([]byte, 512) 59 | var n int 60 | n, err = ws.Read(msg) 61 | assert.NoError(t, err) 62 | assert.Equal(t, "pong", string(msg[:n])) 63 | } 64 | 65 | func TestWebSocketSubPub(t *testing.T) { 66 | resURL := testServerWs.URL + "/abc" 67 | 68 | config := tls.Config{ 69 | Certificates: []tls.Certificate{*user1cert}, 70 | InsecureSkipVerify: true, 71 | } 72 | wsCfg1.TlsConfig = &config 73 | ws, err := websocket.DialConfig(wsCfg1) 74 | assert.NoError(t, err) 75 | ws.SetReadDeadline(time.Now().Add(2 * time.Second)) 76 | 77 | _, err = ws.Write([]byte("sub " + testServerWs.URL + "/")) 78 | assert.NoError(t, err) 79 | 80 | msg := make([]byte, 512) 81 | var n int 82 | n, err = ws.Read(msg) 83 | assert.NoError(t, err) 84 | assert.Equal(t, "ack", string(msg[:3])) 85 | assert.Equal(t, testServerWs.URL+"/", string(msg[4:n])) 86 | 87 | _, err = ws.Write([]byte("sub " + resURL)) 88 | assert.NoError(t, err) 89 | 90 | msg = make([]byte, 512) 91 | n, err = ws.Read(msg) 92 | assert.NoError(t, err) 93 | assert.Equal(t, "ack", string(msg[:3])) 94 | assert.Equal(t, resURL, string(msg[4:n])) 95 | 96 | request, err := http.NewRequest("PUT", resURL, strings.NewReader(" .")) 97 | assert.NoError(t, err) 98 | request.Header.Add("Content-Type", "text/turtle") 99 | response, err := httpClient.Do(request) 100 | assert.NoError(t, err) 101 | assert.Equal(t, 201, response.StatusCode) 102 | 103 | msg = make([]byte, 512) 104 | n, err = ws.Read(msg) 105 | assert.NoError(t, err) 106 | assert.Equal(t, "pub "+resURL, string(msg[:n])) 107 | 108 | request, err = http.NewRequest("POST", testServerWs.URL+"/", nil) 109 | assert.NoError(t, err) 110 | request.Header.Add("Content-Type", "text/turtle") 111 | request.Header.Add("Link", "; rel=\"type\"") 112 | request.Header.Add("Slug", "dir") 113 | response, err = httpClient.Do(request) 114 | assert.NoError(t, err) 115 | assert.Equal(t, 201, response.StatusCode) 116 | 117 | msg = make([]byte, 512) 118 | n, err = ws.Read(msg) 119 | assert.NoError(t, err) 120 | assert.Equal(t, "pub "+testServerWs.URL+"/", string(msg[:n])) 121 | 122 | request, err = http.NewRequest("DELETE", testServerWs.URL+"/dir/", nil) 123 | assert.NoError(t, err) 124 | response, err = httpClient.Do(request) 125 | assert.NoError(t, err) 126 | body, err := ioutil.ReadAll(response.Body) 127 | assert.NoError(t, err) 128 | response.Body.Close() 129 | println(string(body)) 130 | assert.Equal(t, 200, response.StatusCode) 131 | 132 | msg = make([]byte, 512) 133 | n, err = ws.Read(msg) 134 | assert.NoError(t, err) 135 | assert.Equal(t, "pub "+testServerWs.URL+"/", string(msg[:n])) 136 | 137 | request, err = http.NewRequest("POST", testServerWs.URL+"/", nil) 138 | assert.NoError(t, err) 139 | request.Header.Add("Content-Type", "text/turtle") 140 | request.Header.Add("Slug", "res") 141 | response, err = httpClient.Do(request) 142 | assert.NoError(t, err) 143 | assert.Equal(t, 201, response.StatusCode) 144 | 145 | msg = make([]byte, 512) 146 | n, err = ws.Read(msg) 147 | assert.NoError(t, err) 148 | assert.Equal(t, "pub "+testServerWs.URL+"/", string(msg[:n])) 149 | 150 | _, err = ws.Write([]byte("unsub " + resURL)) 151 | assert.NoError(t, err) 152 | 153 | msg = make([]byte, 512) 154 | n, err = ws.Read(msg) 155 | assert.NoError(t, err) 156 | assert.Equal(t, "removed "+resURL, string(msg[:n])) 157 | 158 | err = os.RemoveAll("_test/") 159 | assert.NoError(t, err) 160 | } 161 | --------------------------------------------------------------------------------