Compare commits
38 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
3f7513364a | ||
|
499949ba52 | ||
6a22bfecc4 | |||
|
c36c0200f3 | ||
|
84e1863da2 | ||
|
881bf97334 | ||
|
f13dc593b0 | ||
|
557f9085f6 | ||
|
9de2f796db | ||
|
a8f1a99667 | ||
|
6981b852d0 | ||
|
673671147c | ||
|
e7c21aa35c | ||
|
5ff37be8c5 | ||
|
23822cdf09 | ||
|
9608a7429b | ||
|
e6dd414af6 | ||
|
05db67c8b7 | ||
|
ca84b8dbca | ||
|
e8c50dee76 | ||
|
a1b4ad1202 | ||
|
9b250c8cbb | ||
|
d80c8226b5 | ||
|
bc7e9740d8 | ||
|
da712abbb2 | ||
|
42f1089e6c | ||
|
075ade3dec | ||
|
153851b41d | ||
|
2fe128a017 | ||
|
87494faffe | ||
|
155c006740 | ||
|
aab56909cb | ||
|
563907d477 | ||
|
d914325e2f | ||
|
83a5642829 | ||
|
285dc81dd7 | ||
|
3ab579ad24 | ||
|
66e0639f48 |
9
.gitignore
vendored
9
.gitignore
vendored
@ -1,6 +1,15 @@
|
|||||||
/public-jwks
|
/public-jwks
|
||||||
/go-mockid
|
/go-mockid
|
||||||
|
|
||||||
|
# ---> Security
|
||||||
|
.env
|
||||||
|
|
||||||
|
# ---> Vim
|
||||||
|
.*.sw*
|
||||||
|
|
||||||
|
# ---> Node
|
||||||
|
node_modules
|
||||||
|
|
||||||
# ---> Go
|
# ---> Go
|
||||||
# Binaries for programs and plugins
|
# Binaries for programs and plugins
|
||||||
*.exe
|
*.exe
|
||||||
|
1
.prettierignore
Normal file
1
.prettierignore
Normal file
@ -0,0 +1 @@
|
|||||||
|
vendor/
|
78
README.md
78
README.md
@ -1,3 +1,81 @@
|
|||||||
# go-mockid
|
# go-mockid
|
||||||
|
|
||||||
OAuth2 / JWT / OpenID Connect for mocking auth... which isn't that different from doing it for real, actually.
|
OAuth2 / JWT / OpenID Connect for mocking auth... which isn't that different from doing it for real, actually.
|
||||||
|
|
||||||
|
## Enabling Google OAuth2 (Mid-2020)
|
||||||
|
|
||||||
|
1. Create an account at https://console.developers.google.com/apis/dashboard
|
||||||
|
2. Go back to https://console.developers.google.com/apis/dashboard
|
||||||
|
3. Create a New Project from the dropdown in the upper left that lists the current project name
|
||||||
|
4. Give the project a name such as `Example Web App` and accept its generated ID
|
||||||
|
5. Click "Create"
|
||||||
|
|
||||||
|
Add your test domain
|
||||||
|
|
||||||
|
1. Go back to https://console.developers.google.com/apis/dashboard
|
||||||
|
1. Select your new project from the upper-left drop-down
|
||||||
|
2. Select `Domain Verification` from the left hand side of the screen
|
||||||
|
3. Add your test domain (i.e. `beta.example.com`), but a domain that you actually own
|
||||||
|
4. Select `Verify Ownership`
|
||||||
|
5. Follow the specific instructions for adding a txt record to the subdomain you chose
|
||||||
|
6. Add a collaborator / co-owner if you wish
|
||||||
|
|
||||||
|
Enable OAuth2
|
||||||
|
|
||||||
|
1. Go back to https://console.developers.google.com/apis/dashboard
|
||||||
|
1. Select `OAuth consent screen`
|
||||||
|
2. Select `External`
|
||||||
|
3. Complete the consent screen form
|
||||||
|
|
||||||
|
Create Google Credentials
|
||||||
|
|
||||||
|
1. Go back to https://console.developers.google.com/apis/dashboard
|
||||||
|
1. Select `Credentials` from the left sidebar
|
||||||
|
2. Select `OAuth ID`
|
||||||
|
3. Select `Web Application`
|
||||||
|
4. Fill out the same test domain and test app name as before
|
||||||
|
5. Save the ID and Secret to a place you won't forget (perhaps a .gitignored .env)
|
||||||
|
|
||||||
|
Update your signin page.
|
||||||
|
|
||||||
|
1. You need to put your default scopes (i.e. `profile email`) and client ID in the meta tag of your login page HTML. `profile` is the minimum scope and is always returned.
|
||||||
|
```html
|
||||||
|
<head>
|
||||||
|
<meta name="google-signin-scope" content="email">
|
||||||
|
<meta
|
||||||
|
name="google-signin-client_id"
|
||||||
|
content="xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx.apps.googleusercontent.com"
|
||||||
|
/>
|
||||||
|
</head>
|
||||||
|
```
|
||||||
|
2. Although it should be possible to use an thin OAuth client, you'll probably want to start by including the (huge) Google platform.js
|
||||||
|
```html
|
||||||
|
<script src="https://apis.google.com/js/platform.js" async defer></script>
|
||||||
|
```
|
||||||
|
3. You can start off with the Google's sign in button, but you need your own `data-onsuccess` callback. You can also adjust the `data-scope` per button to include more stuff. Scopes are defined at https://developers.google.com/identity/protocols/oauth2/scopes
|
||||||
|
```html
|
||||||
|
<div
|
||||||
|
class="g-signin2"
|
||||||
|
data-onsuccess="ongsignin"
|
||||||
|
data-scope="profile email https://www.googleapis.com/auth/spreadsheets.readonly https://www.googleapis.com/auth/drive.readonly"
|
||||||
|
></div>
|
||||||
|
<script>
|
||||||
|
window.ongsignin = function (gauth) {
|
||||||
|
// Note: this is a special prototype-style instance object with few
|
||||||
|
// enumerable properties (which don't make sense). Requires API docs.
|
||||||
|
// See https://developers.google.com/identity/sign-in/web
|
||||||
|
console.log(goauth)
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
```
|
||||||
|
4. Despite the documentation stating that passing a token as a query is deprecated and to use the `Authorization` header, the inspect token URL only supports the query parameter: `GET https://oauth2.googleapis.com/tokeninfo?id_token=<token>`
|
||||||
|
- You can also validate the token with Google's public key
|
||||||
|
- https://accounts.google.com/.well-known/openid-configuration
|
||||||
|
- https://www.googleapis.com/oauth2/v3/certs (note that one of the Key IDs will match that of your kid)
|
||||||
|
5. While testing you'll probably want to revoke the app's permissions
|
||||||
|
- Go to https://myaccount.google.com/permissions
|
||||||
|
- Under "Third-party apps with account access" click "Manage third-party access" and search in the long list and click "Remove access".
|
||||||
|
- Under "Signing in to other sites" click "Signing in with Google" and search in the list to revoke access
|
||||||
|
- Active tokens will persist until they expire (1 hour), so you may need to clear cache, cookies, etc, which can be a pain
|
||||||
|
5. Sign out can be accomplished with a button that calls `gapi.auth2.getAuthInstance().signOut().then(function() { });`
|
||||||
|
|
||||||
|
65
cmd/mailer/mailer.go
Normal file
65
cmd/mailer/mailer.go
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"flag"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
mailgun "github.com/mailgun/mailgun-go/v3"
|
||||||
|
|
||||||
|
_ "github.com/joho/godotenv/autoload"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
/*
|
||||||
|
MAILGUN_API_KEY=key-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
|
||||||
|
MAILGUN_DOMAIN=mail.example.com
|
||||||
|
MAILER_FROM="Rob the Robot <rob.the.robot@mail.example.com>"
|
||||||
|
*/
|
||||||
|
|
||||||
|
to := flag.String("to", "", "message recipient in the format of 'John Doe <john@example.com>'")
|
||||||
|
replyTo := flag.String("reply-to", "", "reply-to in the format of 'John Doe <john@example.com>'")
|
||||||
|
subject := flag.String("subject", "Test Subject", "the utf8-encoded subject of the email")
|
||||||
|
text := flag.String(
|
||||||
|
"text",
|
||||||
|
"Testing some Mailgun awesomeness!",
|
||||||
|
"the body of the email as utf8-encoded plain-text format",
|
||||||
|
)
|
||||||
|
flag.Parse()
|
||||||
|
|
||||||
|
if 0 == len(*to) {
|
||||||
|
flag.Usage()
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
domain := os.Getenv("MAILGUN_DOMAIN")
|
||||||
|
apiKey := os.Getenv("MAILGUN_API_KEY")
|
||||||
|
from := os.Getenv("MAILER_FROM")
|
||||||
|
|
||||||
|
if 0 == len(*text) {
|
||||||
|
*text = "Testing some Mailgun awesomeness!"
|
||||||
|
}
|
||||||
|
|
||||||
|
msgId, err := SendSimpleMessage(domain, apiKey, *to, from, *subject, *text, *replyTo)
|
||||||
|
if nil != err {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
fmt.Printf("Queued with Message ID %q\n", msgId)
|
||||||
|
}
|
||||||
|
|
||||||
|
func SendSimpleMessage(domain, apiKey, to, from, subject, text, replyTo string) (string, error) {
|
||||||
|
mg := mailgun.NewMailgun(domain, apiKey)
|
||||||
|
m := mg.NewMessage(from, subject, text, to)
|
||||||
|
if 0 != len(replyTo) {
|
||||||
|
// mailgun's required "h:" prefix is added by the library
|
||||||
|
m.AddHeader("Reply-To", replyTo)
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), time.Second*30)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
_, id, err := mg.Send(ctx, m)
|
||||||
|
return id, err
|
||||||
|
}
|
7
default.jwk.json
Normal file
7
default.jwk.json
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"kty": "EC",
|
||||||
|
"crv": "P-256",
|
||||||
|
"d": "GYAwlBHc2mPsj1lp315HbYOmKNJ7esmO3JAkZVn9nJs",
|
||||||
|
"x": "ToL2HppsTESXQKvp7ED6NMgV4YnwbMeONexNry3KDNQ",
|
||||||
|
"y": "Tt6Q3rxU37KAinUV9PLMlwosNy1t3Bf2VDg5q955AGc"
|
||||||
|
}
|
7
examples/example.env
Normal file
7
examples/example.env
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
SALT=XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
|
||||||
|
|
||||||
|
MAILGUN_API_KEY=key-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
|
||||||
|
MAILGUN_DOMAIN=mail.example.com
|
||||||
|
|
||||||
|
MAILER_FROM="Rob the Robot <rob.the.robot@mail.example.com>"
|
||||||
|
MAILER_REPLY_TO=support@example.com
|
1
go-test.sh
Normal file
1
go-test.sh
Normal file
@ -0,0 +1 @@
|
|||||||
|
go test -mod=vendor -v ./...
|
11
go.mod
11
go.mod
@ -1,5 +1,12 @@
|
|||||||
module git.coolaj86.com/coolaj86/go-mockid
|
module git.coolaj86.com/coolaj86/go-mockid
|
||||||
|
|
||||||
go 1.12
|
go 1.13
|
||||||
|
|
||||||
require github.com/joho/godotenv v1.3.0
|
require (
|
||||||
|
git.rootprojects.org/root/hashcash v1.0.1
|
||||||
|
git.rootprojects.org/root/keypairs v0.6.5
|
||||||
|
github.com/google/uuid v1.1.1
|
||||||
|
github.com/joho/godotenv v1.3.0
|
||||||
|
github.com/mailgun/mailgun-go/v3 v3.6.4
|
||||||
|
github.com/mileusna/useragent v1.0.2
|
||||||
|
)
|
||||||
|
24
go.sum
24
go.sum
@ -1,2 +1,26 @@
|
|||||||
|
git.rootprojects.org/root/hashcash v1.0.1 h1:PkzwZu4CR5q/hwAntJdvcmNhmP0ONhetMo7rYhIZhZ0=
|
||||||
|
git.rootprojects.org/root/hashcash v1.0.1/go.mod h1:HdoULUe94o1NVMES5K6aP3p8QGQiIia73F1HNZ1+FkQ=
|
||||||
|
git.rootprojects.org/root/keypairs v0.6.5 h1:sdRAQD/O/JBS8+ZxUewXnY+cjQVDNH3TmcS+KtANZqA=
|
||||||
|
git.rootprojects.org/root/keypairs v0.6.5/go.mod h1:WGI8PadOp+4LjUuI+wNlSwcJwFtY8L9XuNjuO3213HA=
|
||||||
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/facebookgo/ensure v0.0.0-20160127193407-b4ab57deab51 h1:0JZ+dUmQeA8IIVUMzysrX4/AKuQwWhV2dYQuPZdvdSQ=
|
||||||
|
github.com/facebookgo/ensure v0.0.0-20160127193407-b4ab57deab51/go.mod h1:Yg+htXGokKKdzcwhuNDwVvN+uBxDGXJ7G/VN1d8fa64=
|
||||||
|
github.com/facebookgo/stack v0.0.0-20160209184415-751773369052 h1:JWuenKqqX8nojtoVVWjGfOF9635RETekkoH6Cc9SX0A=
|
||||||
|
github.com/facebookgo/stack v0.0.0-20160209184415-751773369052/go.mod h1:UbMTZqLaRiH3MsBH8va0n7s1pQYcu3uTb8G4tygF4Zg=
|
||||||
|
github.com/facebookgo/subset v0.0.0-20150612182917-8dac2c3c4870 h1:E2s37DuLxFhQDg5gKsWoLBOB0n+ZW8s599zru8FJ2/Y=
|
||||||
|
github.com/facebookgo/subset v0.0.0-20150612182917-8dac2c3c4870/go.mod h1:5tD+neXqOorC30/tWg0LCSkrqj/AR6gu8yY8/fpw1q0=
|
||||||
|
github.com/go-chi/chi v4.0.0+incompatible h1:SiLLEDyAkqNnw+T/uDTf3aFB9T4FTrwMpuYrgaRcnW4=
|
||||||
|
github.com/go-chi/chi v4.0.0+incompatible/go.mod h1:eB3wogJHnLi3x/kFX2A+IbTBlXxmMeXJVKy9tTv1XzQ=
|
||||||
|
github.com/google/uuid v1.1.1 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY=
|
||||||
|
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
github.com/joho/godotenv v1.3.0 h1:Zjp+RcGpHhGlrMbJzXTrZZPrWj+1vfm90La1wgB6Bhc=
|
github.com/joho/godotenv v1.3.0 h1:Zjp+RcGpHhGlrMbJzXTrZZPrWj+1vfm90La1wgB6Bhc=
|
||||||
github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg=
|
github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg=
|
||||||
|
github.com/mailgun/mailgun-go/v3 v3.6.4 h1:+cvbZRgLSHivbz/w1iWLmxVl6Bqf4geD2D7QMj4+8PE=
|
||||||
|
github.com/mailgun/mailgun-go/v3 v3.6.4/go.mod h1:ZjVnH8S0dR2BLjvkZc/rxwerdcirzlA12LQDuGAadR0=
|
||||||
|
github.com/mailru/easyjson v0.0.0-20180823135443-60711f1a8329 h1:2gxZ0XQIU/5z3Z3bUBu+FXuk2pFbkN6tcwi/pjyaDic=
|
||||||
|
github.com/mailru/easyjson v0.0.0-20180823135443-60711f1a8329/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
|
||||||
|
github.com/mileusna/useragent v1.0.2 h1:DgVKtiPnjxlb73z9bCwgdUvU2nQNQ97uhgfO8l9uz/w=
|
||||||
|
github.com/mileusna/useragent v1.0.2/go.mod h1:3d8TOmwL/5I8pJjyVDteHtgDGcefrFUX4ccGOMKNYYc=
|
||||||
|
github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
|
||||||
|
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
|
131
kvdb/kvdb.go
Normal file
131
kvdb/kvdb.go
Normal file
@ -0,0 +1,131 @@
|
|||||||
|
package kvdb
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
type KVDB struct {
|
||||||
|
Prefix string
|
||||||
|
Ext string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (kv *KVDB) Load(
|
||||||
|
keyif interface{},
|
||||||
|
typ ...interface{},
|
||||||
|
) (value interface{}, ok bool, err error) {
|
||||||
|
key, _ := keyif.(string)
|
||||||
|
if "" == key || strings.Contains(key, "..") || strings.ContainsAny(key, "$#!:| \n") {
|
||||||
|
return nil, false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
userFile := filepath.Join(kv.Prefix, key+"."+kv.Ext)
|
||||||
|
fmt.Println("Debug user file:", userFile)
|
||||||
|
b, err := ioutil.ReadFile(userFile)
|
||||||
|
if nil != err {
|
||||||
|
if os.IsNotExist(err) {
|
||||||
|
return nil, false, nil
|
||||||
|
}
|
||||||
|
fmt.Println("kvdb debug read:", err)
|
||||||
|
return nil, false, errors.New("database read failed")
|
||||||
|
}
|
||||||
|
|
||||||
|
ok = true
|
||||||
|
value = b
|
||||||
|
if 1 == len(typ) {
|
||||||
|
err := json.Unmarshal(b, typ[0])
|
||||||
|
if nil != err {
|
||||||
|
return nil, false, err
|
||||||
|
}
|
||||||
|
value = typ[0]
|
||||||
|
} else if len(b) > 0 && '"' == b[0] {
|
||||||
|
var str string
|
||||||
|
err := json.Unmarshal(b, &str)
|
||||||
|
if nil == err {
|
||||||
|
value = str
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return value, ok, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (kv *KVDB) Store(keyif interface{}, value interface{}) (err error) {
|
||||||
|
key, _ := keyif.(string)
|
||||||
|
if "" == key || strings.Contains(key, "..") || strings.ContainsAny(key, "$#! \n") {
|
||||||
|
return errors.New("invalid key name")
|
||||||
|
}
|
||||||
|
|
||||||
|
keypath := filepath.Join(kv.Prefix, key+"."+kv.Ext)
|
||||||
|
f, err := os.Open(keypath)
|
||||||
|
if nil == err {
|
||||||
|
s, err := f.Stat()
|
||||||
|
if nil != err {
|
||||||
|
// if we can open, we should be able to stat
|
||||||
|
return errors.New("database connection failure")
|
||||||
|
}
|
||||||
|
ts := strconv.FormatInt(s.ModTime().Unix(), 10)
|
||||||
|
bakpath := filepath.Join(kv.Prefix, key+"."+ts+"."+kv.Ext)
|
||||||
|
if err := os.Rename(keypath, bakpath); nil != err {
|
||||||
|
// keep the old record as a backup
|
||||||
|
return errors.New("database write failure")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var b []byte
|
||||||
|
switch v := value.(type) {
|
||||||
|
case []byte:
|
||||||
|
b = v
|
||||||
|
case string:
|
||||||
|
b, _ = json.Marshal(v)
|
||||||
|
default:
|
||||||
|
fmt.Println("kvdb: not []byte or string:", v)
|
||||||
|
jsonb, err := json.Marshal(v)
|
||||||
|
if nil != err {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
b = jsonb
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := ioutil.WriteFile(
|
||||||
|
keypath,
|
||||||
|
b,
|
||||||
|
os.FileMode(0600),
|
||||||
|
); nil != err {
|
||||||
|
fmt.Println("write failure:", err)
|
||||||
|
return errors.New("database write failed")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (kv *KVDB) Delete(keyif interface{}) (err error) {
|
||||||
|
key, _ := keyif.(string)
|
||||||
|
if "" == key || strings.Contains(key, "..") || strings.ContainsAny(key, "$#! \n") {
|
||||||
|
return errors.New("invalid key name")
|
||||||
|
}
|
||||||
|
|
||||||
|
keypath := filepath.Join(kv.Prefix, key+"."+kv.Ext)
|
||||||
|
f, err := os.Open(keypath)
|
||||||
|
if nil == err {
|
||||||
|
s, err := f.Stat()
|
||||||
|
if nil != err {
|
||||||
|
return errors.New("database connection failure")
|
||||||
|
}
|
||||||
|
ts := strconv.FormatInt(s.ModTime().Unix(), 64)
|
||||||
|
if err := os.Rename(keypath, filepath.Join(kv.Prefix, key+"."+ts+"."+kv.Ext)); nil != err {
|
||||||
|
return errors.New("database connection failure")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (kv *KVDB) Vacuum() (err error) {
|
||||||
|
return nil
|
||||||
|
}
|
63
kvdb/kvdb_test.go
Normal file
63
kvdb/kvdb_test.go
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
package kvdb
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
type TestEntry struct {
|
||||||
|
Email string `json:"email"`
|
||||||
|
Subjects []string `json:"subjects"`
|
||||||
|
}
|
||||||
|
|
||||||
|
var email = "john@example.com"
|
||||||
|
var sub = "id123"
|
||||||
|
var dbPrefix = "../testdb"
|
||||||
|
var testKV = &KVDB{
|
||||||
|
Prefix: dbPrefix + "/test-entries",
|
||||||
|
Ext: "eml.json",
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestStore(t *testing.T) {
|
||||||
|
entry := &TestEntry{
|
||||||
|
Email: email,
|
||||||
|
Subjects: []string{sub},
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := testKV.Store(email, entry); nil != err {
|
||||||
|
t.Fatal(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
value, ok, err := testKV.Load(email, &(TestEntry{}))
|
||||||
|
if nil != err {
|
||||||
|
t.Fatal(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if !ok {
|
||||||
|
t.Fatal("test entry not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
v, ok := value.(*TestEntry)
|
||||||
|
if !ok {
|
||||||
|
t.Fatal("test entry not of type TestEntry")
|
||||||
|
}
|
||||||
|
|
||||||
|
if email != v.Email || sub != strings.Join(v.Subjects, ",") {
|
||||||
|
t.Fatalf("value: %#v", v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNoExist(t *testing.T) {
|
||||||
|
value, ok, err := testKV.Load("not"+email, &(TestEntry{}))
|
||||||
|
if nil != err {
|
||||||
|
t.Fatal(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if ok {
|
||||||
|
t.Fatal("found entry that doesn't exist")
|
||||||
|
}
|
||||||
|
if value != nil {
|
||||||
|
t.Fatal("had value for entry that doesn't exist")
|
||||||
|
}
|
||||||
|
}
|
54
mockid.go
54
mockid.go
@ -1,16 +1,19 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
|
||||||
"flag"
|
"flag"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
"log"
|
"log"
|
||||||
|
"math/rand"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"time"
|
||||||
|
|
||||||
"git.coolaj86.com/coolaj86/go-mockid/mockid"
|
"git.coolaj86.com/coolaj86/go-mockid/mockid"
|
||||||
|
"git.rootprojects.org/root/keypairs"
|
||||||
|
|
||||||
_ "github.com/joho/godotenv/autoload"
|
_ "github.com/joho/godotenv/autoload"
|
||||||
)
|
)
|
||||||
@ -20,21 +23,7 @@ func main() {
|
|||||||
var port int
|
var port int
|
||||||
var host string
|
var host string
|
||||||
|
|
||||||
jwkm := map[string]string{
|
rand.Seed(time.Now().UnixNano())
|
||||||
"crv": "P-256",
|
|
||||||
"d": "GYAwlBHc2mPsj1lp315HbYOmKNJ7esmO3JAkZVn9nJs",
|
|
||||||
"x": "ToL2HppsTESXQKvp7ED6NMgV4YnwbMeONexNry3KDNQ",
|
|
||||||
"y": "Tt6Q3rxU37KAinUV9PLMlwosNy1t3Bf2VDg5q955AGc",
|
|
||||||
}
|
|
||||||
jwk := &mockid.PrivateJWK{
|
|
||||||
PublicJWK: mockid.PublicJWK{
|
|
||||||
Crv: jwkm["crv"],
|
|
||||||
X: jwkm["x"],
|
|
||||||
Y: jwkm["y"],
|
|
||||||
},
|
|
||||||
D: jwkm["d"],
|
|
||||||
}
|
|
||||||
priv := mockid.ParseKey(jwk)
|
|
||||||
|
|
||||||
portFlag := flag.Int("port", 0, "Port on which the HTTP server should run")
|
portFlag := flag.Int("port", 0, "Port on which the HTTP server should run")
|
||||||
urlFlag := flag.String("url", "", "Outward-facing address, such as https://example.com")
|
urlFlag := flag.String("url", "", "Outward-facing address, such as https://example.com")
|
||||||
@ -52,6 +41,20 @@ func main() {
|
|||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
jwkpath := "./default.jwk.json"
|
||||||
|
jwkb, err := ioutil.ReadFile(jwkpath)
|
||||||
|
if nil != err {
|
||||||
|
panic(fmt.Errorf("read default jwk %v: %w", jwkpath, err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
privkey, err := keypairs.ParseJWKPrivateKey(jwkb)
|
||||||
|
if nil != err {
|
||||||
|
// TODO delete the bad file?
|
||||||
|
panic(fmt.Errorf("unmarshal jwk %v: %w", string(jwkb), err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if nil != urlFlag && "" != *urlFlag {
|
if nil != urlFlag && "" != *urlFlag {
|
||||||
host = *urlFlag
|
host = *urlFlag
|
||||||
} else {
|
} else {
|
||||||
@ -64,15 +67,15 @@ func main() {
|
|||||||
} else {
|
} else {
|
||||||
jwksPrefix = "public-jwks"
|
jwksPrefix = "public-jwks"
|
||||||
}
|
}
|
||||||
err := os.MkdirAll(jwksPrefix, 0755)
|
err = os.MkdirAll(jwksPrefix, 0755)
|
||||||
if nil != err {
|
if nil != err {
|
||||||
fmt.Fprintf(os.Stderr, "couldn't write %q: %s", jwksPrefix, err)
|
fmt.Fprintf(os.Stderr, "couldn't write %q: %s", jwksPrefix, err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
mockid.Route(jwksPrefix, priv, jwk)
|
mux := mockid.Route(jwksPrefix, privkey)
|
||||||
|
|
||||||
fs := http.FileServer(http.Dir("public"))
|
fs := http.FileServer(http.Dir("./public"))
|
||||||
http.Handle("/", fs)
|
http.Handle("/", fs)
|
||||||
/*
|
/*
|
||||||
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
||||||
@ -83,15 +86,16 @@ func main() {
|
|||||||
|
|
||||||
fmt.Printf("Serving on port %d\n", port)
|
fmt.Printf("Serving on port %d\n", port)
|
||||||
go func() {
|
go func() {
|
||||||
log.Fatal(http.ListenAndServe(":"+strconv.Itoa(port), nil))
|
log.Fatal(http.ListenAndServe(":"+strconv.Itoa(port), mux))
|
||||||
done <- true
|
done <- true
|
||||||
}()
|
}()
|
||||||
|
|
||||||
b, _ := json.Marshal(jwk)
|
// TODO privB := keypairs.MarshalJWKPrivateKey(privkey)
|
||||||
fmt.Printf("Private Key:\n\t%s\n", string(b))
|
privB := keypairs.MarshalJWKPrivateKey(privkey)
|
||||||
b, _ = json.Marshal(jwk.PublicJWK)
|
fmt.Printf("Private Key:\n\t%s\n", string(privB))
|
||||||
fmt.Printf("Public Key:\n\t%s\n", string(b))
|
pubB := keypairs.MarshalJWKPublicKey(keypairs.NewPublicKey(privkey.Public()))
|
||||||
protected, payload, token := mockid.GenToken(host, priv, url.Values{})
|
fmt.Printf("Public Key:\n\t%s\n", string(pubB))
|
||||||
|
protected, payload, token := mockid.GenToken(host, privkey, url.Values{})
|
||||||
fmt.Printf("Protected (Header):\n\t%s\n", protected)
|
fmt.Printf("Protected (Header):\n\t%s\n", protected)
|
||||||
fmt.Printf("Payload (Claims):\n\t%s\n", payload)
|
fmt.Printf("Payload (Claims):\n\t%s\n", payload)
|
||||||
fmt.Printf("Access Token:\n\t%s\n", token)
|
fmt.Printf("Access Token:\n\t%s\n", token)
|
||||||
|
71
mockid/api/common.go
Normal file
71
mockid/api/common.go
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"crypto/sha256"
|
||||||
|
"encoding/binary"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"io"
|
||||||
|
"log"
|
||||||
|
"math/rand"
|
||||||
|
mathrand "math/rand"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"git.coolaj86.com/coolaj86/go-mockid/xkeypairs"
|
||||||
|
"git.rootprojects.org/root/keypairs"
|
||||||
|
)
|
||||||
|
|
||||||
|
/*
|
||||||
|
func getJWS(r *http.Request) (*xkeypairs.KeyOptions, error) {
|
||||||
|
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
|
func getOpts(r *http.Request) (*xkeypairs.KeyOptions, error) {
|
||||||
|
tok := make(map[string]interface{})
|
||||||
|
decoder := json.NewDecoder(r.Body)
|
||||||
|
err := decoder.Decode(&tok)
|
||||||
|
if nil != err && io.EOF != err {
|
||||||
|
log.Printf("json decode error: %s", err)
|
||||||
|
return nil, errors.New("Bad Request: invalid json body")
|
||||||
|
}
|
||||||
|
defer r.Body.Close()
|
||||||
|
|
||||||
|
var seed int64
|
||||||
|
seedStr, _ := tok["seed"].(string)
|
||||||
|
if "" != seedStr {
|
||||||
|
if len(seedStr) > 256 {
|
||||||
|
return nil, errors.New("Bad Request: base64 seed should be <256 characters (and is truncated to 64-bits anyway)")
|
||||||
|
}
|
||||||
|
b := sha256.Sum256([]byte(seedStr))
|
||||||
|
seed, _ = binary.ReadVarint(bytes.NewReader(b[0:8]))
|
||||||
|
}
|
||||||
|
|
||||||
|
key, _ := tok["key"].(string)
|
||||||
|
opts := &xkeypairs.KeyOptions{
|
||||||
|
Seed: seed,
|
||||||
|
Key: key,
|
||||||
|
}
|
||||||
|
|
||||||
|
opts.Claims, _ = tok["claims"].(keypairs.Object)
|
||||||
|
opts.Header, _ = tok["header"].(keypairs.Object)
|
||||||
|
|
||||||
|
var n int
|
||||||
|
if 0 != seed {
|
||||||
|
n = opts.MyFooNextReader().(*mathrand.Rand).Intn(2)
|
||||||
|
} else {
|
||||||
|
n = rand.Intn(2)
|
||||||
|
}
|
||||||
|
|
||||||
|
opts.KeyType, _ = tok["kty"].(string)
|
||||||
|
if "" == opts.KeyType {
|
||||||
|
if 0 == n {
|
||||||
|
opts.KeyType = "RSA"
|
||||||
|
} else {
|
||||||
|
opts.KeyType = "EC"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return opts, nil
|
||||||
|
}
|
145
mockid/api/generate.go
Normal file
145
mockid/api/generate.go
Normal file
@ -0,0 +1,145 @@
|
|||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"git.coolaj86.com/coolaj86/go-mockid/xkeypairs"
|
||||||
|
"git.rootprojects.org/root/keypairs"
|
||||||
|
)
|
||||||
|
|
||||||
|
// GeneratePublicJWK will create a new private key in JWK format
|
||||||
|
func GeneratePublicJWK(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if "POST" != r.Method {
|
||||||
|
http.Error(w, "Method Not Allowed", http.StatusMethodNotAllowed)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
opts, err := getOpts(r)
|
||||||
|
if nil != err {
|
||||||
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
privkey, err := getPrivKey(opts)
|
||||||
|
if nil != err {
|
||||||
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
jwk := keypairs.MarshalJWKPublicKey(keypairs.NewPublicKey(privkey.Public()))
|
||||||
|
w.Write(append(jwk, '\n'))
|
||||||
|
}
|
||||||
|
|
||||||
|
// GeneratePrivateJWK will create a new private key in JWK format
|
||||||
|
func GeneratePrivateJWK(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if "POST" != r.Method {
|
||||||
|
http.Error(w, "Method Not Allowed", http.StatusMethodNotAllowed)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
opts, err := getOpts(r)
|
||||||
|
if nil != err {
|
||||||
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
privkey := xkeypairs.GenPrivKey(opts)
|
||||||
|
|
||||||
|
jwk := keypairs.MarshalJWKPrivateKey(privkey)
|
||||||
|
w.Write(append(jwk, '\n'))
|
||||||
|
}
|
||||||
|
|
||||||
|
// GeneratePublicDER will create a new private key in JWK format
|
||||||
|
func GeneratePublicDER(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if "POST" != r.Method {
|
||||||
|
http.Error(w, "Method Not Allowed", http.StatusMethodNotAllowed)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
opts, err := getOpts(r)
|
||||||
|
if nil != err {
|
||||||
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
privkey, err := getPrivKey(opts)
|
||||||
|
if nil != err {
|
||||||
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
b, _ := keypairs.MarshalDERPublicKey(privkey.Public())
|
||||||
|
|
||||||
|
w.Write(b)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GeneratePrivateDER will create a new private key in a valid DER encoding
|
||||||
|
func GeneratePrivateDER(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if "POST" != r.Method {
|
||||||
|
http.Error(w, "Method Not Allowed", http.StatusMethodNotAllowed)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
opts, err := getOpts(r)
|
||||||
|
if nil != err {
|
||||||
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
privkey := xkeypairs.GenPrivKey(opts)
|
||||||
|
|
||||||
|
der, _ := keypairs.MarshalDERPrivateKey(privkey)
|
||||||
|
w.Write(der)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GeneratePublicPEM will create a new private key in JWK format
|
||||||
|
func GeneratePublicPEM(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if "POST" != r.Method {
|
||||||
|
http.Error(w, "Method Not Allowed", http.StatusMethodNotAllowed)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
opts, err := getOpts(r)
|
||||||
|
if nil != err {
|
||||||
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
privkey, err := getPrivKey(opts)
|
||||||
|
if nil != err {
|
||||||
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
b, _ := keypairs.MarshalPEMPublicKey(privkey.Public())
|
||||||
|
|
||||||
|
w.Write(b)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GeneratePrivatePEM will create a new private key in a valid PEM encoding
|
||||||
|
func GeneratePrivatePEM(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if "POST" != r.Method {
|
||||||
|
http.Error(w, "Method Not Allowed", http.StatusMethodNotAllowed)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
opts, err := getOpts(r)
|
||||||
|
if nil != err {
|
||||||
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
privkey := xkeypairs.GenPrivKey(opts)
|
||||||
|
|
||||||
|
privpem, _ := keypairs.MarshalPEMPrivateKey(privkey)
|
||||||
|
w.Write(privpem)
|
||||||
|
}
|
||||||
|
|
||||||
|
const maxRetry = 16
|
||||||
|
|
||||||
|
func getPrivKey(opts *xkeypairs.KeyOptions) (keypairs.PrivateKey, error) {
|
||||||
|
if "" != opts.Key {
|
||||||
|
return keypairs.ParsePrivateKey([]byte(opts.Key))
|
||||||
|
}
|
||||||
|
return xkeypairs.GenPrivKey(opts), nil
|
||||||
|
}
|
57
mockid/api/sign.go
Normal file
57
mockid/api/sign.go
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"git.rootprojects.org/root/keypairs"
|
||||||
|
)
|
||||||
|
|
||||||
|
// SignJWS will create an uncompressed JWT with the given payload
|
||||||
|
func SignJWS(w http.ResponseWriter, r *http.Request) {
|
||||||
|
sign(w, r, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SignJWT will create an compressed JWS (JWT) with the given payload
|
||||||
|
func SignJWT(w http.ResponseWriter, r *http.Request) {
|
||||||
|
sign(w, r, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
func sign(w http.ResponseWriter, r *http.Request, jwt bool) {
|
||||||
|
if "POST" != r.Method {
|
||||||
|
http.Error(w, "Method Not Allowed", http.StatusMethodNotAllowed)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
opts, err := getOpts(r)
|
||||||
|
if nil != err {
|
||||||
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
privkey, err := getPrivKey(opts)
|
||||||
|
if nil != err {
|
||||||
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
header := opts.Header
|
||||||
|
if 0 != opts.Seed {
|
||||||
|
header["_seed"] = opts.Seed
|
||||||
|
}
|
||||||
|
|
||||||
|
jws, err := keypairs.SignClaims(privkey, header, opts.Claims)
|
||||||
|
if nil != err {
|
||||||
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var b []byte
|
||||||
|
if jwt {
|
||||||
|
s := keypairs.JWSToJWT(jws)
|
||||||
|
w.Write(append([]byte(s), '\n'))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
b, _ = json.Marshal(jws)
|
||||||
|
w.Write(append(b, '\n'))
|
||||||
|
}
|
87
mockid/api/verify.go
Normal file
87
mockid/api/verify.go
Normal file
@ -0,0 +1,87 @@
|
|||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/base64"
|
||||||
|
"encoding/json"
|
||||||
|
"io"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"git.rootprojects.org/root/keypairs"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Verify will verify both JWT and uncompressed JWS
|
||||||
|
func Verify(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if "POST" != r.Method {
|
||||||
|
http.Error(w, "Method Not Allowed", http.StatusMethodNotAllowed)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
jws := &keypairs.JWS{}
|
||||||
|
|
||||||
|
authzParts := strings.Split(r.Header.Get("Authorization"), " ")
|
||||||
|
lenAuthz := len(authzParts)
|
||||||
|
if 2 == lenAuthz {
|
||||||
|
jwt := authzParts[1]
|
||||||
|
jwsParts := strings.Split(jwt, ".")
|
||||||
|
if 3 == len(jwsParts) {
|
||||||
|
jws.Protected = jwsParts[0]
|
||||||
|
jws.Payload = jwsParts[1]
|
||||||
|
jws.Signature = jwsParts[2]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if nil == jws {
|
||||||
|
if 0 != lenAuthz {
|
||||||
|
http.Error(w, "Bad Request: malformed Authorization header", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
decoder := json.NewDecoder(r.Body)
|
||||||
|
err := decoder.Decode(jws)
|
||||||
|
if nil != err && io.EOF != err {
|
||||||
|
log.Printf("json decode error: %s", err)
|
||||||
|
http.Error(w, "Bad Request: invalid JWS body", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer r.Body.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
protected, err := base64.RawURLEncoding.DecodeString(jws.Protected)
|
||||||
|
if nil != err {
|
||||||
|
http.Error(w, "Bad Request: invalid JWS header base64Url encoding", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := json.Unmarshal([]byte(protected), &jws.Header); nil != err {
|
||||||
|
log.Printf("json decode header error: %s", err)
|
||||||
|
http.Error(w, "Bad Request: invalid JWS header", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
payload, err := base64.RawURLEncoding.DecodeString(jws.Payload)
|
||||||
|
if nil != err {
|
||||||
|
http.Error(w, "Bad Request: invalid JWS payload base64Url encoding", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := json.Unmarshal([]byte(payload), &jws.Claims); nil != err {
|
||||||
|
log.Printf("json decode claims error: %s", err)
|
||||||
|
http.Error(w, "Bad Request: invalid JWS claims", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if "false" == r.URL.Query().Get("exp") {
|
||||||
|
//expf64, _ := jws.Claims["exp"].(float64)
|
||||||
|
jws.Claims["exp"] = float64(time.Now().Add(5 * time.Minute).Unix())
|
||||||
|
}
|
||||||
|
|
||||||
|
errs := keypairs.VerifyClaims(nil, jws)
|
||||||
|
if 0 == len(errs) {
|
||||||
|
log.Printf("jws verify error: %s", errs)
|
||||||
|
http.Error(w, "Bad Request: could not verify JWS claims", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
b := []byte(`{"success":true}`)
|
||||||
|
w.Write(append(b, '\n'))
|
||||||
|
}
|
101
mockid/hashcash.go
Normal file
101
mockid/hashcash.go
Normal file
@ -0,0 +1,101 @@
|
|||||||
|
package mockid
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"net/http"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"git.rootprojects.org/root/hashcash"
|
||||||
|
)
|
||||||
|
|
||||||
|
var hashcashes = &hashcashDB{
|
||||||
|
db: sync.Map{},
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewHashcash(sub string, exp time.Time) *hashcash.Hashcash {
|
||||||
|
|
||||||
|
h := hashcash.New(hashcash.Hashcash{
|
||||||
|
Subject: sub,
|
||||||
|
ExpiresAt: exp,
|
||||||
|
})
|
||||||
|
|
||||||
|
// ignoring the error because this implementation is backed by an in-memory map
|
||||||
|
_ = hashcashes.Store(h.Nonce, h)
|
||||||
|
|
||||||
|
return h
|
||||||
|
}
|
||||||
|
|
||||||
|
var ErrNotFound = errors.New("not found")
|
||||||
|
|
||||||
|
func UseHashcash(hc, sub string) error {
|
||||||
|
phony, err := hashcash.Parse(hc)
|
||||||
|
if nil != err {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
hi, ok, _ := hashcashes.Load(phony.Nonce)
|
||||||
|
if !ok {
|
||||||
|
return ErrNotFound
|
||||||
|
}
|
||||||
|
mccoy := hi.(*hashcash.Hashcash)
|
||||||
|
mccopy := *mccoy
|
||||||
|
|
||||||
|
mccopy.Solution = phony.Solution
|
||||||
|
if err := mccopy.Verify(sub); nil != err {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
_ = hashcashes.Delete(mccoy.Nonce)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func issueHashcash(w http.ResponseWriter, r *http.Request) *hashcash.Hashcash {
|
||||||
|
h := NewHashcash(r.Host, time.Now().Add(5*time.Minute))
|
||||||
|
w.Header().Set("Hashcash-Challenge", h.String())
|
||||||
|
return h
|
||||||
|
}
|
||||||
|
|
||||||
|
func requireHashcash(next http.HandlerFunc) http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
hc := r.Header.Get("Hashcash")
|
||||||
|
_ = issueHashcash(w, r)
|
||||||
|
if err := UseHashcash(hc, r.Host); nil != err {
|
||||||
|
http.Error(w, "Bad Request", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
next(w, r)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type hashcashDB struct {
|
||||||
|
db sync.Map
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *hashcashDB) Load(key interface{}) (value interface{}, ok bool, err error) {
|
||||||
|
v, ok := h.db.Load(key)
|
||||||
|
return v, ok, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *hashcashDB) Store(key interface{}, value interface{}) (err error) {
|
||||||
|
h.db.Store(key, value)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *hashcashDB) Delete(key interface{}) (err error) {
|
||||||
|
h.db.Delete(key)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *hashcashDB) vacuum() (err error) {
|
||||||
|
now := time.Now().UTC()
|
||||||
|
h.db.Range(func(key interface{}, val interface{}) bool {
|
||||||
|
v := val.(*hashcash.Hashcash)
|
||||||
|
if v.ExpiresAt.Sub(now) < 0 {
|
||||||
|
h.db.Delete(key)
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
return nil
|
||||||
|
}
|
46
mockid/mailgun.go
Normal file
46
mockid/mailgun.go
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
package mockid
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"os"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
mailgun "github.com/mailgun/mailgun-go/v3"
|
||||||
|
|
||||||
|
_ "github.com/joho/godotenv/autoload"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
mgDomain string
|
||||||
|
mgAPIKey string
|
||||||
|
mgFrom string
|
||||||
|
mg *mailgun.MailgunImpl
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
/*
|
||||||
|
MAILGUN_API_KEY=key-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
|
||||||
|
MAILGUN_DOMAIN=mail.example.com
|
||||||
|
MAILER_FROM="Rob the Robot <rob.the.robot@mail.example.com>"
|
||||||
|
*/
|
||||||
|
|
||||||
|
mgDomain = os.Getenv("MAILGUN_DOMAIN")
|
||||||
|
mgAPIKey = os.Getenv("MAILGUN_API_KEY")
|
||||||
|
mgFrom = os.Getenv("MAILER_FROM")
|
||||||
|
|
||||||
|
mg = mailgun.NewMailgun(mgDomain, mgAPIKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
func SendSimpleMessage(to, from, subject, text, replyTo string) (string, error) {
|
||||||
|
m := mg.NewMessage(from, subject, text, to)
|
||||||
|
if 0 != len(replyTo) {
|
||||||
|
// mailgun's required "h:" prefix is added by the library
|
||||||
|
m.AddHeader("Reply-To", replyTo)
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), time.Second*30)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
_, id, err := mg.Send(ctx, m)
|
||||||
|
return id, err
|
||||||
|
}
|
508
mockid/mockid.go
508
mockid/mockid.go
@ -1,29 +1,27 @@
|
|||||||
package mockid
|
package mockid
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"crypto"
|
||||||
"crypto/ecdsa"
|
"crypto/ecdsa"
|
||||||
"crypto/elliptic"
|
|
||||||
"crypto/rand"
|
"crypto/rand"
|
||||||
|
"crypto/rsa"
|
||||||
"crypto/sha256"
|
"crypto/sha256"
|
||||||
"crypto/sha512"
|
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io/ioutil"
|
"io"
|
||||||
"log"
|
|
||||||
"math/big"
|
"math/big"
|
||||||
"net/http"
|
|
||||||
"net/url"
|
"net/url"
|
||||||
"path/filepath"
|
"os"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"git.rootprojects.org/root/keypairs"
|
||||||
|
//jwt "github.com/dgrijalva/jwt-go"
|
||||||
)
|
)
|
||||||
|
|
||||||
type PrivateJWK struct {
|
// TestMain will overwrite this
|
||||||
PublicJWK
|
var rndsrc io.Reader = rand.Reader
|
||||||
D string `json:"d"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type PublicJWK struct {
|
type PublicJWK struct {
|
||||||
Crv string `json:"crv"`
|
Crv string `json:"crv"`
|
||||||
@ -33,332 +31,79 @@ type PublicJWK struct {
|
|||||||
Y string `json:"y"`
|
Y string `json:"y"`
|
||||||
}
|
}
|
||||||
|
|
||||||
var nonces map[string]int64
|
type KVDB interface {
|
||||||
|
Load(key interface{}) (value interface{}, ok bool, err error)
|
||||||
func init() {
|
Store(key interface{}, value interface{}) (err error)
|
||||||
nonces = make(map[string]int64)
|
Delete(key interface{}) (err error)
|
||||||
|
Vacuum() (err error)
|
||||||
}
|
}
|
||||||
|
|
||||||
func Route(jwksPrefix string, priv *ecdsa.PrivateKey, jwk *PrivateJWK) {
|
type InspectableToken struct {
|
||||||
pub := &priv.PublicKey
|
Public keypairs.PublicKey `json:"jwk"`
|
||||||
thumbprint := thumbprintKey(pub)
|
Protected map[string]interface{} `json:"protected"`
|
||||||
|
Payload map[string]interface{} `json:"payload"`
|
||||||
http.HandleFunc("/api/new-nonce", func(w http.ResponseWriter, r *http.Request) {
|
Signature string `json:"signature"`
|
||||||
baseURL := getBaseURL(r)
|
Verified bool `json:"verified"`
|
||||||
/*
|
Errors []string `json:"errors"`
|
||||||
res.statusCode = 200;
|
|
||||||
res.setHeader("Cache-Control", "max-age=0, no-cache, no-store");
|
|
||||||
// TODO
|
|
||||||
//res.setHeader("Date", "Sun, 10 Mar 2019 08:04:45 GMT");
|
|
||||||
// is this the expiration of the nonce itself? methinks maybe so
|
|
||||||
//res.setHeader("Expires", "Sun, 10 Mar 2019 08:04:45 GMT");
|
|
||||||
// TODO use one of the registered domains
|
|
||||||
//var indexUrl = "https://acme-staging-v02.api.letsencrypt.org/index"
|
|
||||||
*/
|
|
||||||
//var port = (state.config.ipc && state.config.ipc.port || state._ipc.port || undefined);
|
|
||||||
//var indexUrl = "http://localhost:" + port + "/index";
|
|
||||||
indexUrl := baseURL + "/index"
|
|
||||||
w.Header().Set("Link", "<"+indexUrl+">;rel=\"index\"")
|
|
||||||
w.Header().Set("Cache-Control", "max-age=0, no-cache, no-store")
|
|
||||||
w.Header().Set("Pragma", "no-cache")
|
|
||||||
//res.setHeader("Strict-Transport-Security", "max-age=604800");
|
|
||||||
|
|
||||||
w.Header().Set("X-Frame-Options", "DENY")
|
|
||||||
issueNonce(w, r)
|
|
||||||
})
|
|
||||||
|
|
||||||
http.HandleFunc("/api/new-account", requireNonce(func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
http.Error(w, "Not Implemented", http.StatusNotImplemented)
|
|
||||||
}))
|
|
||||||
|
|
||||||
http.HandleFunc("/api/jwks", func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
log.Printf("%s %s %s", r.Method, r.Host, r.URL.Path)
|
|
||||||
if "POST" != r.Method {
|
|
||||||
http.Error(w, "Method Not Allowed", http.StatusMethodNotAllowed)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
tok := make(map[string]interface{})
|
|
||||||
decoder := json.NewDecoder(r.Body)
|
|
||||||
err := decoder.Decode(&tok)
|
|
||||||
if nil != err {
|
|
||||||
http.Error(w, "Bad Request: invalid json", http.StatusBadRequest)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
defer r.Body.Close()
|
|
||||||
|
|
||||||
// TODO better, JSON error messages
|
|
||||||
if _, ok := tok["d"]; ok {
|
|
||||||
http.Error(w, "Bad Request: private key", http.StatusBadRequest)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
kty, _ := tok["kty"].(string)
|
|
||||||
switch kty {
|
|
||||||
case "EC":
|
|
||||||
postEC(jwksPrefix, tok, w, r)
|
|
||||||
case "RSA":
|
|
||||||
postRSA(jwksPrefix, tok, w, r)
|
|
||||||
default:
|
|
||||||
http.Error(w, "Bad Request: only EC and RSA keys are supported", http.StatusBadRequest)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
http.HandleFunc("/access_token", func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
log.Printf("%s %s\n", r.Method, r.URL.Path)
|
|
||||||
_, _, token := GenToken(getBaseURL(r), priv, r.URL.Query())
|
|
||||||
fmt.Fprintf(w, token)
|
|
||||||
})
|
|
||||||
|
|
||||||
http.HandleFunc("/authorization_header", func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
log.Printf("%s %s\n", r.Method, r.URL.Path)
|
|
||||||
|
|
||||||
var header string
|
|
||||||
headers, _ := r.URL.Query()["header"]
|
|
||||||
if 0 == len(headers) {
|
|
||||||
header = "Authorization"
|
|
||||||
} else {
|
|
||||||
header = headers[0]
|
|
||||||
}
|
|
||||||
|
|
||||||
var prefix string
|
|
||||||
prefixes, _ := r.URL.Query()["prefix"]
|
|
||||||
if 0 == len(prefixes) {
|
|
||||||
prefix = "Bearer "
|
|
||||||
} else {
|
|
||||||
prefix = prefixes[0]
|
|
||||||
}
|
|
||||||
|
|
||||||
_, _, token := GenToken(getBaseURL(r), priv, r.URL.Query())
|
|
||||||
fmt.Fprintf(w, "%s: %s%s", header, prefix, token)
|
|
||||||
})
|
|
||||||
|
|
||||||
http.HandleFunc("/key.jwk.json", func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
log.Printf("%s %s", r.Method, r.URL.Path)
|
|
||||||
fmt.Fprintf(w, `{ "kty": "EC" , "crv": %q , "d": %q , "x": %q , "y": %q , "ext": true , "key_ops": ["sign"] }`, jwk.Crv, jwk.D, jwk.X, jwk.Y)
|
|
||||||
})
|
|
||||||
|
|
||||||
http.HandleFunc("/.well-known/openid-configuration", func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
baseURL := getBaseURL(r)
|
|
||||||
log.Printf("%s %s\n", r.Method, r.URL.Path)
|
|
||||||
fmt.Fprintf(w, `{ "issuer": "%s", "jwks_uri": "%s/.well-known/jwks.json" }`, baseURL, baseURL)
|
|
||||||
})
|
|
||||||
|
|
||||||
http.HandleFunc("/.well-known/jwks.json", func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
log.Printf("%s %s %s", r.Method, r.Host, r.URL.Path)
|
|
||||||
parts := strings.Split(r.Host, ".")
|
|
||||||
kid := parts[0]
|
|
||||||
|
|
||||||
b, err := ioutil.ReadFile(filepath.Join(jwksPrefix, strings.ToLower(kid)+".jwk.json"))
|
|
||||||
if nil != err {
|
|
||||||
//http.Error(w, "Not Found", http.StatusNotFound)
|
|
||||||
jwkstr := fmt.Sprintf(
|
|
||||||
`{ "keys": [ { "kty": "EC" , "crv": %q , "x": %q , "y": %q , "kid": %q , "ext": true , "key_ops": ["verify"] , "exp": %s } ] }`,
|
|
||||||
jwk.Crv, jwk.X, jwk.Y, thumbprint, strconv.FormatInt(time.Now().Add(15*time.Minute).Unix(), 10),
|
|
||||||
)
|
|
||||||
fmt.Println(jwkstr)
|
|
||||||
fmt.Fprintf(w, jwkstr)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
tok := &PublicJWK{}
|
|
||||||
err = json.Unmarshal(b, tok)
|
|
||||||
if nil != err {
|
|
||||||
// TODO delete the bad file?
|
|
||||||
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
jwkstr := fmt.Sprintf(
|
|
||||||
`{ "keys": [ { "kty": "EC", "crv": %q, "x": %q, "y": %q, "kid": %q,`+
|
|
||||||
` "ext": true, "key_ops": ["verify"], "exp": %s } ] }`,
|
|
||||||
tok.Crv, tok.X, tok.Y, tok.KeyID, strconv.FormatInt(time.Now().Add(15*time.Minute).Unix(), 10),
|
|
||||||
)
|
|
||||||
fmt.Println(jwkstr)
|
|
||||||
fmt.Fprintf(w, jwkstr)
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func parseExp(exp string) (int, error) {
|
func (t *InspectableToken) MarshalJSON() ([]byte, error) {
|
||||||
if "" == exp {
|
pub := keypairs.MarshalJWKPublicKey(t.Public)
|
||||||
exp = "15m"
|
header, _ := json.Marshal(t.Protected)
|
||||||
}
|
payload, _ := json.Marshal(t.Payload)
|
||||||
mult := 1
|
errs, _ := json.Marshal(t.Errors)
|
||||||
switch exp[len(exp)-1] {
|
return []byte(fmt.Sprintf(
|
||||||
case 'w':
|
`{"jwk":%s,"protected":%s,"payload":%s,"signature":%q,"verified":%t,"errors":%s}`,
|
||||||
mult *= 7
|
pub, header, payload, t.Signature, t.Verified, errs,
|
||||||
fallthrough
|
)), nil
|
||||||
case 'd':
|
|
||||||
mult *= 24
|
|
||||||
fallthrough
|
|
||||||
case 'h':
|
|
||||||
mult *= 60
|
|
||||||
fallthrough
|
|
||||||
case 'm':
|
|
||||||
mult *= 60
|
|
||||||
fallthrough
|
|
||||||
case 's':
|
|
||||||
// no fallthrough
|
|
||||||
default:
|
|
||||||
// could be 'k' or 'z', but we assume its empty
|
|
||||||
exp += "s"
|
|
||||||
}
|
|
||||||
|
|
||||||
num, err := strconv.Atoi(exp[:len(exp)-1])
|
|
||||||
if nil != err {
|
|
||||||
return 0, err
|
|
||||||
}
|
|
||||||
return num * mult, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func postEC(jwksPrefix string, tok map[string]interface{}, w http.ResponseWriter, r *http.Request) {
|
var defaultFrom string
|
||||||
crv, ok := tok["crv"].(string)
|
var defaultReplyTo string
|
||||||
if 5 != len(crv) || "P-" != crv[:2] {
|
|
||||||
http.Error(w, "Bad Request: bad curve", http.StatusBadRequest)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
x, ok := tok["x"].(string)
|
|
||||||
if !ok {
|
|
||||||
http.Error(w, "Bad Request: missing 'x'", http.StatusBadRequest)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
y, ok := tok["y"].(string)
|
|
||||||
if !ok {
|
|
||||||
http.Error(w, "Bad Request: missing 'y'", http.StatusBadRequest)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
thumbprintable := []byte(
|
var salt []byte
|
||||||
fmt.Sprintf(`{"crv":%q,"kty":"EC","x":%q,"y":%q}`, crv, x, y),
|
|
||||||
)
|
|
||||||
alg := crv[2:]
|
|
||||||
|
|
||||||
var thumb []byte
|
func Init() {
|
||||||
switch alg {
|
var err error
|
||||||
case "256":
|
salt64 := os.Getenv("SALT")
|
||||||
hash := sha256.Sum256(thumbprintable)
|
salt, err = base64.RawURLEncoding.DecodeString(salt64)
|
||||||
thumb = hash[:]
|
if len(salt64) < 22 || nil != err {
|
||||||
case "384":
|
panic("SALT must be set as 22+ character base64")
|
||||||
hash := sha512.Sum384(thumbprintable)
|
|
||||||
thumb = hash[:]
|
|
||||||
case "521":
|
|
||||||
fallthrough
|
|
||||||
case "512":
|
|
||||||
hash := sha512.Sum512(thumbprintable)
|
|
||||||
thumb = hash[:]
|
|
||||||
default:
|
|
||||||
http.Error(w, "Bad Request: bad key length or curve", http.StatusBadRequest)
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
defaultFrom = os.Getenv("MAILER_FROM")
|
||||||
|
defaultReplyTo = os.Getenv("MAILER_REPLY_TO")
|
||||||
|
//nonces = make(map[string]int64)
|
||||||
|
//nonCh = make(chan string)
|
||||||
|
|
||||||
kid := base64.RawURLEncoding.EncodeToString(thumb)
|
/*
|
||||||
if kid2, _ := tok["kid"].(string); "" != kid2 && kid != kid2 {
|
go func() {
|
||||||
http.Error(w, "Bad Request: kid should be "+kid, http.StatusBadRequest)
|
for {
|
||||||
return
|
nonce := <- nonCh
|
||||||
}
|
nonces[nonce] = time.Now().Unix()
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
*/
|
||||||
|
|
||||||
pub := []byte(fmt.Sprintf(
|
go func() {
|
||||||
`{"crv":%q,"kid":%q,"kty":"EC","x":%q,"y":%q}`, crv, kid, x, y,
|
for {
|
||||||
))
|
time.Sleep(15 * time.Second)
|
||||||
|
hashcashes.vacuum()
|
||||||
// TODO allow posting at the top-level?
|
}
|
||||||
// TODO support a group of keys by PPID
|
}()
|
||||||
// (right now it's only by KID)
|
|
||||||
if !strings.HasPrefix(r.Host, strings.ToLower(kid)+".") {
|
|
||||||
http.Error(w, "Bad Request: prefix should be "+kid, http.StatusBadRequest)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := ioutil.WriteFile(
|
|
||||||
filepath.Join(jwksPrefix, strings.ToLower(kid)+".jwk.json"),
|
|
||||||
pub,
|
|
||||||
0644,
|
|
||||||
); nil != err {
|
|
||||||
fmt.Println("can't write file")
|
|
||||||
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
baseURL := getBaseURL(r)
|
|
||||||
w.Write([]byte(fmt.Sprintf(
|
|
||||||
`{ "iss":%q, "jwks_url":%q }`, baseURL+"/", baseURL+"/.well-known/jwks.json",
|
|
||||||
)))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func postRSA(jwksPrefix string, tok map[string]interface{}, w http.ResponseWriter, r *http.Request) {
|
func GenToken(host string, privkey keypairs.PrivateKey, query url.Values) (string, string, string) {
|
||||||
e, ok := tok["e"].(string)
|
thumbprint := keypairs.ThumbprintPublicKey(keypairs.NewPublicKey(privkey.Public()))
|
||||||
if !ok {
|
// TODO keypairs.Alg(key)
|
||||||
http.Error(w, "Bad Request: missing 'e'", http.StatusBadRequest)
|
alg := "ES256"
|
||||||
return
|
switch privkey.(type) {
|
||||||
|
case *rsa.PrivateKey:
|
||||||
|
alg = "RS256"
|
||||||
}
|
}
|
||||||
n, ok := tok["n"].(string)
|
protected := fmt.Sprintf(`{"typ":"JWT","alg":%q,"kid":"%s"}`, alg, thumbprint)
|
||||||
if !ok {
|
|
||||||
http.Error(w, "Bad Request: missing 'n'", http.StatusBadRequest)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
thumbprintable := []byte(
|
|
||||||
fmt.Sprintf(`{"e":%q,"kty":"RSA","n":%q}`, e, n),
|
|
||||||
)
|
|
||||||
|
|
||||||
var thumb []byte
|
|
||||||
// TODO handle bit lengths well
|
|
||||||
switch 3 * (len(n) / 4.0) {
|
|
||||||
case 256:
|
|
||||||
hash := sha256.Sum256(thumbprintable)
|
|
||||||
thumb = hash[:]
|
|
||||||
case 384:
|
|
||||||
hash := sha512.Sum384(thumbprintable)
|
|
||||||
thumb = hash[:]
|
|
||||||
case 512:
|
|
||||||
hash := sha512.Sum512(thumbprintable)
|
|
||||||
thumb = hash[:]
|
|
||||||
default:
|
|
||||||
http.Error(w, "Bad Request: only standard RSA key lengths (2048, 3072, 4096) are supported", http.StatusBadRequest)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
kid := base64.RawURLEncoding.EncodeToString(thumb)
|
|
||||||
if kid2, _ := tok["kid"].(string); "" != kid2 && kid != kid2 {
|
|
||||||
http.Error(w, "Bad Request: kid should be "+kid, http.StatusBadRequest)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
pub := []byte(fmt.Sprintf(
|
|
||||||
`{"e":%q,"kid":%q,"kty":"EC","n":%q}`, e, kid, n,
|
|
||||||
))
|
|
||||||
|
|
||||||
// TODO allow posting at the top-level?
|
|
||||||
// TODO support a group of keys by PPID
|
|
||||||
// (right now it's only by KID)
|
|
||||||
if !strings.HasPrefix(r.Host, strings.ToLower(kid)+".") {
|
|
||||||
http.Error(w, "Bad Request: prefix should be "+kid, http.StatusBadRequest)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := ioutil.WriteFile(
|
|
||||||
filepath.Join(jwksPrefix, strings.ToLower(kid)+".jwk.json"),
|
|
||||||
pub,
|
|
||||||
0644,
|
|
||||||
); nil != err {
|
|
||||||
fmt.Println("can't write file")
|
|
||||||
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
baseURL := getBaseURL(r)
|
|
||||||
w.Write([]byte(fmt.Sprintf(
|
|
||||||
`{ "iss":%q, "jwks_url":%q }`, baseURL+"/", baseURL+"/.well-known/jwks.json",
|
|
||||||
)))
|
|
||||||
}
|
|
||||||
|
|
||||||
func GenToken(host string, priv *ecdsa.PrivateKey, query url.Values) (string, string, string) {
|
|
||||||
thumbprint := thumbprintKey(&priv.PublicKey)
|
|
||||||
protected := fmt.Sprintf(`{"typ":"JWT","alg":"ES256","kid":"%s"}`, thumbprint)
|
|
||||||
protected64 := base64.RawURLEncoding.EncodeToString([]byte(protected))
|
protected64 := base64.RawURLEncoding.EncodeToString([]byte(protected))
|
||||||
|
|
||||||
exp, err := parseExp(query.Get("exp"))
|
exp, err := time.ParseDuration(query.Get("exp"))
|
||||||
if nil != err {
|
if nil != err {
|
||||||
// cryptic error code
|
// cryptic error code
|
||||||
// TODO propagate error
|
// TODO propagate error
|
||||||
@ -367,94 +112,61 @@ func GenToken(host string, priv *ecdsa.PrivateKey, query url.Values) (string, st
|
|||||||
|
|
||||||
payload := fmt.Sprintf(
|
payload := fmt.Sprintf(
|
||||||
`{"iss":"%s/","sub":"dummy","exp":%s}`,
|
`{"iss":"%s/","sub":"dummy","exp":%s}`,
|
||||||
host, strconv.FormatInt(time.Now().Add(time.Duration(exp)*time.Second).Unix(), 10),
|
host, strconv.FormatInt(time.Now().Add(exp*time.Second).Unix(), 10),
|
||||||
)
|
)
|
||||||
payload64 := base64.RawURLEncoding.EncodeToString([]byte(payload))
|
payload64 := base64.RawURLEncoding.EncodeToString([]byte(payload))
|
||||||
|
|
||||||
hash := sha256.Sum256([]byte(fmt.Sprintf(`%s.%s`, protected64, payload64)))
|
hash := sha256.Sum256([]byte(fmt.Sprintf(`%s.%s`, protected64, payload64)))
|
||||||
r, s, _ := ecdsa.Sign(rand.Reader, priv, hash[:])
|
sig := JOSESign(privkey, hash[:])
|
||||||
rb := r.Bytes()
|
sig64 := base64.RawURLEncoding.EncodeToString(sig)
|
||||||
for len(rb) < 32 {
|
token := fmt.Sprintf("%s.%s.%s\n", protected64, payload64, sig64)
|
||||||
rb = append([]byte{0}, rb...)
|
|
||||||
}
|
|
||||||
sb := s.Bytes()
|
|
||||||
for len(rb) < 32 {
|
|
||||||
sb = append([]byte{0}, sb...)
|
|
||||||
}
|
|
||||||
sig64 := base64.RawURLEncoding.EncodeToString(append(rb, sb...))
|
|
||||||
token := fmt.Sprintf(`%s.%s.%s`, protected64, payload64, sig64)
|
|
||||||
return protected, payload, token
|
return protected, payload, token
|
||||||
}
|
}
|
||||||
|
|
||||||
func ParseKey(jwk *PrivateJWK) *ecdsa.PrivateKey {
|
func JOSESign(privkey keypairs.PrivateKey, hash []byte) []byte {
|
||||||
xb, _ := base64.RawURLEncoding.DecodeString(jwk.X)
|
var sig []byte
|
||||||
xi := &big.Int{}
|
|
||||||
xi.SetBytes(xb)
|
|
||||||
yb, _ := base64.RawURLEncoding.DecodeString(jwk.Y)
|
|
||||||
yi := &big.Int{}
|
|
||||||
yi.SetBytes(yb)
|
|
||||||
pub := &ecdsa.PublicKey{
|
|
||||||
Curve: elliptic.P256(),
|
|
||||||
X: xi,
|
|
||||||
Y: yi,
|
|
||||||
}
|
|
||||||
|
|
||||||
db, _ := base64.RawURLEncoding.DecodeString(jwk.D)
|
switch k := privkey.(type) {
|
||||||
di := &big.Int{}
|
case *rsa.PrivateKey:
|
||||||
di.SetBytes(db)
|
panic("TODO: implement rsa sign")
|
||||||
priv := &ecdsa.PrivateKey{
|
case *ecdsa.PrivateKey:
|
||||||
PublicKey: *pub,
|
r, s, _ := ecdsa.Sign(rndsrc, k, hash[:])
|
||||||
D: di,
|
rb := r.Bytes()
|
||||||
}
|
for len(rb) < 32 {
|
||||||
return priv
|
rb = append([]byte{0}, rb...)
|
||||||
}
|
|
||||||
|
|
||||||
func thumbprintKey(pub *ecdsa.PublicKey) string {
|
|
||||||
minpub := []byte(fmt.Sprintf(`{"crv":%q,"kty":"EC","x":%q,"y":%q}`, "P-256", pub.X, pub.Y))
|
|
||||||
sha := sha256.Sum256(minpub)
|
|
||||||
return base64.RawURLEncoding.EncodeToString(sha[:])
|
|
||||||
}
|
|
||||||
|
|
||||||
func issueNonce(w http.ResponseWriter, r *http.Request) {
|
|
||||||
b := make([]byte, 16)
|
|
||||||
_, _ = rand.Read(b)
|
|
||||||
nonce := base64.RawURLEncoding.EncodeToString(b)
|
|
||||||
nonces[nonce] = time.Now().Unix()
|
|
||||||
|
|
||||||
w.Header().Set("Replay-Nonce", nonce)
|
|
||||||
}
|
|
||||||
|
|
||||||
func requireNonce(next http.HandlerFunc) http.HandlerFunc {
|
|
||||||
return func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
nonce := r.Header.Get("Replay-Nonce")
|
|
||||||
// TODO expire nonces every so often
|
|
||||||
t := nonces[nonce]
|
|
||||||
if 0 == t {
|
|
||||||
http.Error(
|
|
||||||
w,
|
|
||||||
`{ "error": "invalid or expired nonce", "error_code": "ENONCE" }`,
|
|
||||||
http.StatusBadRequest,
|
|
||||||
)
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
sb := s.Bytes()
|
||||||
delete(nonces, nonce)
|
for len(rb) < 32 {
|
||||||
issueNonce(w, r)
|
sb = append([]byte{0}, sb...)
|
||||||
|
}
|
||||||
next(w, r)
|
sig = append(rb, sb...)
|
||||||
}
|
}
|
||||||
|
return sig
|
||||||
}
|
}
|
||||||
|
|
||||||
func getBaseURL(r *http.Request) string {
|
// TODO: move to keypairs
|
||||||
var scheme string
|
|
||||||
if nil != r.TLS || "https" == r.Header.Get("X-Forwarded-Proto") {
|
func JOSEVerify(pubkey keypairs.PublicKey, hash []byte, sig []byte) bool {
|
||||||
scheme = "https:"
|
|
||||||
} else {
|
switch pub := pubkey.Key().(type) {
|
||||||
scheme = "http:"
|
case *rsa.PublicKey:
|
||||||
|
// TODO keypairs.Size(key) to detect key size ?
|
||||||
|
//alg := "SHA256"
|
||||||
|
// TODO: this hasn't been tested yet
|
||||||
|
if err := rsa.VerifyPKCS1v15(pub, crypto.SHA256, hash, sig); nil != err {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
case *ecdsa.PublicKey:
|
||||||
|
r := &big.Int{}
|
||||||
|
r.SetBytes(sig[0:32])
|
||||||
|
s := &big.Int{}
|
||||||
|
s.SetBytes(sig[32:])
|
||||||
|
fmt.Println("debug: sig len:", len(sig))
|
||||||
|
fmt.Println("debug: r, s:", r, s)
|
||||||
|
return ecdsa.Verify(pub, hash, r, s)
|
||||||
|
default:
|
||||||
|
panic("impossible condition: non-rsa/non-ecdsa key")
|
||||||
|
return false
|
||||||
}
|
}
|
||||||
return fmt.Sprintf(
|
|
||||||
"%s//%s",
|
|
||||||
scheme,
|
|
||||||
r.Host,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
@ -1,12 +1,476 @@
|
|||||||
package mockid
|
package mockid
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
|
"crypto/ecdsa"
|
||||||
|
"crypto/elliptic"
|
||||||
|
"crypto/rsa"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
"log"
|
||||||
|
mathrand "math/rand"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"net/url"
|
||||||
|
"os"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
|
||||||
|
"git.coolaj86.com/coolaj86/go-mockid/xkeypairs"
|
||||||
|
"git.rootprojects.org/root/keypairs"
|
||||||
//keypairs "github.com/big-squid/go-keypairs"
|
//keypairs "github.com/big-squid/go-keypairs"
|
||||||
//"github.com/big-squid/go-keypairs/keyfetch/uncached"
|
//"github.com/big-squid/go-keypairs/keyfetch/uncached"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestTest(t *testing.T) {
|
var srv *httptest.Server
|
||||||
t.Fatal("no test")
|
|
||||||
|
type TestReader struct{}
|
||||||
|
|
||||||
|
func (TestReader) Read(p []byte) (n int, err error) {
|
||||||
|
return mathrand.Read(p)
|
||||||
|
}
|
||||||
|
|
||||||
|
var testrnd = TestReader{}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
xkeypairs.RandomReader = testrnd
|
||||||
|
rndsrc = testrnd
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMain(m *testing.M) {
|
||||||
|
mathrand.Seed(0) // Predictable results
|
||||||
|
|
||||||
|
os.Setenv("SALT", "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX")
|
||||||
|
jwksPrefix := "public-jwks"
|
||||||
|
err := os.MkdirAll(jwksPrefix, 0755)
|
||||||
|
if nil != err {
|
||||||
|
fmt.Fprintf(os.Stderr, "couldn't write %q: %s", jwksPrefix, err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
privkey, _ := ecdsa.GenerateKey(elliptic.P256(), rndsrc)
|
||||||
|
mux := Route(jwksPrefix, privkey)
|
||||||
|
|
||||||
|
srv = httptest.NewServer(mux)
|
||||||
|
|
||||||
|
//fs := http.FileServer(http.Dir("public"))
|
||||||
|
//http.Handle("/", fs)
|
||||||
|
|
||||||
|
os.Exit(m.Run())
|
||||||
|
}
|
||||||
|
|
||||||
|
//func TestSelfSignWithoutExp(t *testing.T)
|
||||||
|
//func TestSelfSignWithJTIWithoutExp(t *testing.T)
|
||||||
|
|
||||||
|
func TestVerifyExpired(t *testing.T) {
|
||||||
|
jwt := "eyJfc2VlZCI6LTEzMDY3NDU1MDQxNDQsImFsZyI6IlJTMjU2IiwiandrIjp7ImUiOiJBUUFCIiwia2lkIjoiSEZ4ZTlGV1dVc2N3bjltaVozSXNJeWMwMjMtbEJ1UmtvOEJpVV9IRG9KOCIsImt0eSI6IlJTQSIsIm4iOiJ2NUZkSTdYaC0wekxWVEVQZl94ekdIUVpDcEZ2MWR2N2h3eHhrVjctYmxpYmt6LXIxUG9lZ3lQYzFXMjZlWFBvd0xQQXQ3a3dHQnVOdjdMVjh5MEtvMkxOZklaXzRILW54SkJPaWIybXlHOVVfQ29WRDBiM3NBWTdmcDd2QlV1bTBXYVM4R3hZOGtYU0ZOS0VTY0NDNVBpSmFyblNISk1PcUdIVm51YmpsSjl5c1NyNmNsaGpxc0R4dU9qOHpxamF2MUFxek1STWVpRl9CREJsOUFoUGNZSHpHN0JtaXB5UEo2XzBwdWNLTi0tUDZDRk92d05SVGx2ek41RmlRM3VHcy1fMHcwQzVMZWJ6N21BNmJNTFdXc0tRRFBvb3cxallCWHJKdVF1WkZoSmxLMmdidm9ZcV85dWhfLUM1Z3pPZnR4UHBCNnhtY3RfelVaeUdwUUxnQlEiLCJ1c2UiOiJzaWcifSwidHlwIjoiSldUIn0.eyJleHAiOjE1OTY2MTQ3NTYsInN1YiI6ImJhbmFuYXMifQ.qHpzlglOfZMzE3CTNAUXld_wC62JTAJuoQfMaNeFa-XPtYB2Maj8_w3YmRZg_q5S6y9ToCmZ8nWd1kuMheA5qBKOUQeQH47Jts5zWLd0UBckIHo5lK4mk0bUWuiNgr7c9DY6k1DIdFaavyWCXbhFwG0X83qlMhQlPh02dDpCuU78Nn2hF3mZETQKpBIVESYtfeU1Xy3OU_am0kwcN2klLcdweOcrLx_ONfcvAGY3KiIdFiz0ViySAsQ39BiSSvoDYqOOOi41Hky67bnyZQOdalQC_95McTeXApzmGXRUE74Gj-S8c9e5it5d4QZLPaQ1JHzUKz1s7TPvThIn58NA-g"
|
||||||
|
client := srv.Client()
|
||||||
|
urlstr, _ := url.Parse(srv.URL + "/debug/verify")
|
||||||
|
|
||||||
|
req := &http.Request{
|
||||||
|
Method: "POST",
|
||||||
|
URL: urlstr,
|
||||||
|
Header: http.Header{},
|
||||||
|
}
|
||||||
|
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", jwt))
|
||||||
|
res, err := client.Do(req)
|
||||||
|
if nil != err {
|
||||||
|
t.Error(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
data, err := ioutil.ReadAll(res.Body)
|
||||||
|
if nil != err {
|
||||||
|
t.Error(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if 200 == res.StatusCode {
|
||||||
|
log.Printf(string(data))
|
||||||
|
t.Error(fmt.Errorf("did not expect successful status code: %d", res.StatusCode))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestVerifySelfSignedJWT(t *testing.T) {
|
||||||
|
jwt := "eyJfc2VlZCI6LTEzMDY3NDU1MDQxNDQsImFsZyI6IlJTMjU2IiwiandrIjp7ImUiOiJBUUFCIiwia2lkIjoiSEZ4ZTlGV1dVc2N3bjltaVozSXNJeWMwMjMtbEJ1UmtvOEJpVV9IRG9KOCIsImt0eSI6IlJTQSIsIm4iOiJ2NUZkSTdYaC0wekxWVEVQZl94ekdIUVpDcEZ2MWR2N2h3eHhrVjctYmxpYmt6LXIxUG9lZ3lQYzFXMjZlWFBvd0xQQXQ3a3dHQnVOdjdMVjh5MEtvMkxOZklaXzRILW54SkJPaWIybXlHOVVfQ29WRDBiM3NBWTdmcDd2QlV1bTBXYVM4R3hZOGtYU0ZOS0VTY0NDNVBpSmFyblNISk1PcUdIVm51YmpsSjl5c1NyNmNsaGpxc0R4dU9qOHpxamF2MUFxek1STWVpRl9CREJsOUFoUGNZSHpHN0JtaXB5UEo2XzBwdWNLTi0tUDZDRk92d05SVGx2ek41RmlRM3VHcy1fMHcwQzVMZWJ6N21BNmJNTFdXc0tRRFBvb3cxallCWHJKdVF1WkZoSmxLMmdidm9ZcV85dWhfLUM1Z3pPZnR4UHBCNnhtY3RfelVaeUdwUUxnQlEiLCJ1c2UiOiJzaWcifSwidHlwIjoiSldUIn0.eyJleHAiOjE1OTY2MTQ3NTYsInN1YiI6ImJhbmFuYXMifQ.qHpzlglOfZMzE3CTNAUXld_wC62JTAJuoQfMaNeFa-XPtYB2Maj8_w3YmRZg_q5S6y9ToCmZ8nWd1kuMheA5qBKOUQeQH47Jts5zWLd0UBckIHo5lK4mk0bUWuiNgr7c9DY6k1DIdFaavyWCXbhFwG0X83qlMhQlPh02dDpCuU78Nn2hF3mZETQKpBIVESYtfeU1Xy3OU_am0kwcN2klLcdweOcrLx_ONfcvAGY3KiIdFiz0ViySAsQ39BiSSvoDYqOOOi41Hky67bnyZQOdalQC_95McTeXApzmGXRUE74Gj-S8c9e5it5d4QZLPaQ1JHzUKz1s7TPvThIn58NA-g"
|
||||||
|
client := srv.Client()
|
||||||
|
urlstr, _ := url.Parse(srv.URL + "/debug/verify?exp=false")
|
||||||
|
|
||||||
|
req := &http.Request{
|
||||||
|
Method: "POST",
|
||||||
|
URL: urlstr,
|
||||||
|
//Body: ioutil.NopCloser(bytes.NewReader(jws)),
|
||||||
|
Header: http.Header{},
|
||||||
|
}
|
||||||
|
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", jwt))
|
||||||
|
res, err := client.Do(req)
|
||||||
|
if nil != err {
|
||||||
|
t.Error(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
data, err := ioutil.ReadAll(res.Body)
|
||||||
|
if nil != err {
|
||||||
|
t.Error(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if 200 != res.StatusCode {
|
||||||
|
log.Printf(string(data))
|
||||||
|
t.Error(fmt.Errorf("bad status code: %d", res.StatusCode))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("TODO: verify, and verify non-self-signed")
|
||||||
|
log.Printf(string(data))
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSelfSign(t *testing.T) {
|
||||||
|
client := srv.Client()
|
||||||
|
//urlstr, _ := url.Parse(srv.URL + "/debug/jose.jws.json")
|
||||||
|
urlstr, _ := url.Parse(srv.URL + "/debug/jose.jws.jwt")
|
||||||
|
|
||||||
|
//fmt.Println("URL:", srv.URL, urlstr)
|
||||||
|
tokenRequest := []byte(`{"seed":"test","header":{"_jwk":true},"claims":{"sub":"bananas","exp":"10m"}}`)
|
||||||
|
res, err := client.Do(&http.Request{
|
||||||
|
Method: "POST",
|
||||||
|
URL: urlstr,
|
||||||
|
Body: ioutil.NopCloser(bytes.NewReader(tokenRequest)),
|
||||||
|
})
|
||||||
|
if nil != err {
|
||||||
|
t.Error(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if 200 != res.StatusCode {
|
||||||
|
t.Error(fmt.Errorf("bad status code: %d", res.StatusCode))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := ioutil.ReadAll(res.Body)
|
||||||
|
if nil != err {
|
||||||
|
t.Error(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("TODO: verify, and verify non-self-signed")
|
||||||
|
log.Printf(string(data))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGenerateJWK(t *testing.T) {
|
||||||
|
client := srv.Client()
|
||||||
|
urlstr, _ := url.Parse(srv.URL + "/debug/private.jwk.json")
|
||||||
|
//fmt.Println("URL:", srv.URL, urlstr)
|
||||||
|
res, err := client.Do(&http.Request{
|
||||||
|
Method: "POST",
|
||||||
|
URL: urlstr,
|
||||||
|
})
|
||||||
|
if nil != err {
|
||||||
|
//t.Fatal(err)
|
||||||
|
t.Error(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if 200 != res.StatusCode {
|
||||||
|
t.Error(fmt.Errorf("bad status code: %d", res.StatusCode))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := ioutil.ReadAll(res.Body)
|
||||||
|
if nil != err {
|
||||||
|
//t.Fatal(err)
|
||||||
|
t.Error(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
jwk := map[string]string{}
|
||||||
|
err = json.Unmarshal(data, &jwk)
|
||||||
|
if nil != err {
|
||||||
|
//t.Fatal(err)
|
||||||
|
t.Error(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if "" == jwk["d"] {
|
||||||
|
t.Fatal("Missing key 'd' from supposed private key")
|
||||||
|
}
|
||||||
|
|
||||||
|
key, err := keypairs.ParsePrivateKey(data)
|
||||||
|
if nil != err {
|
||||||
|
t.Error(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
switch key.(type) {
|
||||||
|
case *rsa.PrivateKey:
|
||||||
|
// no-op
|
||||||
|
//log.Println("is RSA")
|
||||||
|
case *ecdsa.PrivateKey:
|
||||||
|
// no-op
|
||||||
|
//log.Println("is EC")
|
||||||
|
default:
|
||||||
|
t.Fatal(errors.New("impossible key type"))
|
||||||
|
}
|
||||||
|
|
||||||
|
//fmt.Printf("%#v\n", jwk)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGenWithSeed(t *testing.T) {
|
||||||
|
// Key A
|
||||||
|
client := srv.Client()
|
||||||
|
urlstr, _ := url.Parse(srv.URL + "/debug/private.jwk.json")
|
||||||
|
res, err := client.Do(&http.Request{
|
||||||
|
Method: "POST",
|
||||||
|
URL: urlstr,
|
||||||
|
Body: ioutil.NopCloser(bytes.NewReader([]byte(`{"seed":"test"}`))),
|
||||||
|
})
|
||||||
|
if nil != err {
|
||||||
|
//t.Fatal(err)
|
||||||
|
t.Error(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if 200 != res.StatusCode {
|
||||||
|
t.Error(fmt.Errorf("bad status code: %d", res.StatusCode))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
dataA, err := ioutil.ReadAll(res.Body)
|
||||||
|
if nil != err {
|
||||||
|
//t.Fatal(err)
|
||||||
|
t.Error(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// See https://github.com/square/go-jose/issues/189
|
||||||
|
for i := 0; i < 8; i++ {
|
||||||
|
// Key B
|
||||||
|
client = srv.Client()
|
||||||
|
urlstr, _ = url.Parse(srv.URL + "/debug/private.jwk.json")
|
||||||
|
res, err = client.Do(&http.Request{
|
||||||
|
Method: "POST",
|
||||||
|
URL: urlstr,
|
||||||
|
Body: ioutil.NopCloser(bytes.NewReader([]byte(`{"seed":"test"}`))),
|
||||||
|
})
|
||||||
|
if nil != err {
|
||||||
|
//t.Fatal(err)
|
||||||
|
t.Error(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if 200 != res.StatusCode {
|
||||||
|
t.Error(fmt.Errorf("bad status code: %d", res.StatusCode))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
dataB, err := ioutil.ReadAll(res.Body)
|
||||||
|
if nil != err {
|
||||||
|
//t.Fatal(err)
|
||||||
|
t.Error(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if '{' != dataA[0] || len(dataA) < 100 || string(dataA) != string(dataB) {
|
||||||
|
log.Println(string(dataA))
|
||||||
|
log.Println(string(dataB))
|
||||||
|
t.Error(errors.New("keys with identical seeds should be identical"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGenWithRand(t *testing.T) {
|
||||||
|
// Key A
|
||||||
|
client := srv.Client()
|
||||||
|
urlstr, _ := url.Parse(srv.URL + "/debug/private.jwk.json")
|
||||||
|
res, err := client.Do(&http.Request{
|
||||||
|
Method: "POST",
|
||||||
|
URL: urlstr,
|
||||||
|
Body: ioutil.NopCloser(bytes.NewReader([]byte(`{"seed":""}`))),
|
||||||
|
})
|
||||||
|
if nil != err {
|
||||||
|
//t.Fatal(err)
|
||||||
|
t.Error(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if 200 != res.StatusCode {
|
||||||
|
t.Error(fmt.Errorf("bad status code: %d", res.StatusCode))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
dataA, err := ioutil.ReadAll(res.Body)
|
||||||
|
if nil != err {
|
||||||
|
//t.Fatal(err)
|
||||||
|
t.Error(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Key B
|
||||||
|
client = srv.Client()
|
||||||
|
urlstr, _ = url.Parse(srv.URL + "/debug/private.jwk.json")
|
||||||
|
res, err = client.Do(&http.Request{
|
||||||
|
Method: "POST",
|
||||||
|
URL: urlstr,
|
||||||
|
Body: ioutil.NopCloser(bytes.NewReader([]byte(`{"seed":""}`))),
|
||||||
|
})
|
||||||
|
if nil != err {
|
||||||
|
//t.Fatal(err)
|
||||||
|
t.Error(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if 200 != res.StatusCode {
|
||||||
|
t.Error(fmt.Errorf("bad status code: %d", res.StatusCode))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
dataB, err := ioutil.ReadAll(res.Body)
|
||||||
|
if nil != err {
|
||||||
|
//t.Fatal(err)
|
||||||
|
t.Error(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if string(dataA) == string(dataB) {
|
||||||
|
t.Error(errors.New("keys with identical seeds should yield identical keys"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGeneratePEM(t *testing.T) {
|
||||||
|
client := srv.Client()
|
||||||
|
urlstr, _ := url.Parse(srv.URL + "/debug/priv.pem")
|
||||||
|
//fmt.Println("URL:", srv.URL, urlstr)
|
||||||
|
res, err := client.Do(&http.Request{
|
||||||
|
Method: "POST",
|
||||||
|
URL: urlstr,
|
||||||
|
})
|
||||||
|
if nil != err {
|
||||||
|
//t.Fatal(err)
|
||||||
|
t.Error(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if 200 != res.StatusCode {
|
||||||
|
t.Error(fmt.Errorf("bad status code: %d", res.StatusCode))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := ioutil.ReadAll(res.Body)
|
||||||
|
if nil != err {
|
||||||
|
//t.Fatal(err)
|
||||||
|
t.Error(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
key, err := xkeypairs.ParsePEMPrivateKey(data)
|
||||||
|
if nil != err {
|
||||||
|
t.Error(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
switch key.(type) {
|
||||||
|
case *rsa.PrivateKey:
|
||||||
|
// no-op
|
||||||
|
//log.Println("is RSA")
|
||||||
|
case *ecdsa.PrivateKey:
|
||||||
|
// no-op
|
||||||
|
//log.Println("is EC")
|
||||||
|
default:
|
||||||
|
t.Fatal(errors.New("impossible key type"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPublicJWKWithKey(t *testing.T) {
|
||||||
|
client := srv.Client()
|
||||||
|
urlstr, _ := url.Parse(srv.URL + "/debug/public.jwk.json")
|
||||||
|
//fmt.Println("URL:", srv.URL, urlstr)
|
||||||
|
res, err := client.Do(&http.Request{
|
||||||
|
Method: "POST",
|
||||||
|
URL: urlstr,
|
||||||
|
Body: ioutil.NopCloser(bytes.NewReader([]byte(`{"key":"{\"crv\":\"P-256\",\"d\":\"s0YhjGUJpp6OvyuNS_4igrc7ddDZy5N2ANxoQm7E5sc\",\"kty\":\"EC\",\"x\":\"hPsE4OMhpd2TvrhjDgr1BhF-L1n4O-gPm1flwTh5kzo\",\"y\":\"BWZ1naEJuNOdnQ4HmbHavqdLKxoj77Fu8mkJPjSuh54\"}"}`))),
|
||||||
|
})
|
||||||
|
if nil != err {
|
||||||
|
//t.Fatal(err)
|
||||||
|
t.Error(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if 200 != res.StatusCode {
|
||||||
|
t.Error(fmt.Errorf("bad status code: %d", res.StatusCode))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := ioutil.ReadAll(res.Body)
|
||||||
|
if nil != err {
|
||||||
|
//t.Fatal(err)
|
||||||
|
t.Error(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
jwk := map[string]string{}
|
||||||
|
err = json.Unmarshal(data, &jwk)
|
||||||
|
if nil != err {
|
||||||
|
//t.Fatal(err)
|
||||||
|
t.Error(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if "" != jwk["d"] {
|
||||||
|
t.Fatal("Has private key 'd' from supposed public key")
|
||||||
|
}
|
||||||
|
|
||||||
|
if "hPsE4OMhpd2TvrhjDgr1BhF-L1n4O-gPm1flwTh5kzo" != jwk["x"] {
|
||||||
|
t.Fatal("Missing public key 'x' or 'e' from supposed public key")
|
||||||
|
}
|
||||||
|
|
||||||
|
key, err := keypairs.ParsePublicKey(data)
|
||||||
|
if nil != err {
|
||||||
|
t.Error(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
switch key.Key().(type) {
|
||||||
|
case *ecdsa.PublicKey:
|
||||||
|
// no-op
|
||||||
|
//log.Println("is EC")
|
||||||
|
default:
|
||||||
|
t.Fatal(errors.New("impossible key type"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPublicPEMWithSeed(t *testing.T) {
|
||||||
|
client := srv.Client()
|
||||||
|
urlstr, _ := url.Parse(srv.URL + "/debug/pub.pem")
|
||||||
|
//fmt.Println("URL:", srv.URL, urlstr)
|
||||||
|
res, err := client.Do(&http.Request{
|
||||||
|
Method: "POST",
|
||||||
|
URL: urlstr,
|
||||||
|
Body: ioutil.NopCloser(bytes.NewReader([]byte(`{"seed":"test"}`))),
|
||||||
|
})
|
||||||
|
if nil != err {
|
||||||
|
//t.Fatal(err)
|
||||||
|
t.Error(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if 200 != res.StatusCode {
|
||||||
|
t.Error(fmt.Errorf("bad status code: %d", res.StatusCode))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := ioutil.ReadAll(res.Body)
|
||||||
|
if nil != err {
|
||||||
|
//t.Fatal(err)
|
||||||
|
t.Error(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
key, err := keypairs.ParsePublicKey(data)
|
||||||
|
if nil != err {
|
||||||
|
t.Error(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
switch key.Key().(type) {
|
||||||
|
case *rsa.PublicKey:
|
||||||
|
// no-op
|
||||||
|
//log.Println("is RSA")
|
||||||
|
case *ecdsa.PublicKey:
|
||||||
|
// no-op
|
||||||
|
//log.Println("is EC")
|
||||||
|
default:
|
||||||
|
t.Fatal(errors.New("impossible key type"))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
64
mockid/nonce.go
Normal file
64
mockid/nonce.go
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
package mockid
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/rand"
|
||||||
|
"encoding/base64"
|
||||||
|
"net/http"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
//var nonces map[string]int64
|
||||||
|
//var nonCh chan string
|
||||||
|
var nonces sync.Map
|
||||||
|
|
||||||
|
func issueNonce(w http.ResponseWriter, r *http.Request) {
|
||||||
|
b := make([]byte, 16)
|
||||||
|
_, _ = rand.Read(b)
|
||||||
|
nonce := base64.RawURLEncoding.EncodeToString(b)
|
||||||
|
//nonCh <- nonce
|
||||||
|
nonces.Store(nonce, time.Now())
|
||||||
|
|
||||||
|
w.Header().Set("Replay-Nonce", nonce)
|
||||||
|
}
|
||||||
|
|
||||||
|
func requireNonce(next http.HandlerFunc) http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
nonce := r.Header.Get("Replay-Nonce")
|
||||||
|
// TODO expire nonces every so often
|
||||||
|
//t := nonces[nonce]
|
||||||
|
|
||||||
|
if !useNonce(nonce) {
|
||||||
|
http.Error(
|
||||||
|
w,
|
||||||
|
`{ "error": "invalid or expired nonce", "error_code": "ENONCE" }`,
|
||||||
|
http.StatusBadRequest,
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
issueNonce(w, r)
|
||||||
|
next(w, r)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func checkNonce(nonce string) bool {
|
||||||
|
var t time.Time
|
||||||
|
tmp, ok := nonces.Load(nonce)
|
||||||
|
if ok {
|
||||||
|
t = tmp.(time.Time)
|
||||||
|
}
|
||||||
|
if ok && time.Now().Sub(t) <= 15*time.Minute {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func useNonce(nonce string) bool {
|
||||||
|
if checkNonce(nonce) {
|
||||||
|
//delete(nonces, nonce)
|
||||||
|
nonces.Delete(nonce)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
1160
mockid/route.go
Normal file
1160
mockid/route.go
Normal file
File diff suppressed because it is too large
Load Diff
46
oldxkeypairs/jose.go
Normal file
46
oldxkeypairs/jose.go
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
package xkeypairs
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/base64"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (jws *JWS) DecodeComponents() error {
|
||||||
|
protected, err := base64.RawURLEncoding.DecodeString(jws.Protected)
|
||||||
|
if nil != err {
|
||||||
|
return errors.New("invalid JWS header base64Url encoding")
|
||||||
|
}
|
||||||
|
if err := json.Unmarshal([]byte(protected), &jws.Header); nil != err {
|
||||||
|
return errors.New("invalid JWS header")
|
||||||
|
}
|
||||||
|
|
||||||
|
payload, err := base64.RawURLEncoding.DecodeString(jws.Payload)
|
||||||
|
if nil != err {
|
||||||
|
return errors.New("invalid JWS payload base64Url encoding")
|
||||||
|
}
|
||||||
|
if err := json.Unmarshal([]byte(payload), &jws.Claims); nil != err {
|
||||||
|
return errors.New("invalid JWS claims")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
func Decode(msg string) (*JWS, error) {
|
||||||
|
jws := &JWS{}
|
||||||
|
|
||||||
|
decoder := json.NewDecoder(r.Body)
|
||||||
|
err := decoder.Decode(jws)
|
||||||
|
return jws, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func Unmarshal(msg string) (*JWS, error) {
|
||||||
|
jws := &JWS{}
|
||||||
|
|
||||||
|
if err := json.Unmarshal([]byte(msg), jws); nil != err {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return jws, nil
|
||||||
|
}
|
||||||
|
*/
|
70
oldxkeypairs/jwk.go
Normal file
70
oldxkeypairs/jwk.go
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
package xkeypairs
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/ecdsa"
|
||||||
|
"crypto/rsa"
|
||||||
|
"encoding/base64"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"math/big"
|
||||||
|
|
||||||
|
"git.rootprojects.org/root/keypairs"
|
||||||
|
)
|
||||||
|
|
||||||
|
type JWK interface {
|
||||||
|
marshalJWK() ([]byte, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type ECJWK struct {
|
||||||
|
KeyID string `json:"kid,omitempty"`
|
||||||
|
Curve string `json:"crv"`
|
||||||
|
X string `json:"x"`
|
||||||
|
Y string `json:"y"`
|
||||||
|
Use []string `json:"use,omitempty"`
|
||||||
|
Seed string `json:"_seed,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (k *ECJWK) marshalJWK() ([]byte, error) {
|
||||||
|
return []byte(fmt.Sprintf(`{"crv":%q,"kty":"EC","x":%q,"y":%q}`, k.Curve, k.X, k.Y)), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type RSAJWK struct {
|
||||||
|
KeyID string `json:"kid,omitempty"`
|
||||||
|
Exp string `json:"e"`
|
||||||
|
N string `json"n"`
|
||||||
|
Use []string `json:"use,omitempty"`
|
||||||
|
Seed string `json:"_seed,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (k *RSAJWK) marshalJWK() ([]byte, error) {
|
||||||
|
return []byte(fmt.Sprintf(`{"e":%q,"kty":"RSA","n":%q}`, k.Exp, k.N)), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func ToPublicJWK(pubkey keypairs.PublicKey) JWK {
|
||||||
|
switch k := pubkey.Key().(type) {
|
||||||
|
case *ecdsa.PublicKey:
|
||||||
|
return ECToPublicJWK(k)
|
||||||
|
case *rsa.PublicKey:
|
||||||
|
return RSAToPublicJWK(k)
|
||||||
|
default:
|
||||||
|
panic(errors.New("impossible key type"))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ECToPublicJWK will output the most minimal version of an EC JWK (no key id, no "use" flag, nada)
|
||||||
|
func ECToPublicJWK(k *ecdsa.PublicKey) *ECJWK {
|
||||||
|
return &ECJWK{
|
||||||
|
Curve: k.Curve.Params().Name,
|
||||||
|
X: base64.RawURLEncoding.EncodeToString(k.X.Bytes()),
|
||||||
|
Y: base64.RawURLEncoding.EncodeToString(k.Y.Bytes()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// RSAToPublicJWK will output the most minimal version of an RSA JWK (no key id, no "use" flag, nada)
|
||||||
|
func RSAToPublicJWK(p *rsa.PublicKey) *RSAJWK {
|
||||||
|
return &RSAJWK{
|
||||||
|
Exp: base64.RawURLEncoding.EncodeToString(big.NewInt(int64(p.E)).Bytes()),
|
||||||
|
N: base64.RawURLEncoding.EncodeToString(p.N.Bytes()),
|
||||||
|
}
|
||||||
|
}
|
173
oldxkeypairs/marshal.go
Normal file
173
oldxkeypairs/marshal.go
Normal file
@ -0,0 +1,173 @@
|
|||||||
|
package xkeypairs
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto"
|
||||||
|
"crypto/ecdsa"
|
||||||
|
"crypto/rsa"
|
||||||
|
"crypto/x509"
|
||||||
|
"encoding/base64"
|
||||||
|
"encoding/pem"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"math/big"
|
||||||
|
mathrand "math/rand"
|
||||||
|
|
||||||
|
"git.rootprojects.org/root/keypairs"
|
||||||
|
)
|
||||||
|
|
||||||
|
// MarshalPEMPublicKey outputs the given public key as JWK
|
||||||
|
func MarshalPEMPublicKey(pubkey crypto.PublicKey) ([]byte, error) {
|
||||||
|
block, err := marshalDERPublicKey(pubkey)
|
||||||
|
if nil != err {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return pem.EncodeToMemory(block), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// MarshalDERPublicKey outputs the given public key as JWK
|
||||||
|
func MarshalDERPublicKey(pubkey crypto.PublicKey) ([]byte, error) {
|
||||||
|
block, err := marshalDERPublicKey(pubkey)
|
||||||
|
if nil != err {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return block.Bytes, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// marshalDERPublicKey outputs the given public key as JWK
|
||||||
|
func marshalDERPublicKey(pubkey crypto.PublicKey) (*pem.Block, error) {
|
||||||
|
|
||||||
|
var der []byte
|
||||||
|
var typ string
|
||||||
|
var err error
|
||||||
|
switch k := pubkey.(type) {
|
||||||
|
case *rsa.PublicKey:
|
||||||
|
der = x509.MarshalPKCS1PublicKey(k)
|
||||||
|
typ = "RSA PUBLIC KEY"
|
||||||
|
case *ecdsa.PublicKey:
|
||||||
|
typ = "PUBLIC KEY"
|
||||||
|
der, err = x509.MarshalPKIXPublicKey(k)
|
||||||
|
if nil != err {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
panic("Developer Error: impossible key type")
|
||||||
|
}
|
||||||
|
|
||||||
|
return &pem.Block{
|
||||||
|
Bytes: der,
|
||||||
|
Type: typ,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// MarshalJWKPrivateKey outputs the given private key as JWK
|
||||||
|
func MarshalJWKPrivateKey(privkey keypairs.PrivateKey) []byte {
|
||||||
|
// thumbprint keys are alphabetically sorted and only include the necessary public parts
|
||||||
|
switch k := privkey.(type) {
|
||||||
|
case *rsa.PrivateKey:
|
||||||
|
return MarshalRSAPrivateKey(k)
|
||||||
|
case *ecdsa.PrivateKey:
|
||||||
|
return MarshalECPrivateKey(k)
|
||||||
|
default:
|
||||||
|
// this is unreachable because we know the types that we pass in
|
||||||
|
log.Printf("keytype: %t, %+v\n", privkey, privkey)
|
||||||
|
panic(keypairs.ErrInvalidPublicKey)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MarshalDERPrivateKey outputs the given private key as ASN.1 DER
|
||||||
|
func MarshalDERPrivateKey(privkey keypairs.PrivateKey) ([]byte, error) {
|
||||||
|
// thumbprint keys are alphabetically sorted and only include the necessary public parts
|
||||||
|
switch k := privkey.(type) {
|
||||||
|
case *rsa.PrivateKey:
|
||||||
|
return x509.MarshalPKCS1PrivateKey(k), nil
|
||||||
|
case *ecdsa.PrivateKey:
|
||||||
|
return x509.MarshalECPrivateKey(k)
|
||||||
|
default:
|
||||||
|
// this is unreachable because we know the types that we pass in
|
||||||
|
log.Printf("keytype: %t, %+v\n", privkey, privkey)
|
||||||
|
panic(keypairs.ErrInvalidPublicKey)
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func marshalDERPrivateKey(privkey keypairs.PrivateKey) (*pem.Block, error) {
|
||||||
|
var typ string
|
||||||
|
var bytes []byte
|
||||||
|
var err error
|
||||||
|
|
||||||
|
switch k := privkey.(type) {
|
||||||
|
case *rsa.PrivateKey:
|
||||||
|
if 0 == mathrand.Intn(2) {
|
||||||
|
typ = "PRIVATE KEY"
|
||||||
|
bytes, err = x509.MarshalPKCS8PrivateKey(k)
|
||||||
|
if nil != err {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
typ = "RSA PRIVATE KEY"
|
||||||
|
bytes = x509.MarshalPKCS1PrivateKey(k)
|
||||||
|
}
|
||||||
|
return &pem.Block{
|
||||||
|
Type: typ,
|
||||||
|
Bytes: bytes,
|
||||||
|
}, nil
|
||||||
|
case *ecdsa.PrivateKey:
|
||||||
|
if 0 == mathrand.Intn(2) {
|
||||||
|
typ = "PRIVATE KEY"
|
||||||
|
bytes, err = x509.MarshalPKCS8PrivateKey(k)
|
||||||
|
} else {
|
||||||
|
typ = "EC PRIVATE KEY"
|
||||||
|
bytes, err = x509.MarshalECPrivateKey(k)
|
||||||
|
}
|
||||||
|
if nil != err {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &pem.Block{
|
||||||
|
Type: typ,
|
||||||
|
Bytes: bytes,
|
||||||
|
}, nil
|
||||||
|
default:
|
||||||
|
// this is unreachable because we know the types that we pass in
|
||||||
|
log.Printf("keytype: %t, %+v\n", privkey, privkey)
|
||||||
|
panic(keypairs.ErrInvalidPublicKey)
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MarshalPEMPrivateKey outputs the given private key as ASN.1 PEM
|
||||||
|
func MarshalPEMPrivateKey(privkey keypairs.PrivateKey) ([]byte, error) {
|
||||||
|
block, err := marshalDERPrivateKey(privkey)
|
||||||
|
if nil != err {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return pem.EncodeToMemory(block), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// MarshalECPrivateKey will output the given private key as JWK
|
||||||
|
func MarshalECPrivateKey(k *ecdsa.PrivateKey) []byte {
|
||||||
|
crv := k.Curve.Params().Name
|
||||||
|
d := base64.RawURLEncoding.EncodeToString(k.D.Bytes())
|
||||||
|
x := base64.RawURLEncoding.EncodeToString(k.X.Bytes())
|
||||||
|
y := base64.RawURLEncoding.EncodeToString(k.Y.Bytes())
|
||||||
|
return []byte(fmt.Sprintf(
|
||||||
|
`{"crv":%q,"d":%q,"kty":"EC","x":%q,"y":%q}`,
|
||||||
|
crv, d, x, y,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
// MarshalRSAPrivateKey will output the given private key as JWK
|
||||||
|
func MarshalRSAPrivateKey(pk *rsa.PrivateKey) []byte {
|
||||||
|
e := base64.RawURLEncoding.EncodeToString(big.NewInt(int64(pk.E)).Bytes())
|
||||||
|
n := base64.RawURLEncoding.EncodeToString(pk.N.Bytes())
|
||||||
|
d := base64.RawURLEncoding.EncodeToString(pk.D.Bytes())
|
||||||
|
p := base64.RawURLEncoding.EncodeToString(pk.Primes[0].Bytes())
|
||||||
|
q := base64.RawURLEncoding.EncodeToString(pk.Primes[1].Bytes())
|
||||||
|
dp := base64.RawURLEncoding.EncodeToString(pk.Precomputed.Dp.Bytes())
|
||||||
|
dq := base64.RawURLEncoding.EncodeToString(pk.Precomputed.Dq.Bytes())
|
||||||
|
qi := base64.RawURLEncoding.EncodeToString(pk.Precomputed.Qinv.Bytes())
|
||||||
|
return []byte(fmt.Sprintf(
|
||||||
|
`{"d":%q,"dp":%q,"dq":%q,"e":%q,"kty":"RSA","n":%q,"p":%q,"q":%q,"qi":%q}`,
|
||||||
|
d, dp, dq, e, n, p, q, qi,
|
||||||
|
))
|
||||||
|
}
|
173
oldxkeypairs/sign.go
Normal file
173
oldxkeypairs/sign.go
Normal file
@ -0,0 +1,173 @@
|
|||||||
|
package xkeypairs
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto"
|
||||||
|
"crypto/ecdsa"
|
||||||
|
"crypto/rand"
|
||||||
|
"crypto/rsa"
|
||||||
|
"crypto/sha256"
|
||||||
|
"encoding/base64"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
mathrand "math/rand"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"git.rootprojects.org/root/keypairs"
|
||||||
|
)
|
||||||
|
|
||||||
|
// RandomReader may be overwritten for testing
|
||||||
|
var RandomReader io.Reader = rand.Reader
|
||||||
|
|
||||||
|
//var RandomReader = rand.Reader
|
||||||
|
|
||||||
|
type JWS struct {
|
||||||
|
Header Object `json:"header"` // JSON
|
||||||
|
Claims Object `json:"claims"` // JSON
|
||||||
|
Protected string `json:"protected"` // base64
|
||||||
|
Payload string `json:"payload"` // base64
|
||||||
|
Signature string `json:"signature"` // base64
|
||||||
|
}
|
||||||
|
|
||||||
|
type Object = map[string]interface{}
|
||||||
|
|
||||||
|
// SignClaims adds `typ`, `kid` (or `jwk`), and `alg` in the header and expects claims for `jti`, `exp`, `iss`, and `iat`
|
||||||
|
func SignClaims(privkey keypairs.PrivateKey, header Object, claims Object) (*JWS, error) {
|
||||||
|
var randsrc io.Reader = RandomReader
|
||||||
|
seed, _ := header["_seed"].(int64)
|
||||||
|
if 0 != seed {
|
||||||
|
randsrc = mathrand.New(mathrand.NewSource(seed))
|
||||||
|
//delete(header, "_seed")
|
||||||
|
}
|
||||||
|
|
||||||
|
protected, err := headerToProtected(keypairs.NewPublicKey(privkey.Public()), header)
|
||||||
|
if nil != err {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
protected64 := base64.RawURLEncoding.EncodeToString(protected)
|
||||||
|
|
||||||
|
payload, err := claimsToPayload(claims)
|
||||||
|
if nil != err {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
payload64 := base64.RawURLEncoding.EncodeToString(payload)
|
||||||
|
|
||||||
|
signable := fmt.Sprintf(`%s.%s`, protected64, payload64)
|
||||||
|
hash := sha256.Sum256([]byte(signable))
|
||||||
|
|
||||||
|
sig := Sign(randsrc, privkey, hash[:])
|
||||||
|
sig64 := base64.RawURLEncoding.EncodeToString(sig)
|
||||||
|
//log.Printf("\n(Sign)\nSignable: %s", signable)
|
||||||
|
//log.Printf("Hash: %s", hash)
|
||||||
|
//log.Printf("Sig: %s", sig64)
|
||||||
|
|
||||||
|
return &JWS{
|
||||||
|
Header: header,
|
||||||
|
Claims: claims,
|
||||||
|
Protected: protected64,
|
||||||
|
Payload: payload64,
|
||||||
|
Signature: sig64,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func headerToProtected(pub keypairs.PublicKey, header Object) ([]byte, error) {
|
||||||
|
if nil == header {
|
||||||
|
header = Object{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only supporting 2048-bit and P256 keys right now
|
||||||
|
// because that's all that's practical and well-supported.
|
||||||
|
// No security theatre here.
|
||||||
|
alg := "ES256"
|
||||||
|
switch pub.Key().(type) {
|
||||||
|
case *rsa.PublicKey:
|
||||||
|
alg = "RS256"
|
||||||
|
}
|
||||||
|
|
||||||
|
if selfSign, _ := header["_jwk"].(bool); selfSign {
|
||||||
|
delete(header, "_jwk")
|
||||||
|
any := Object{}
|
||||||
|
_ = json.Unmarshal(keypairs.MarshalJWKPublicKey(pub), &any)
|
||||||
|
header["jwk"] = any
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO what are the acceptable values? JWT. JWS? others?
|
||||||
|
header["typ"] = "JWT"
|
||||||
|
if _, ok := header["jwk"]; !ok {
|
||||||
|
thumbprint := keypairs.ThumbprintPublicKey(pub)
|
||||||
|
kid, _ := header["kid"].(string)
|
||||||
|
if "" != kid && thumbprint != kid {
|
||||||
|
return nil, errors.New("'kid' should be the key's thumbprint")
|
||||||
|
}
|
||||||
|
header["kid"] = thumbprint
|
||||||
|
}
|
||||||
|
header["alg"] = alg
|
||||||
|
|
||||||
|
protected, err := json.Marshal(header)
|
||||||
|
if nil != err {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return protected, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func claimsToPayload(claims Object) ([]byte, error) {
|
||||||
|
if nil == claims {
|
||||||
|
claims = Object{}
|
||||||
|
}
|
||||||
|
|
||||||
|
jti, _ := claims["jti"].(string)
|
||||||
|
exp, _ := claims["exp"].(int64)
|
||||||
|
dur, _ := claims["exp"].(string)
|
||||||
|
insecure, _ := claims["insecure"].(bool)
|
||||||
|
|
||||||
|
// parse if exp is actually a duration, such as "15m"
|
||||||
|
if 0 == exp && "" != dur {
|
||||||
|
s, err := time.ParseDuration(dur)
|
||||||
|
// TODO s, err := time.ParseDuration(dur)
|
||||||
|
if nil != err {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
exp = time.Now().Add(s * time.Second).Unix()
|
||||||
|
claims["exp"] = exp
|
||||||
|
}
|
||||||
|
if "" == jti && 0 == exp && !insecure {
|
||||||
|
return nil, errors.New("token must have jti or exp as to be expirable / cancellable")
|
||||||
|
}
|
||||||
|
|
||||||
|
return json.Marshal(claims)
|
||||||
|
}
|
||||||
|
|
||||||
|
func JWSToJWT(jwt *JWS) string {
|
||||||
|
return fmt.Sprintf(
|
||||||
|
"%s.%s.%s",
|
||||||
|
jwt.Protected,
|
||||||
|
jwt.Payload,
|
||||||
|
jwt.Signature,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func Sign(rand io.Reader, privkey keypairs.PrivateKey, hash []byte) []byte {
|
||||||
|
var sig []byte
|
||||||
|
|
||||||
|
if len(hash) != 32 {
|
||||||
|
panic("only 256-bit hashes for 2048-bit and 256-bit keys are supported")
|
||||||
|
}
|
||||||
|
|
||||||
|
switch k := privkey.(type) {
|
||||||
|
case *rsa.PrivateKey:
|
||||||
|
sig, _ = rsa.SignPKCS1v15(rand, k, crypto.SHA256, hash)
|
||||||
|
case *ecdsa.PrivateKey:
|
||||||
|
r, s, _ := ecdsa.Sign(rand, k, hash[:])
|
||||||
|
rb := r.Bytes()
|
||||||
|
for len(rb) < 32 {
|
||||||
|
rb = append([]byte{0}, rb...)
|
||||||
|
}
|
||||||
|
sb := s.Bytes()
|
||||||
|
for len(rb) < 32 {
|
||||||
|
sb = append([]byte{0}, sb...)
|
||||||
|
}
|
||||||
|
sig = append(rb, sb...)
|
||||||
|
}
|
||||||
|
return sig
|
||||||
|
}
|
172
oldxkeypairs/verify.go
Normal file
172
oldxkeypairs/verify.go
Normal file
@ -0,0 +1,172 @@
|
|||||||
|
package xkeypairs
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto"
|
||||||
|
"crypto/ecdsa"
|
||||||
|
"crypto/elliptic"
|
||||||
|
"crypto/rsa"
|
||||||
|
"crypto/sha256"
|
||||||
|
"encoding/base64"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"log"
|
||||||
|
"math/big"
|
||||||
|
mathrand "math/rand"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"git.rootprojects.org/root/keypairs"
|
||||||
|
)
|
||||||
|
|
||||||
|
func VerifyClaims(pubkey keypairs.PublicKey, jws *JWS) (bool, error) {
|
||||||
|
seed, _ := jws.Header["_seed"].(int64)
|
||||||
|
seedf64, _ := jws.Header["_seed"].(float64)
|
||||||
|
kty, _ := jws.Header["_kty"].(string)
|
||||||
|
kid, _ := jws.Header["kid"].(string)
|
||||||
|
jwkmap, hasJWK := jws.Header["jwk"].(Object)
|
||||||
|
//var jwk JWK = nil
|
||||||
|
|
||||||
|
if 0 == seed {
|
||||||
|
seed = int64(seedf64)
|
||||||
|
}
|
||||||
|
|
||||||
|
var pub keypairs.PublicKey = nil
|
||||||
|
if hasJWK {
|
||||||
|
log.Println("Security TODO: did not check jws.Claims[\"sub\"] against 'jwk' thumbprint")
|
||||||
|
log.Println("Security TODO: did not check jws.Claims[\"iss\"]")
|
||||||
|
kty := jwkmap["kty"]
|
||||||
|
var err error
|
||||||
|
if "RSA" == kty {
|
||||||
|
e, _ := jwkmap["e"].(string)
|
||||||
|
n, _ := jwkmap["n"].(string)
|
||||||
|
k, _ := (&RSAJWK{
|
||||||
|
Exp: e,
|
||||||
|
N: n,
|
||||||
|
}).marshalJWK()
|
||||||
|
pub, err = keypairs.ParseJWKPublicKey(k)
|
||||||
|
if nil != err {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
crv, _ := jwkmap["crv"].(string)
|
||||||
|
x, _ := jwkmap["x"].(string)
|
||||||
|
y, _ := jwkmap["y"].(string)
|
||||||
|
k, _ := (&ECJWK{
|
||||||
|
Curve: crv,
|
||||||
|
X: x,
|
||||||
|
Y: y,
|
||||||
|
}).marshalJWK()
|
||||||
|
pub, err = keypairs.ParseJWKPublicKey(k)
|
||||||
|
if nil != err {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if "" == kid {
|
||||||
|
return false, errors.New("token should have 'kid' or 'jwk' in header")
|
||||||
|
}
|
||||||
|
if nil == pubkey {
|
||||||
|
if 0 == seed {
|
||||||
|
return false, errors.New("the debug API requires '_seed' to accompany 'kid'")
|
||||||
|
}
|
||||||
|
if "" == kty {
|
||||||
|
return false, errors.New("the debug API requires '_kty' to accompany '_seed'")
|
||||||
|
}
|
||||||
|
privkey := genPrivKey(seed, kty)
|
||||||
|
pub = keypairs.NewPublicKey(privkey.Public())
|
||||||
|
} else {
|
||||||
|
pub = pubkey
|
||||||
|
}
|
||||||
|
log.Println("Security TODO: did not check jws.Claims[\"kid\"] against thumbprint")
|
||||||
|
}
|
||||||
|
|
||||||
|
jti, _ := jws.Claims["jti"].(string)
|
||||||
|
expf64, _ := jws.Claims["exp"].(float64)
|
||||||
|
exp := int64(expf64)
|
||||||
|
if 0 == exp {
|
||||||
|
if "" == jti {
|
||||||
|
return false, errors.New("one of 'jti' or 'exp' must exist for token expiry")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if time.Now().Unix() > exp {
|
||||||
|
return false, fmt.Errorf("token expired at %d (%s)", exp, time.Unix(exp, 0))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
signable := fmt.Sprintf("%s.%s", jws.Protected, jws.Payload)
|
||||||
|
hash := sha256.Sum256([]byte(signable))
|
||||||
|
sig, err := base64.RawURLEncoding.DecodeString(jws.Signature)
|
||||||
|
if nil != err {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
//log.Printf("\n(Verify)\nSignable: %s", signable)
|
||||||
|
//log.Printf("Hash: %s", hash)
|
||||||
|
//log.Printf("Sig: %s", jws.Signature)
|
||||||
|
|
||||||
|
return Verify(pub, hash[:], sig), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func Verify(pubkey keypairs.PublicKey, hash []byte, sig []byte) bool {
|
||||||
|
|
||||||
|
switch pub := pubkey.Key().(type) {
|
||||||
|
case *rsa.PublicKey:
|
||||||
|
//log.Printf("RSA VERIFY")
|
||||||
|
// TODO keypairs.Size(key) to detect key size ?
|
||||||
|
//alg := "SHA256"
|
||||||
|
// TODO: this hasn't been tested yet
|
||||||
|
if err := rsa.VerifyPKCS1v15(pub, crypto.SHA256, hash, sig); nil != err {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
case *ecdsa.PublicKey:
|
||||||
|
r := &big.Int{}
|
||||||
|
r.SetBytes(sig[0:32])
|
||||||
|
s := &big.Int{}
|
||||||
|
s.SetBytes(sig[32:])
|
||||||
|
return ecdsa.Verify(pub, hash, r, s)
|
||||||
|
default:
|
||||||
|
panic("impossible condition: non-rsa/non-ecdsa key")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const maxRetry = 16
|
||||||
|
|
||||||
|
func genPrivKey(seed int64, kty string) keypairs.PrivateKey {
|
||||||
|
var privkey keypairs.PrivateKey
|
||||||
|
|
||||||
|
if "RSA" == kty {
|
||||||
|
keylen := 2048
|
||||||
|
privkey, _ = rsa.GenerateKey(nextReader(seed), keylen)
|
||||||
|
if 0 != seed {
|
||||||
|
for i := 0; i < maxRetry; i++ {
|
||||||
|
otherkey, _ := rsa.GenerateKey(nextReader(seed), keylen)
|
||||||
|
otherCmp := otherkey.D.Cmp(privkey.(*rsa.PrivateKey).D)
|
||||||
|
if 0 != otherCmp {
|
||||||
|
// There are two possible keys, choose the lesser D value
|
||||||
|
// See https://github.com/square/go-jose/issues/189
|
||||||
|
if otherCmp < 0 {
|
||||||
|
privkey = otherkey
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if maxRetry == i-1 {
|
||||||
|
log.Printf("error: coinflip landed on heads %d times", maxRetry)
|
||||||
|
// TODO return random / retry error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// TODO: EC keys may also suffer the same random problems in the future
|
||||||
|
privkey, _ = ecdsa.GenerateKey(elliptic.P256(), nextReader(seed))
|
||||||
|
}
|
||||||
|
return privkey
|
||||||
|
}
|
||||||
|
|
||||||
|
// this shananigans is only for testing and debug API stuff
|
||||||
|
func nextReader(seed int64) io.Reader {
|
||||||
|
if 0 == seed {
|
||||||
|
return RandomReader
|
||||||
|
}
|
||||||
|
return mathrand.New(mathrand.NewSource(seed))
|
||||||
|
}
|
1
public/.gitignore
vendored
Normal file
1
public/.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
package-lock.json
|
16
public/.jshintrc
Normal file
16
public/.jshintrc
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
{ "node": true
|
||||||
|
, "browser": true
|
||||||
|
, "globals": { "Promise": true }
|
||||||
|
, "esversion": 8
|
||||||
|
|
||||||
|
, "indent": 2
|
||||||
|
, "onevar": true
|
||||||
|
, "laxbreak": true
|
||||||
|
, "curly": true
|
||||||
|
, "nonbsp": true
|
||||||
|
|
||||||
|
, "eqeqeq": true
|
||||||
|
, "immed": true
|
||||||
|
, "undef": true
|
||||||
|
, "unused": true
|
||||||
|
}
|
1
public/.prettierignore
Normal file
1
public/.prettierignore
Normal file
@ -0,0 +1 @@
|
|||||||
|
dist/
|
8
public/.prettierrc
Normal file
8
public/.prettierrc
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"bracketSpacing": true,
|
||||||
|
"printWidth": 80,
|
||||||
|
"singleQuote": true,
|
||||||
|
"tabWidth": 4,
|
||||||
|
"trailingComma": "none",
|
||||||
|
"useTabs": true
|
||||||
|
}
|
@ -1,60 +0,0 @@
|
|||||||
'use strict';
|
|
||||||
|
|
||||||
// https://developers.google.com/sheets/api/guides/authorizing
|
|
||||||
// https://www.googleapis.com/auth/spreadsheets.readonly
|
|
||||||
|
|
||||||
// Scope names
|
|
||||||
// https://developers.google.com/drive/api/v3/about-auth
|
|
||||||
|
|
||||||
// Google Sheets API
|
|
||||||
// https://developers.google.com/sheets/api/guides/migration#list_spreadsheets_for_the_authenticated_user
|
|
||||||
|
|
||||||
function onSignIn(googleUser) {
|
|
||||||
var profile = googleUser.getBasicProfile();
|
|
||||||
|
|
||||||
var id_token = googleUser.getAuthResponse().id_token;
|
|
||||||
var access_token = googleUser.getAuthResponse();
|
|
||||||
console.log('ID: ' + profile.getId()); // Do not send to your backend! Use an ID token instead.
|
|
||||||
console.log('Name: ' + profile.getName());
|
|
||||||
console.log('Image URL: ' + profile.getImageUrl());
|
|
||||||
console.log('Email: ' + profile.getEmail()); // This is null if the 'email' scope is not present.
|
|
||||||
console.log('access_token', access_token);
|
|
||||||
console.log('id_token', id_token);
|
|
||||||
window
|
|
||||||
.fetch('https://oauth2.googleapis.com/tokeninfo?id_token=' + id_token)
|
|
||||||
.then(function(resp) {
|
|
||||||
return resp.json().then(function(data) {
|
|
||||||
console.log(data);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
/*
|
|
||||||
window
|
|
||||||
.fetch(
|
|
||||||
"https://www.googleapis.com/drive/v3/files?q=mimeType%3D'application%2Fvnd.google-apps.spreadsheet'",
|
|
||||||
{
|
|
||||||
headers: {Authorization: 'Bearer ' + access_token.access_token},
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.then(function(resp) {
|
|
||||||
return resp.json().then(function(data) {
|
|
||||||
console.log(data);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
window
|
|
||||||
.fetch('https://spreadsheets.google.com/feeds/spreadsheets/private/full', {
|
|
||||||
headers: {Authorization: 'Bearer ' + access_token.access_token},
|
|
||||||
})
|
|
||||||
.then(function(resp) {
|
|
||||||
return resp.json().then(function(data) {
|
|
||||||
console.log(data);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
*/
|
|
||||||
}
|
|
||||||
|
|
||||||
function signOut() {
|
|
||||||
var auth2 = gapi.auth2.getAuthInstance();
|
|
||||||
auth2.signOut().then(function() {
|
|
||||||
console.log('User signed out.');
|
|
||||||
});
|
|
||||||
}
|
|
@ -1,12 +1,45 @@
|
|||||||
<meta
|
<html>
|
||||||
name="google-signin-client_id"
|
<head>
|
||||||
content="128764648444-nk2ss16gmals7rhsk2kj0i0ove0v0tnk.apps.googleusercontent.com"
|
<meta name="google-signin-scope" content="email" />
|
||||||
/>
|
<meta
|
||||||
<!-- https://developers.google.com/identity/sign-in/web/sign-in -->
|
name="google-signin-client_id"
|
||||||
<!-- https://developers.google.com/identity/sign-in/web/quick-migration-guide -->
|
content="291138637698-9hjbgadgkibuv9j26104aj0bg5bia30j.apps.googleusercontent.com"
|
||||||
<!-- Note: You can also specify your app's client ID with the client_id parameter of the gapi.auth2.init() method. -->
|
/>
|
||||||
|
|
||||||
<pre><code>
|
<style>
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
body {
|
||||||
|
background-color: #222;
|
||||||
|
color: #aaa;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
body {
|
||||||
|
background-color: #222;
|
||||||
|
color: #aaa;
|
||||||
|
}
|
||||||
|
.container {
|
||||||
|
padding: 2em;
|
||||||
|
}
|
||||||
|
.authn-container {
|
||||||
|
width: 300px;
|
||||||
|
margin: auto;
|
||||||
|
border: solid 1px #c0c0c0;
|
||||||
|
}
|
||||||
|
.authn-container hr {
|
||||||
|
width: 80%;
|
||||||
|
}
|
||||||
|
.link {
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
padding: 0;
|
||||||
|
text-decoration: underline;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<pre><code>
|
||||||
<h1>Tokens for Testing</h1>
|
<h1>Tokens for Testing</h1>
|
||||||
Compatible with
|
Compatible with
|
||||||
|
|
||||||
@ -21,6 +54,7 @@ Compatible with
|
|||||||
|
|
||||||
* https://mock.pocketid.app/access_token
|
* https://mock.pocketid.app/access_token
|
||||||
* https://mock.pocketid.app/authorization_header
|
* https://mock.pocketid.app/authorization_header
|
||||||
|
* https://mock.pocketid.app/inspect_token
|
||||||
* https://xxx.mock.pocketid.app/.well-known/openid-configuration
|
* https://xxx.mock.pocketid.app/.well-known/openid-configuration
|
||||||
* https://xxx.mock.pocketid.app/.well-known/jwks.json
|
* https://xxx.mock.pocketid.app/.well-known/jwks.json
|
||||||
* https://mock.pocketid.app/key.jwk.json
|
* https://mock.pocketid.app/key.jwk.json
|
||||||
@ -56,6 +90,13 @@ For example:
|
|||||||
HEADER=$(curl -fL https://mock.pocketid.app/authorization_header)
|
HEADER=$(curl -fL https://mock.pocketid.app/authorization_header)
|
||||||
# Authorization: Bearer <token>
|
# Authorization: Bearer <token>
|
||||||
|
|
||||||
|
<h3>Inspecting the Token</h3>
|
||||||
|
|
||||||
|
You can see its decoded form at the `inspect_token` endpoint:
|
||||||
|
|
||||||
|
curl -fL https://mock.pocketid.app/inspect_token \
|
||||||
|
-H "$(curl -fL https://mock.pocketid.app/authorization_header)"
|
||||||
|
|
||||||
<h3>The Token, Decoded</h3>
|
<h3>The Token, Decoded</h3>
|
||||||
|
|
||||||
The Token will look like this:
|
The Token will look like this:
|
||||||
@ -113,12 +154,110 @@ You shouldn't use it for automated testing, because it will change, but it looks
|
|||||||
}
|
}
|
||||||
|
|
||||||
</code></pre>
|
</code></pre>
|
||||||
|
</div>
|
||||||
|
|
||||||
<script src="https://apis.google.com/js/platform.js" async defer></script>
|
<div class="authn-flow">
|
||||||
<div
|
<div class="authn-container authn-loading">
|
||||||
class="g-signin2"
|
<center>⌛</center>
|
||||||
data-onsuccess="onSignIn"
|
</div>
|
||||||
data-scope="profile email https://www.googleapis.com/auth/spreadsheets.readonly https://www.googleapis.com/auth/drive.readonly"
|
|
||||||
></div>
|
<div class="authn-container authn-email">
|
||||||
<a href="#" onclick="signOut();">Sign out</a>
|
<center>
|
||||||
<script src="app.js"></script>
|
<form class="authn-form">
|
||||||
|
<h2>Login</h2>
|
||||||
|
<input
|
||||||
|
name="username"
|
||||||
|
type="email"
|
||||||
|
placeholder="email"
|
||||||
|
value="coolaj86+noreply@gmail.com"
|
||||||
|
/>
|
||||||
|
<br />
|
||||||
|
<button type="submit">Continue</button>
|
||||||
|
</form>
|
||||||
|
<hr />
|
||||||
|
<div
|
||||||
|
class="g-signin2"
|
||||||
|
data-scope="email"
|
||||||
|
data-onsuccess="onSignIn"
|
||||||
|
data-theme="dark"
|
||||||
|
></div>
|
||||||
|
<br />
|
||||||
|
</center>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="authn-container authn-new-user">
|
||||||
|
<center>
|
||||||
|
<h2>Choose Password</h2>
|
||||||
|
<form class="authn-form">
|
||||||
|
<input
|
||||||
|
name="password"
|
||||||
|
type="password"
|
||||||
|
placeholder="password"
|
||||||
|
value="secret"
|
||||||
|
/><button type="button">Show</button>
|
||||||
|
<br />
|
||||||
|
<button type="submit">Create Account</button>
|
||||||
|
<br />
|
||||||
|
Already have an account?
|
||||||
|
<button class="link" type="button">
|
||||||
|
link existing account
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</center>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="authn-container authn-existing">
|
||||||
|
<!-- skip this for google auth -->
|
||||||
|
<center>
|
||||||
|
<h2>Existing User</h2>
|
||||||
|
<form class="authn-form">
|
||||||
|
<input
|
||||||
|
name="password"
|
||||||
|
type="password"
|
||||||
|
placeholder="password"
|
||||||
|
/>
|
||||||
|
<br />
|
||||||
|
<button type="submit">Continue</button>
|
||||||
|
<br />
|
||||||
|
<button class="link" type="button">
|
||||||
|
forgot password
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</center>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="authn-container authn-failed">
|
||||||
|
<center>
|
||||||
|
<h2>Incorrect Password</h2>
|
||||||
|
<form class="authn-form">
|
||||||
|
<input
|
||||||
|
name="password"
|
||||||
|
type="password"
|
||||||
|
placeholder="password"
|
||||||
|
/>
|
||||||
|
<br />
|
||||||
|
<button type="submit">Continue</button>
|
||||||
|
<br />
|
||||||
|
<button class="link" type="button">
|
||||||
|
forgot password
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</center>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="authn-container authn-new-device">
|
||||||
|
<center>
|
||||||
|
<h2>New Device</h2>
|
||||||
|
<p>Check your email to confirm new device.</p>
|
||||||
|
</center>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script src="./dist/main.js"></script>
|
||||||
|
<script
|
||||||
|
src="https://apis.google.com/js/platform.js"
|
||||||
|
async
|
||||||
|
defer
|
||||||
|
></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
3
public/main.js
Normal file
3
public/main.js
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
require('./pocket/consumer.js');
|
34
public/package.json
Normal file
34
public/package.json
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
{
|
||||||
|
"name": "pocketid",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"description": "ID tokens made easy",
|
||||||
|
"main": "pocketid.js",
|
||||||
|
"scripts": {
|
||||||
|
"prettier": "prettier --write '**/*.{css,js,md}'",
|
||||||
|
"test": "node pocketid_test.js"
|
||||||
|
},
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://example.com/pocketid.git"
|
||||||
|
},
|
||||||
|
"keywords": [
|
||||||
|
"oauth1",
|
||||||
|
"oauth2",
|
||||||
|
"oauth3",
|
||||||
|
"oidc",
|
||||||
|
"acme",
|
||||||
|
"jwt",
|
||||||
|
"jose",
|
||||||
|
"jws",
|
||||||
|
"jwk"
|
||||||
|
],
|
||||||
|
"author": "AJ ONeal <coolaj86@gmail.com>",
|
||||||
|
"license": "MPL-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"@root/keypairs": "^0.10.1"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"webpack": "^5.0.0-beta.28",
|
||||||
|
"webpack-cli": "^3.3.12"
|
||||||
|
}
|
||||||
|
}
|
19
public/webpack.config.js
Normal file
19
public/webpack.config.js
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
var path = require('path');
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
entry: './main.js',
|
||||||
|
mode: 'development',
|
||||||
|
devServer: {
|
||||||
|
contentBase: path.join(__dirname, 'dist'),
|
||||||
|
port: 3001
|
||||||
|
},
|
||||||
|
output: {
|
||||||
|
publicPath: 'http://localhost:3001/'
|
||||||
|
},
|
||||||
|
module: {
|
||||||
|
rules: [{}]
|
||||||
|
},
|
||||||
|
plugins: []
|
||||||
|
};
|
19
vendor/git.rootprojects.org/root/hashcash/.gitignore
generated
vendored
Normal file
19
vendor/git.rootprojects.org/root/hashcash/.gitignore
generated
vendored
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
cmd/hashcash/hashcash
|
||||||
|
|
||||||
|
# ---> Go
|
||||||
|
# Binaries for programs and plugins
|
||||||
|
*.exe
|
||||||
|
*.exe~
|
||||||
|
*.dll
|
||||||
|
*.so
|
||||||
|
*.dylib
|
||||||
|
|
||||||
|
# Test binary, built with `go test -c`
|
||||||
|
*.test
|
||||||
|
|
||||||
|
# Output of the go coverage tool, specifically when used with LiteIDE
|
||||||
|
*.out
|
||||||
|
|
||||||
|
# Dependency directories (remove the comment below to include it)
|
||||||
|
# vendor/
|
||||||
|
|
312
vendor/git.rootprojects.org/root/hashcash/LICENSE
generated
vendored
Normal file
312
vendor/git.rootprojects.org/root/hashcash/LICENSE
generated
vendored
Normal file
@ -0,0 +1,312 @@
|
|||||||
|
Mozilla Public License Version 2.0
|
||||||
|
|
||||||
|
1. Definitions
|
||||||
|
|
||||||
|
1.1. "Contributor" means each individual or legal entity that creates, contributes
|
||||||
|
to the creation of, or owns Covered Software.
|
||||||
|
|
||||||
|
1.2. "Contributor Version" means the combination of the Contributions of others
|
||||||
|
(if any) used by a Contributor and that particular Contributor's Contribution.
|
||||||
|
|
||||||
|
1.3. "Contribution" means Covered Software of a particular Contributor.
|
||||||
|
|
||||||
|
1.4. "Covered Software" means Source Code Form to which the initial Contributor
|
||||||
|
has attached the notice in Exhibit A, the Executable Form of such Source Code
|
||||||
|
Form, and Modifications of such Source Code Form, in each case including portions
|
||||||
|
thereof.
|
||||||
|
|
||||||
|
1.5. "Incompatible With Secondary Licenses" means
|
||||||
|
|
||||||
|
(a) that the initial Contributor has attached the notice described in Exhibit
|
||||||
|
B to the Covered Software; or
|
||||||
|
|
||||||
|
(b) that the Covered Software was made available under the terms of version
|
||||||
|
1.1 or earlier of the License, but not also under the terms of a Secondary
|
||||||
|
License.
|
||||||
|
|
||||||
|
1.6. "Executable Form" means any form of the work other than Source Code Form.
|
||||||
|
|
||||||
|
1.7. "Larger Work" means a work that combines Covered Software with other
|
||||||
|
material, in a separate file or files, that is not Covered Software.
|
||||||
|
|
||||||
|
1.8. "License" means this document.
|
||||||
|
|
||||||
|
1.9. "Licensable" means having the right to grant, to the maximum extent possible,
|
||||||
|
whether at the time of the initial grant or subsequently, any and all of the
|
||||||
|
rights conveyed by this License.
|
||||||
|
|
||||||
|
1.10. "Modifications" means any of the following:
|
||||||
|
|
||||||
|
(a) any file in Source Code Form that results from an addition to, deletion
|
||||||
|
from, or modification of the contents of Covered Software; or
|
||||||
|
|
||||||
|
(b) any new file in Source Code Form that contains any Covered Software.
|
||||||
|
|
||||||
|
1.11. "Patent Claims" of a Contributor means any patent claim(s), including
|
||||||
|
without limitation, method, process, and apparatus claims, in any patent Licensable
|
||||||
|
by such Contributor that would be infringed, but for the grant of the License,
|
||||||
|
by the making, using, selling, offering for sale, having made, import, or
|
||||||
|
transfer of either its Contributions or its Contributor Version.
|
||||||
|
|
||||||
|
1.12. "Secondary License" means either the GNU General Public License, Version
|
||||||
|
2.0, the GNU Lesser General Public License, Version 2.1, the GNU Affero General
|
||||||
|
Public License, Version 3.0, or any later versions of those licenses.
|
||||||
|
|
||||||
|
1.13. "Source Code Form" means the form of the work preferred for making modifications.
|
||||||
|
|
||||||
|
1.14. "You" (or "Your") means an individual or a legal entity exercising rights
|
||||||
|
under this License. For legal entities, "You" includes any entity that controls,
|
||||||
|
is controlled by, or is under common control with You. For purposes of this
|
||||||
|
definition, "control" means (a) the power, direct or indirect, to cause the
|
||||||
|
direction or management of such entity, whether by contract or otherwise,
|
||||||
|
or (b) ownership of more than fifty percent (50%) of the outstanding shares
|
||||||
|
or beneficial ownership of such entity.
|
||||||
|
|
||||||
|
2. License Grants and Conditions
|
||||||
|
|
||||||
|
2.1. Grants
|
||||||
|
|
||||||
|
Each Contributor hereby grants You a world-wide, royalty-free, non-exclusive
|
||||||
|
license:
|
||||||
|
|
||||||
|
(a) under intellectual property rights (other than patent or trademark) Licensable
|
||||||
|
by such Contributor to use, reproduce, make available, modify, display, perform,
|
||||||
|
distribute, and otherwise exploit its Contributions, either on an unmodified
|
||||||
|
basis, with Modifications, or as part of a Larger Work; and
|
||||||
|
|
||||||
|
(b) under Patent Claims of such Contributor to make, use, sell, offer for
|
||||||
|
sale, have made, import, and otherwise transfer either its Contributions or
|
||||||
|
its Contributor Version.
|
||||||
|
|
||||||
|
2.2. Effective Date
|
||||||
|
|
||||||
|
The licenses granted in Section 2.1 with respect to any Contribution become
|
||||||
|
effective for each Contribution on the date the Contributor first distributes
|
||||||
|
such Contribution.
|
||||||
|
|
||||||
|
2.3. Limitations on Grant Scope
|
||||||
|
|
||||||
|
The licenses granted in this Section 2 are the only rights granted under this
|
||||||
|
License. No additional rights or licenses will be implied from the distribution
|
||||||
|
or licensing of Covered Software under this License. Notwithstanding Section
|
||||||
|
2.1(b) above, no patent license is granted by a Contributor:
|
||||||
|
|
||||||
|
(a) for any code that a Contributor has removed from Covered Software; or
|
||||||
|
|
||||||
|
(b) for infringements caused by: (i) Your and any other third party's modifications
|
||||||
|
of Covered Software, or (ii) the combination of its Contributions with other
|
||||||
|
software (except as part of its Contributor Version); or
|
||||||
|
|
||||||
|
(c) under Patent Claims infringed by Covered Software in the absence of its
|
||||||
|
Contributions.
|
||||||
|
|
||||||
|
This License does not grant any rights in the trademarks, service marks, or
|
||||||
|
logos of any Contributor (except as may be necessary to comply with the notice
|
||||||
|
requirements in Section 3.4).
|
||||||
|
|
||||||
|
2.4. Subsequent Licenses
|
||||||
|
|
||||||
|
No Contributor makes additional grants as a result of Your choice to distribute
|
||||||
|
the Covered Software under a subsequent version of this License (see Section
|
||||||
|
10.2) or under the terms of a Secondary License (if permitted under the terms
|
||||||
|
of Section 3.3).
|
||||||
|
|
||||||
|
2.5. Representation
|
||||||
|
|
||||||
|
Each Contributor represents that the Contributor believes its Contributions
|
||||||
|
are its original creation(s) or it has sufficient rights to grant the rights
|
||||||
|
to its Contributions conveyed by this License.
|
||||||
|
|
||||||
|
2.6. Fair Use
|
||||||
|
|
||||||
|
This License is not intended to limit any rights You have under applicable
|
||||||
|
copyright doctrines of fair use, fair dealing, or other equivalents.
|
||||||
|
|
||||||
|
2.7. Conditions
|
||||||
|
|
||||||
|
Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted in
|
||||||
|
Section 2.1.
|
||||||
|
|
||||||
|
3. Responsibilities
|
||||||
|
|
||||||
|
3.1. Distribution of Source Form
|
||||||
|
|
||||||
|
All distribution of Covered Software in Source Code Form, including any Modifications
|
||||||
|
that You create or to which You contribute, must be under the terms of this
|
||||||
|
License. You must inform recipients that the Source Code Form of the Covered
|
||||||
|
Software is governed by the terms of this License, and how they can obtain
|
||||||
|
a copy of this License. You may not attempt to alter or restrict the recipients'
|
||||||
|
rights in the Source Code Form.
|
||||||
|
|
||||||
|
3.2. Distribution of Executable Form
|
||||||
|
|
||||||
|
If You distribute Covered Software in Executable Form then:
|
||||||
|
|
||||||
|
(a) such Covered Software must also be made available in Source Code Form,
|
||||||
|
as described in Section 3.1, and You must inform recipients of the Executable
|
||||||
|
Form how they can obtain a copy of such Source Code Form by reasonable means
|
||||||
|
in a timely manner, at a charge no more than the cost of distribution to the
|
||||||
|
recipient; and
|
||||||
|
|
||||||
|
(b) You may distribute such Executable Form under the terms of this License,
|
||||||
|
or sublicense it under different terms, provided that the license for the
|
||||||
|
Executable Form does not attempt to limit or alter the recipients' rights
|
||||||
|
in the Source Code Form under this License.
|
||||||
|
|
||||||
|
3.3. Distribution of a Larger Work
|
||||||
|
|
||||||
|
You may create and distribute a Larger Work under terms of Your choice, provided
|
||||||
|
that You also comply with the requirements of this License for the Covered
|
||||||
|
Software. If the Larger Work is a combination of Covered Software with a work
|
||||||
|
governed by one or more Secondary Licenses, and the Covered Software is not
|
||||||
|
Incompatible With Secondary Licenses, this License permits You to additionally
|
||||||
|
distribute such Covered Software under the terms of such Secondary License(s),
|
||||||
|
so that the recipient of the Larger Work may, at their option, further distribute
|
||||||
|
the Covered Software under the terms of either this License or such Secondary
|
||||||
|
License(s).
|
||||||
|
|
||||||
|
3.4. Notices
|
||||||
|
|
||||||
|
You may not remove or alter the substance of any license notices (including
|
||||||
|
copyright notices, patent notices, disclaimers of warranty, or limitations
|
||||||
|
of liability) contained within the Source Code Form of the Covered Software,
|
||||||
|
except that You may alter any license notices to the extent required to remedy
|
||||||
|
known factual inaccuracies.
|
||||||
|
|
||||||
|
3.5. Application of Additional Terms
|
||||||
|
|
||||||
|
You may choose to offer, and to charge a fee for, warranty, support, indemnity
|
||||||
|
or liability obligations to one or more recipients of Covered Software. However,
|
||||||
|
You may do so only on Your own behalf, and not on behalf of any Contributor.
|
||||||
|
You must make it absolutely clear that any such warranty, support, indemnity,
|
||||||
|
or liability obligation is offered by You alone, and You hereby agree to indemnify
|
||||||
|
every Contributor for any liability incurred by such Contributor as a result
|
||||||
|
of warranty, support, indemnity or liability terms You offer. You may include
|
||||||
|
additional disclaimers of warranty and limitations of liability specific to
|
||||||
|
any jurisdiction.
|
||||||
|
|
||||||
|
4. Inability to Comply Due to Statute or Regulation
|
||||||
|
|
||||||
|
If it is impossible for You to comply with any of the terms of this License
|
||||||
|
with respect to some or all of the Covered Software due to statute, judicial
|
||||||
|
order, or regulation then You must: (a) comply with the terms of this License
|
||||||
|
to the maximum extent possible; and (b) describe the limitations and the code
|
||||||
|
they affect. Such description must be placed in a text file included with
|
||||||
|
all distributions of the Covered Software under this License. Except to the
|
||||||
|
extent prohibited by statute or regulation, such description must be sufficiently
|
||||||
|
detailed for a recipient of ordinary skill to be able to understand it.
|
||||||
|
|
||||||
|
5. Termination
|
||||||
|
|
||||||
|
5.1. The rights granted under this License will terminate automatically if
|
||||||
|
You fail to comply with any of its terms. However, if You become compliant,
|
||||||
|
then the rights granted under this License from a particular Contributor are
|
||||||
|
reinstated (a) provisionally, unless and until such Contributor explicitly
|
||||||
|
and finally terminates Your grants, and (b) on an ongoing basis, if such Contributor
|
||||||
|
fails to notify You of the non-compliance by some reasonable means prior to
|
||||||
|
60 days after You have come back into compliance. Moreover, Your grants from
|
||||||
|
a particular Contributor are reinstated on an ongoing basis if such Contributor
|
||||||
|
notifies You of the non-compliance by some reasonable means, this is the first
|
||||||
|
time You have received notice of non-compliance with this License from such
|
||||||
|
Contributor, and You become compliant prior to 30 days after Your receipt
|
||||||
|
of the notice.
|
||||||
|
|
||||||
|
5.2. If You initiate litigation against any entity by asserting a patent infringement
|
||||||
|
claim (excluding declaratory judgment actions, counter-claims, and cross-claims)
|
||||||
|
alleging that a Contributor Version directly or indirectly infringes any patent,
|
||||||
|
then the rights granted to You by any and all Contributors for the Covered
|
||||||
|
Software under Section 2.1 of this License shall terminate.
|
||||||
|
|
||||||
|
5.3. In the event of termination under Sections 5.1 or 5.2 above, all end
|
||||||
|
user license agreements (excluding distributors and resellers) which have
|
||||||
|
been validly granted by You or Your distributors under this License prior
|
||||||
|
to termination shall survive termination.
|
||||||
|
|
||||||
|
6. Disclaimer of Warranty
|
||||||
|
|
||||||
|
Covered Software is provided under this License on an "as is" basis, without
|
||||||
|
warranty of any kind, either expressed, implied, or statutory, including,
|
||||||
|
without limitation, warranties that the Covered Software is free of defects,
|
||||||
|
merchantable, fit for a particular purpose or non-infringing. The entire risk
|
||||||
|
as to the quality and performance of the Covered Software is with You. Should
|
||||||
|
any Covered Software prove defective in any respect, You (not any Contributor)
|
||||||
|
assume the cost of any necessary servicing, repair, or correction. This disclaimer
|
||||||
|
of warranty constitutes an essential part of this License. No use of any Covered
|
||||||
|
Software is authorized under this License except under this disclaimer.
|
||||||
|
|
||||||
|
7. Limitation of Liability
|
||||||
|
|
||||||
|
Under no circumstances and under no legal theory, whether tort (including
|
||||||
|
negligence), contract, or otherwise, shall any Contributor, or anyone who
|
||||||
|
distributes Covered Software as permitted above, be liable to You for any
|
||||||
|
direct, indirect, special, incidental, or consequential damages of any character
|
||||||
|
including, without limitation, damages for lost profits, loss of goodwill,
|
||||||
|
work stoppage, computer failure or malfunction, or any and all other commercial
|
||||||
|
damages or losses, even if such party shall have been informed of the possibility
|
||||||
|
of such damages. This limitation of liability shall not apply to liability
|
||||||
|
for death or personal injury resulting from such party's negligence to the
|
||||||
|
extent applicable law prohibits such limitation. Some jurisdictions do not
|
||||||
|
allow the exclusion or limitation of incidental or consequential damages,
|
||||||
|
so this exclusion and limitation may not apply to You.
|
||||||
|
|
||||||
|
8. Litigation
|
||||||
|
|
||||||
|
Any litigation relating to this License may be brought only in the courts
|
||||||
|
of a jurisdiction where the defendant maintains its principal place of business
|
||||||
|
and such litigation shall be governed by laws of that jurisdiction, without
|
||||||
|
reference to its conflict-of-law provisions. Nothing in this Section shall
|
||||||
|
prevent a party's ability to bring cross-claims or counter-claims.
|
||||||
|
|
||||||
|
9. Miscellaneous
|
||||||
|
|
||||||
|
This License represents the complete agreement concerning the subject matter
|
||||||
|
hereof. If any provision of this License is held to be unenforceable, such
|
||||||
|
provision shall be reformed only to the extent necessary to make it enforceable.
|
||||||
|
Any law or regulation which provides that the language of a contract shall
|
||||||
|
be construed against the drafter shall not be used to construe this License
|
||||||
|
against a Contributor.
|
||||||
|
|
||||||
|
10. Versions of the License
|
||||||
|
|
||||||
|
10.1. New Versions
|
||||||
|
|
||||||
|
Mozilla Foundation is the license steward. Except as provided in Section 10.3,
|
||||||
|
no one other than the license steward has the right to modify or publish new
|
||||||
|
versions of this License. Each version will be given a distinguishing version
|
||||||
|
number.
|
||||||
|
|
||||||
|
10.2. Effect of New Versions
|
||||||
|
|
||||||
|
You may distribute the Covered Software under the terms of the version of
|
||||||
|
the License under which You originally received the Covered Software, or under
|
||||||
|
the terms of any subsequent version published by the license steward.
|
||||||
|
|
||||||
|
10.3. Modified Versions
|
||||||
|
|
||||||
|
If you create software not governed by this License, and you want to create
|
||||||
|
a new license for such software, you may create and use a modified version
|
||||||
|
of this License if you rename the license and remove any references to the
|
||||||
|
name of the license steward (except to note that such modified license differs
|
||||||
|
from this License).
|
||||||
|
|
||||||
|
10.4. Distributing Source Code Form that is Incompatible With Secondary Licenses
|
||||||
|
|
||||||
|
If You choose to distribute Source Code Form that is Incompatible With Secondary
|
||||||
|
Licenses under the terms of this version of the License, the notice described
|
||||||
|
in Exhibit B of this License must be attached. Exhibit A - Source Code Form
|
||||||
|
License Notice
|
||||||
|
|
||||||
|
This Source Code Form is subject to the terms of the Mozilla Public License,
|
||||||
|
v. 2.0. If a copy of the MPL was not distributed with this file, You can obtain
|
||||||
|
one at http://mozilla.org/MPL/2.0/.
|
||||||
|
|
||||||
|
If it is not possible or desirable to put the notice in a particular file,
|
||||||
|
then You may include the notice in a location (such as a LICENSE file in a
|
||||||
|
relevant directory) where a recipient would be likely to look for such a notice.
|
||||||
|
|
||||||
|
You may add additional accurate notices of copyright ownership.
|
||||||
|
|
||||||
|
Exhibit B - "Incompatible With Secondary Licenses" Notice
|
||||||
|
|
||||||
|
This Source Code Form is "Incompatible With Secondary Licenses", as defined
|
||||||
|
by the Mozilla Public License, v. 2.0.
|
41
vendor/git.rootprojects.org/root/hashcash/README.md
generated
vendored
Normal file
41
vendor/git.rootprojects.org/root/hashcash/README.md
generated
vendored
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
# hashcash
|
||||||
|
|
||||||
|
HTTP Hashcash implemented in Go.
|
||||||
|
|
||||||
|
Explanation at https://therootcompany.com/blog/http-hashcash/
|
||||||
|
|
||||||
|
Go docs at https://godoc.org/git.rootprojects.org/root/hashcash
|
||||||
|
|
||||||
|
# CLI Usage
|
||||||
|
|
||||||
|
Install:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
go get git.rootprojects.org/root/hashcash/cmd/hashcash
|
||||||
|
```
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
|
||||||
|
```txt
|
||||||
|
Usage:
|
||||||
|
hashcash new [subject *] [expires in 5m] [difficulty 10]
|
||||||
|
hashcash parse <hashcash>
|
||||||
|
hashcash solve <hashcash>
|
||||||
|
hashcash verify <hashcash> [subject *]
|
||||||
|
```
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
my_hc=$(hashcash new)
|
||||||
|
echo New: $my_hc
|
||||||
|
hashcash parse "$my_hc"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
my_hc=$(hashcash solve "$my_hc")
|
||||||
|
echo Solved: $my_hc
|
||||||
|
hashcash parse "$my_hc"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
hashcash verify "$my_hc"
|
||||||
|
```
|
3
vendor/git.rootprojects.org/root/hashcash/go.mod
generated
vendored
Normal file
3
vendor/git.rootprojects.org/root/hashcash/go.mod
generated
vendored
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
module git.rootprojects.org/root/hashcash
|
||||||
|
|
||||||
|
go 1.15
|
284
vendor/git.rootprojects.org/root/hashcash/hashcash.go
generated
vendored
Normal file
284
vendor/git.rootprojects.org/root/hashcash/hashcash.go
generated
vendored
Normal file
@ -0,0 +1,284 @@
|
|||||||
|
package hashcash
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/rand"
|
||||||
|
"crypto/sha256"
|
||||||
|
"encoding/base64"
|
||||||
|
"encoding/binary"
|
||||||
|
"errors"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ErrParse is returned when fewer than 6 or more than 7 segments are split
|
||||||
|
var ErrParse = errors.New("could not split the hashcash parts")
|
||||||
|
|
||||||
|
// ErrInvalidTag is returned when the Hashcash version is unsupported
|
||||||
|
var ErrInvalidTag = errors.New("expected tag to be 'H'")
|
||||||
|
|
||||||
|
// ErrInvalidDifficulty is returned when the difficulty is outside of the acceptable range
|
||||||
|
var ErrInvalidDifficulty = errors.New("the number of bits of difficulty is too low or too high")
|
||||||
|
|
||||||
|
// ErrInvalidDate is returned when the date cannot be parsed as a positive int64
|
||||||
|
var ErrInvalidDate = errors.New("invalid date")
|
||||||
|
|
||||||
|
// ErrExpired is returned when the current time is past that of ExpiresAt
|
||||||
|
var ErrExpired = errors.New("expired hashcash")
|
||||||
|
|
||||||
|
// ErrInvalidSubject is returned when the subject is invalid or does not match that passed to Verify()
|
||||||
|
var ErrInvalidSubject = errors.New("the subject is invalid or rejected")
|
||||||
|
|
||||||
|
// ErrInvalidNonce is returned when the nonce
|
||||||
|
//var ErrInvalidNonce = errors.New("the nonce has been used or is invalid")
|
||||||
|
|
||||||
|
// ErrUnsupportedAlgorithm is returned when the given algorithm is not supported
|
||||||
|
var ErrUnsupportedAlgorithm = errors.New("the given algorithm is invalid or not supported")
|
||||||
|
|
||||||
|
// ErrInvalidSolution is returned when the given hashcash is not properly solved
|
||||||
|
var ErrInvalidSolution = errors.New("the given solution is not valid")
|
||||||
|
|
||||||
|
// MaxDifficulty is the upper bound for all Solve() operations
|
||||||
|
var MaxDifficulty = 26
|
||||||
|
|
||||||
|
// Sep is the separator character to use
|
||||||
|
var Sep = ":"
|
||||||
|
|
||||||
|
// no milliseconds
|
||||||
|
//var isoTS = "2006-01-02T15:04:05Z"
|
||||||
|
|
||||||
|
// Hashcash represents a parsed Hashcash string
|
||||||
|
type Hashcash struct {
|
||||||
|
Tag string `json:"tag"` // Always "H" for "HTTP"
|
||||||
|
Difficulty int `json:"difficulty"` // Number of "partial pre-image" (zero) bits in the hashed code
|
||||||
|
ExpiresAt time.Time `json:"exp"` // The timestamp that the hashcash expires, as seconds since the Unix epoch
|
||||||
|
Subject string `json:"sub"` // Resource data string being transmitted, e.g., a domain or URL
|
||||||
|
Nonce string `json:"nonce"` // Unique string of random characters, encoded as url-safe base-64
|
||||||
|
Alg string `json:"alg"` // always SHA-256 for now
|
||||||
|
Solution string `json:"solution"` // Binary counter, encoded as url-safe base-64
|
||||||
|
}
|
||||||
|
|
||||||
|
// New returns a Hashcash with reasonable defaults
|
||||||
|
func New(h Hashcash) *Hashcash {
|
||||||
|
h.Tag = "H"
|
||||||
|
|
||||||
|
if 0 == h.Difficulty {
|
||||||
|
// safe for WebCrypto
|
||||||
|
h.Difficulty = 10
|
||||||
|
}
|
||||||
|
|
||||||
|
if h.ExpiresAt.IsZero() {
|
||||||
|
h.ExpiresAt = time.Now().Add(5 * time.Minute)
|
||||||
|
}
|
||||||
|
h.ExpiresAt = h.ExpiresAt.UTC().Truncate(time.Second)
|
||||||
|
|
||||||
|
if "" == h.Subject {
|
||||||
|
h.Subject = "*"
|
||||||
|
}
|
||||||
|
|
||||||
|
if "" == h.Nonce {
|
||||||
|
nonce := make([]byte, 16)
|
||||||
|
if _, err := rand.Read(nonce); nil != err {
|
||||||
|
panic(err)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
h.Nonce = base64.RawURLEncoding.EncodeToString(nonce)
|
||||||
|
}
|
||||||
|
|
||||||
|
if "" == h.Alg {
|
||||||
|
h.Alg = "SHA-256"
|
||||||
|
}
|
||||||
|
/*
|
||||||
|
if "SHA-256" != h.Alg {
|
||||||
|
// TODO error
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
|
return &h
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse will (obviously) parse the hashcash string, without verifying any
|
||||||
|
// of the parameters.
|
||||||
|
func Parse(hc string) (*Hashcash, error) {
|
||||||
|
parts := strings.Split(hc, Sep)
|
||||||
|
n := len(parts)
|
||||||
|
if n < 6 || n > 7 {
|
||||||
|
return nil, ErrParse
|
||||||
|
}
|
||||||
|
|
||||||
|
tag := parts[0]
|
||||||
|
if "H" != tag {
|
||||||
|
return nil, ErrInvalidTag
|
||||||
|
}
|
||||||
|
|
||||||
|
bits, err := strconv.Atoi(parts[1])
|
||||||
|
if nil != err || bits < 0 {
|
||||||
|
return nil, ErrInvalidDifficulty
|
||||||
|
}
|
||||||
|
|
||||||
|
// Allow empty ExpiresAt
|
||||||
|
var exp time.Time
|
||||||
|
if "" != parts[2] {
|
||||||
|
expAt, err := strconv.ParseInt(parts[2], 10, 64)
|
||||||
|
if nil != err || expAt < 0 {
|
||||||
|
return nil, ErrInvalidDate
|
||||||
|
}
|
||||||
|
exp = time.Unix(int64(expAt), 0).UTC()
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
exp, err := time.ParseInLocation(isoTS, parts[2], time.UTC)
|
||||||
|
if nil != err {
|
||||||
|
return nil, ErrInvalidDate
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
|
sub := parts[3]
|
||||||
|
|
||||||
|
nonce := parts[4]
|
||||||
|
|
||||||
|
alg := parts[5]
|
||||||
|
|
||||||
|
var solution string
|
||||||
|
if n > 6 {
|
||||||
|
solution = parts[6]
|
||||||
|
}
|
||||||
|
|
||||||
|
h := &Hashcash{
|
||||||
|
Tag: tag,
|
||||||
|
Difficulty: bits,
|
||||||
|
ExpiresAt: exp.UTC().Truncate(time.Second),
|
||||||
|
Subject: sub,
|
||||||
|
Nonce: nonce,
|
||||||
|
Alg: alg,
|
||||||
|
Solution: solution,
|
||||||
|
}
|
||||||
|
|
||||||
|
return h, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// String will return the formatted Hashcash, omitting the solution if it has not be solved.
|
||||||
|
func (h *Hashcash) String() string {
|
||||||
|
var solution string
|
||||||
|
if "" != h.Solution {
|
||||||
|
solution = Sep + h.Solution
|
||||||
|
}
|
||||||
|
|
||||||
|
var expAt string
|
||||||
|
if !h.ExpiresAt.IsZero() {
|
||||||
|
expAt = strconv.FormatInt(h.ExpiresAt.UTC().Truncate(time.Second).Unix(), 10)
|
||||||
|
}
|
||||||
|
return strings.Join(
|
||||||
|
[]string{
|
||||||
|
"H",
|
||||||
|
strconv.Itoa(h.Difficulty),
|
||||||
|
//h.ExpiresAt.UTC().Format(isoTS),
|
||||||
|
expAt,
|
||||||
|
h.Subject,
|
||||||
|
h.Nonce,
|
||||||
|
h.Alg,
|
||||||
|
},
|
||||||
|
Sep,
|
||||||
|
) + solution
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify the Hashcash based on Difficulty, Algorithm, ExpiresAt, Subject and,
|
||||||
|
// of course, the Solution and hash.
|
||||||
|
func (h *Hashcash) Verify(subject string) error {
|
||||||
|
if h.Difficulty < 0 {
|
||||||
|
return ErrInvalidDifficulty
|
||||||
|
}
|
||||||
|
|
||||||
|
if "SHA-256" != h.Alg {
|
||||||
|
return ErrUnsupportedAlgorithm
|
||||||
|
}
|
||||||
|
|
||||||
|
if !h.ExpiresAt.IsZero() && h.ExpiresAt.Sub(time.Now()) < 0 {
|
||||||
|
return ErrExpired
|
||||||
|
}
|
||||||
|
|
||||||
|
if subject != h.Subject {
|
||||||
|
return ErrInvalidSubject
|
||||||
|
}
|
||||||
|
|
||||||
|
bits := h.Difficulty
|
||||||
|
hash := sha256.Sum256([]byte(h.String()))
|
||||||
|
n := bits / 8 // 10 / 8 = 1
|
||||||
|
m := bits % 8 // 10 % 8 = 2
|
||||||
|
if m > 0 {
|
||||||
|
n++ // 10 bits = 2 bytes
|
||||||
|
}
|
||||||
|
|
||||||
|
if !verifyBits(hash[:n], bits, n) {
|
||||||
|
return ErrInvalidSolution
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func verifyBits(hash []byte, bits, n int) bool {
|
||||||
|
for i := 0; i < n; i++ {
|
||||||
|
if bits > 8 {
|
||||||
|
bits -= 8
|
||||||
|
if 0 != hash[i] {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// (bits % 8) == bits
|
||||||
|
pad := 8 - bits
|
||||||
|
if 0 != hash[i]>>pad {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// 0 == bits
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Solve will search for a solution, returning an error if the difficulty is
|
||||||
|
// above the local or global MaxDifficulty, the Algorithm is unsupported.
|
||||||
|
func (h *Hashcash) Solve(maxDifficulty int) error {
|
||||||
|
if "SHA-256" != h.Alg {
|
||||||
|
return ErrUnsupportedAlgorithm
|
||||||
|
}
|
||||||
|
|
||||||
|
if h.Difficulty < 0 {
|
||||||
|
return ErrInvalidDifficulty
|
||||||
|
}
|
||||||
|
|
||||||
|
if h.Difficulty > maxDifficulty || h.Difficulty > MaxDifficulty {
|
||||||
|
return ErrInvalidDifficulty
|
||||||
|
}
|
||||||
|
|
||||||
|
if "" != h.Solution {
|
||||||
|
if nil == h.Verify(h.Subject) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
h.Solution = ""
|
||||||
|
}
|
||||||
|
|
||||||
|
hashcash := h.String()
|
||||||
|
bits := h.Difficulty
|
||||||
|
n := bits / 8 // 10 / 8 = 1
|
||||||
|
m := bits % 8 // 10 % 8 = 2
|
||||||
|
if m > 0 {
|
||||||
|
n++ // 10 bits = 2 bytes
|
||||||
|
}
|
||||||
|
|
||||||
|
var solution uint32 = 0
|
||||||
|
sb := make([]byte, 4)
|
||||||
|
for {
|
||||||
|
// Note: it's not actually important what method of change or encoding is used
|
||||||
|
// but incrementing by 1 on an int32 is good enough, and makes for a small base64 encoding
|
||||||
|
binary.LittleEndian.PutUint32(sb, solution)
|
||||||
|
h.Solution = base64.RawURLEncoding.EncodeToString(sb)
|
||||||
|
hash := sha256.Sum256([]byte(hashcash + Sep + h.Solution))
|
||||||
|
if verifyBits(hash[:n], bits, n) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
solution++
|
||||||
|
}
|
||||||
|
}
|
5
vendor/git.rootprojects.org/root/keypairs/.gitignore
generated
vendored
Normal file
5
vendor/git.rootprojects.org/root/keypairs/.gitignore
generated
vendored
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
/keypairs
|
||||||
|
/dist/
|
||||||
|
|
||||||
|
.DS_Store
|
||||||
|
.*.sw*
|
41
vendor/git.rootprojects.org/root/keypairs/.goreleaser.yml
generated
vendored
Normal file
41
vendor/git.rootprojects.org/root/keypairs/.goreleaser.yml
generated
vendored
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
# This is an example goreleaser.yaml file with some sane defaults.
|
||||||
|
# Make sure to check the documentation at http://goreleaser.com
|
||||||
|
before:
|
||||||
|
hooks:
|
||||||
|
- go generate ./...
|
||||||
|
builds:
|
||||||
|
- id: keypairs
|
||||||
|
main: ./cmd/keypairs/keypairs.go
|
||||||
|
env:
|
||||||
|
- CGO_ENABLED=0
|
||||||
|
flags:
|
||||||
|
- -mod=vendor
|
||||||
|
goos:
|
||||||
|
- linux
|
||||||
|
- windows
|
||||||
|
- darwin
|
||||||
|
- freebsd
|
||||||
|
goarch:
|
||||||
|
- amd64
|
||||||
|
- arm
|
||||||
|
- arm64
|
||||||
|
archives:
|
||||||
|
- replacements:
|
||||||
|
386: i386
|
||||||
|
amd64: x86-64
|
||||||
|
arm64: aarch64
|
||||||
|
format_overrides:
|
||||||
|
- goos: windows
|
||||||
|
format: zip
|
||||||
|
env_files:
|
||||||
|
github_token: ~/.config/goreleaser/github_token.txt
|
||||||
|
checksum:
|
||||||
|
name_template: 'checksums.txt'
|
||||||
|
snapshot:
|
||||||
|
name_template: "{{ .Tag }}-next"
|
||||||
|
changelog:
|
||||||
|
sort: asc
|
||||||
|
filters:
|
||||||
|
exclude:
|
||||||
|
- '^docs:'
|
||||||
|
- '^test:'
|
1
vendor/git.rootprojects.org/root/keypairs/AUTHORS
generated
vendored
Normal file
1
vendor/git.rootprojects.org/root/keypairs/AUTHORS
generated
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
AJ ONeal <aj@therootcompany.com> (https://therootcompany.com)
|
21
vendor/git.rootprojects.org/root/keypairs/LICENSE
generated
vendored
Normal file
21
vendor/git.rootprojects.org/root/keypairs/LICENSE
generated
vendored
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
The MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2018-2019 Big Squid, Inc
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
63
vendor/git.rootprojects.org/root/keypairs/README.md
generated
vendored
Normal file
63
vendor/git.rootprojects.org/root/keypairs/README.md
generated
vendored
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
# [keypairs](https://git.rootprojects.org/root/keypairs)
|
||||||
|
|
||||||
|
JSON Web Key (JWK) support and type safety lightly placed over top of Go's `crypto/ecdsa` and `crypto/rsa`
|
||||||
|
|
||||||
|
Useful for JWT, JOSE, etc.
|
||||||
|
|
||||||
|
```go
|
||||||
|
key, err := keypairs.ParsePrivateKey(bytesForJWKOrPEMOrDER)
|
||||||
|
|
||||||
|
pub, err := keypairs.ParsePublicKey(bytesForJWKOrPEMOrDER)
|
||||||
|
|
||||||
|
jwk, err := keypairs.MarshalJWKPublicKey(pub, time.Now().Add(2 * time.Day))
|
||||||
|
|
||||||
|
kid, err := keypairs.ThumbprintPublicKey(pub)
|
||||||
|
```
|
||||||
|
|
||||||
|
# GoDoc API Documentation
|
||||||
|
|
||||||
|
See <https://pkg.go.dev/git.rootprojects.org/root/keypairs>
|
||||||
|
|
||||||
|
# Philosophy
|
||||||
|
|
||||||
|
Go's standard library is great.
|
||||||
|
|
||||||
|
Go has _excellent_ crytography support and provides wonderful
|
||||||
|
primitives for dealing with them.
|
||||||
|
|
||||||
|
I prefer to stay as close to Go's `crypto` package as possible,
|
||||||
|
just adding a light touch for JWT support and type safety.
|
||||||
|
|
||||||
|
# Type Safety
|
||||||
|
|
||||||
|
`crypto.PublicKey` is a "marker interface", meaning that it is **not typesafe**!
|
||||||
|
|
||||||
|
`go-keypairs` defines `type keypairs.PrivateKey interface { Public() crypto.PublicKey }`,
|
||||||
|
which is implemented by `crypto/rsa` and `crypto/ecdsa`
|
||||||
|
(but not `crypto/dsa`, which we really don't care that much about).
|
||||||
|
|
||||||
|
Go1.15 will add `[PublicKey.Equal(crypto.PublicKey)](https://github.com/golang/go/issues/21704)`,
|
||||||
|
which will make it possible to remove the additional wrapper over `PublicKey`
|
||||||
|
and use an interface instead.
|
||||||
|
|
||||||
|
Since there are no common methods between `rsa.PublicKey` and `ecdsa.PublicKey`,
|
||||||
|
go-keypairs lightly wraps each to implement `Thumbprint() string` (part of the JOSE/JWK spec).
|
||||||
|
|
||||||
|
## JSON Web Key (JWK) as a "codec"
|
||||||
|
|
||||||
|
Although there are many, many ways that JWKs could be interpreted
|
||||||
|
(possibly why they haven't made it into the standard library), `go-keypairs`
|
||||||
|
follows the basic pattern of `encoding/x509` to `Parse` and `Marshal`
|
||||||
|
only the most basic and most meaningful parts of a key.
|
||||||
|
|
||||||
|
I highly recommend that you use `Thumbprint()` for `KeyID` you also
|
||||||
|
get the benefit of not losing information when encoding and decoding
|
||||||
|
between the ASN.1, x509, PEM, and JWK formats.
|
||||||
|
|
||||||
|
# LICENSE
|
||||||
|
|
||||||
|
Copyright (c) 2020-present AJ ONeal \
|
||||||
|
Copyright (c) 2018-2019 Big Squid, Inc.
|
||||||
|
|
||||||
|
This work is licensed under the terms of the MIT license. \
|
||||||
|
For a copy, see <https://opensource.org/licenses/MIT>.
|
19
vendor/git.rootprojects.org/root/keypairs/cli_test.sh
generated
vendored
Normal file
19
vendor/git.rootprojects.org/root/keypairs/cli_test.sh
generated
vendored
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
set -u
|
||||||
|
|
||||||
|
go build -mod=vendor cmd/keypairs/*.go
|
||||||
|
./keypairs gen > testkey.jwk.json 2> testpub.jwk.json
|
||||||
|
|
||||||
|
./keypairs sign --exp 1h ./testkey.jwk.json '{"foo":"bar"}' > testjwt.txt 2> testjws.json
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "Should pass:"
|
||||||
|
./keypairs verify ./testpub.jwk.json testjwt.txt > /dev/null
|
||||||
|
./keypairs verify ./testpub.jwk.json "$(cat testjwt.txt)" > /dev/null
|
||||||
|
./keypairs verify ./testpub.jwk.json testjws.json > /dev/null
|
||||||
|
./keypairs verify ./testpub.jwk.json "$(cat testjws.json)" > /dev/null
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "Should fail:"
|
||||||
|
./keypairs sign --exp -1m ./testkey.jwk.json '{"bar":"foo"}' > errjwt.txt 2> errjws.json
|
||||||
|
./keypairs verify ./testpub.jwk.json errjwt.txt > /dev/null
|
40
vendor/git.rootprojects.org/root/keypairs/doc.go
generated
vendored
Normal file
40
vendor/git.rootprojects.org/root/keypairs/doc.go
generated
vendored
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
/*
|
||||||
|
Package keypairs complements Go's standard keypair-related packages
|
||||||
|
(encoding/pem, crypto/x509, crypto/rsa, crypto/ecdsa, crypto/elliptic)
|
||||||
|
with JWK encoding support and typesafe PrivateKey and PublicKey interfaces.
|
||||||
|
|
||||||
|
Basics
|
||||||
|
|
||||||
|
key, err := keypairs.ParsePrivateKey(bytesForJWKOrPEMOrDER)
|
||||||
|
|
||||||
|
pub, err := keypairs.ParsePublicKey(bytesForJWKOrPEMOrDER)
|
||||||
|
|
||||||
|
jwk, err := keypairs.MarshalJWKPublicKey(pub, time.Now().Add(2 * time.Day))
|
||||||
|
|
||||||
|
kid, err := keypairs.ThumbprintPublicKey(pub)
|
||||||
|
|
||||||
|
Convenience functions are available which will fetch keys
|
||||||
|
(or retrieve them from cache) via OIDC, .well-known/jwks.json, and direct urls.
|
||||||
|
All keys are cached by Thumbprint, as well as kid(@issuer), if available.
|
||||||
|
|
||||||
|
import "git.rootprojects.org/root/keypairs/keyfetch"
|
||||||
|
|
||||||
|
pubs, err := keyfetch.OIDCJWKs("https://example.com/")
|
||||||
|
pubs, err := keyfetch.OIDCJWK(ThumbOrKeyID, "https://example.com/")
|
||||||
|
|
||||||
|
pubs, err := keyfetch.WellKnownJWKs("https://example.com/")
|
||||||
|
pubs, err := keyfetch.WellKnownJWK(ThumbOrKeyID, "https://example.com/")
|
||||||
|
|
||||||
|
pubs, err := keyfetch.JWKs("https://example.com/path/to/jwks/")
|
||||||
|
pubs, err := keyfetch.JWK(ThumbOrKeyID, "https://example.com/path/to/jwks/")
|
||||||
|
|
||||||
|
// From URL
|
||||||
|
pub, err := keyfetch.Fetch("https://example.com/jwk.json")
|
||||||
|
|
||||||
|
// From Cache only
|
||||||
|
pub := keyfetch.Get(thumbprint, "https://example.com/jwk.json")
|
||||||
|
|
||||||
|
A non-caching version with the same capabilities is also available.
|
||||||
|
|
||||||
|
*/
|
||||||
|
package keypairs
|
69
vendor/git.rootprojects.org/root/keypairs/generate.go
generated
vendored
Normal file
69
vendor/git.rootprojects.org/root/keypairs/generate.go
generated
vendored
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
package keypairs
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/ecdsa"
|
||||||
|
"crypto/elliptic"
|
||||||
|
"crypto/rand"
|
||||||
|
"crypto/rsa"
|
||||||
|
"io"
|
||||||
|
mathrand "math/rand"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
var randReader io.Reader = rand.Reader
|
||||||
|
var allowMocking = false
|
||||||
|
|
||||||
|
// KeyOptions are the things that we may need to know about a request to fulfill it properly
|
||||||
|
type keyOptions struct {
|
||||||
|
//Key string `json:"key"`
|
||||||
|
KeyType string `json:"kty"`
|
||||||
|
mockSeed int64 //`json:"-"`
|
||||||
|
//SeedStr string `json:"seed"`
|
||||||
|
//Claims Object `json:"claims"`
|
||||||
|
//Header Object `json:"header"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *keyOptions) nextReader() io.Reader {
|
||||||
|
if allowMocking {
|
||||||
|
return o.maybeMockReader()
|
||||||
|
}
|
||||||
|
return randReader
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewDefaultPrivateKey generates a key with reasonable strength.
|
||||||
|
// Today that means a 256-bit equivalent - either RSA 2048 or EC P-256.
|
||||||
|
func NewDefaultPrivateKey() PrivateKey {
|
||||||
|
// insecure random is okay here,
|
||||||
|
// it's just used for a coin toss
|
||||||
|
mathrand.Seed(time.Now().UnixNano())
|
||||||
|
coin := mathrand.Int()
|
||||||
|
|
||||||
|
// the idea here is that we want to make
|
||||||
|
// it dead simple to support RSA and EC
|
||||||
|
// so it shouldn't matter which is used
|
||||||
|
if 0 == coin%2 {
|
||||||
|
return newPrivateKey(&keyOptions{
|
||||||
|
KeyType: "RSA",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return newPrivateKey(&keyOptions{
|
||||||
|
KeyType: "EC",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// newPrivateKey generates a 256-bit entropy RSA or ECDSA private key
|
||||||
|
func newPrivateKey(opts *keyOptions) PrivateKey {
|
||||||
|
var privkey PrivateKey
|
||||||
|
|
||||||
|
if "RSA" == opts.KeyType {
|
||||||
|
keylen := 2048
|
||||||
|
privkey, _ = rsa.GenerateKey(opts.nextReader(), keylen)
|
||||||
|
if allowMocking {
|
||||||
|
privkey = maybeDerandomizeMockKey(privkey, keylen, opts)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// TODO: EC keys may also suffer the same random problems in the future
|
||||||
|
privkey, _ = ecdsa.GenerateKey(elliptic.P256(), opts.nextReader())
|
||||||
|
}
|
||||||
|
return privkey
|
||||||
|
}
|
3
vendor/git.rootprojects.org/root/keypairs/go.mod
generated
vendored
Normal file
3
vendor/git.rootprojects.org/root/keypairs/go.mod
generated
vendored
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
module git.rootprojects.org/root/keypairs
|
||||||
|
|
||||||
|
go 1.12
|
69
vendor/git.rootprojects.org/root/keypairs/jwk.go
generated
vendored
Normal file
69
vendor/git.rootprojects.org/root/keypairs/jwk.go
generated
vendored
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
package keypairs
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
)
|
||||||
|
|
||||||
|
// JWK abstracts EC and RSA keys
|
||||||
|
type JWK interface {
|
||||||
|
marshalJWK() ([]byte, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ECJWK is the EC variant
|
||||||
|
type ECJWK struct {
|
||||||
|
KeyID string `json:"kid,omitempty"`
|
||||||
|
Curve string `json:"crv"`
|
||||||
|
X string `json:"x"`
|
||||||
|
Y string `json:"y"`
|
||||||
|
Use []string `json:"use,omitempty"`
|
||||||
|
Seed string `json:"_seed,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (k *ECJWK) marshalJWK() ([]byte, error) {
|
||||||
|
return []byte(fmt.Sprintf(`{"crv":%q,"kty":"EC","x":%q,"y":%q}`, k.Curve, k.X, k.Y)), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// RSAJWK is the RSA variant
|
||||||
|
type RSAJWK struct {
|
||||||
|
KeyID string `json:"kid,omitempty"`
|
||||||
|
Exp string `json:"e"`
|
||||||
|
N string `json:"n"`
|
||||||
|
Use []string `json:"use,omitempty"`
|
||||||
|
Seed string `json:"_seed,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (k *RSAJWK) marshalJWK() ([]byte, error) {
|
||||||
|
return []byte(fmt.Sprintf(`{"e":%q,"kty":"RSA","n":%q}`, k.Exp, k.N)), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
// ToPublicJWK exposes only the public parts
|
||||||
|
func ToPublicJWK(pubkey PublicKey) JWK {
|
||||||
|
switch k := pubkey.Key().(type) {
|
||||||
|
case *ecdsa.PublicKey:
|
||||||
|
return ECToPublicJWK(k)
|
||||||
|
case *rsa.PublicKey:
|
||||||
|
return RSAToPublicJWK(k)
|
||||||
|
default:
|
||||||
|
panic(errors.New("impossible key type"))
|
||||||
|
//return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ECToPublicJWK will output the most minimal version of an EC JWK (no key id, no "use" flag, nada)
|
||||||
|
func ECToPublicJWK(k *ecdsa.PublicKey) *ECJWK {
|
||||||
|
return &ECJWK{
|
||||||
|
Curve: k.Curve.Params().Name,
|
||||||
|
X: base64.RawURLEncoding.EncodeToString(k.X.Bytes()),
|
||||||
|
Y: base64.RawURLEncoding.EncodeToString(k.Y.Bytes()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// RSAToPublicJWK will output the most minimal version of an RSA JWK (no key id, no "use" flag, nada)
|
||||||
|
func RSAToPublicJWK(p *rsa.PublicKey) *RSAJWK {
|
||||||
|
return &RSAJWK{
|
||||||
|
Exp: base64.RawURLEncoding.EncodeToString(big.NewInt(int64(p.E)).Bytes()),
|
||||||
|
N: base64.RawURLEncoding.EncodeToString(p.N.Bytes()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
*/
|
63
vendor/git.rootprojects.org/root/keypairs/jws.go
generated
vendored
Normal file
63
vendor/git.rootprojects.org/root/keypairs/jws.go
generated
vendored
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
package keypairs
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/base64"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// JWS is a parsed JWT, representation as signable/verifiable and human-readable parts
|
||||||
|
type JWS struct {
|
||||||
|
Header Object `json:"header"` // JSON
|
||||||
|
Claims Object `json:"claims"` // JSON
|
||||||
|
Protected string `json:"protected"` // base64
|
||||||
|
Payload string `json:"payload"` // base64
|
||||||
|
Signature string `json:"signature"` // base64
|
||||||
|
}
|
||||||
|
|
||||||
|
// JWSToJWT joins JWS parts into a JWT as {ProtectedHeader}.{SerializedPayload}.{Signature}.
|
||||||
|
func JWSToJWT(jwt *JWS) string {
|
||||||
|
return fmt.Sprintf(
|
||||||
|
"%s.%s.%s",
|
||||||
|
jwt.Protected,
|
||||||
|
jwt.Payload,
|
||||||
|
jwt.Signature,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// JWTToJWS splits the JWT into its JWS segments
|
||||||
|
func JWTToJWS(jwt string) (jws *JWS) {
|
||||||
|
jwt = strings.TrimSpace(jwt)
|
||||||
|
parts := strings.Split(jwt, ".")
|
||||||
|
if 3 != len(parts) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return &JWS{
|
||||||
|
Protected: parts[0],
|
||||||
|
Payload: parts[1],
|
||||||
|
Signature: parts[2],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// DecodeComponents decodes JWS Header and Claims
|
||||||
|
func (jws *JWS) DecodeComponents() error {
|
||||||
|
protected, err := base64.RawURLEncoding.DecodeString(jws.Protected)
|
||||||
|
if nil != err {
|
||||||
|
return errors.New("invalid JWS header base64Url encoding")
|
||||||
|
}
|
||||||
|
if err := json.Unmarshal([]byte(protected), &jws.Header); nil != err {
|
||||||
|
return errors.New("invalid JWS header")
|
||||||
|
}
|
||||||
|
|
||||||
|
payload, err := base64.RawURLEncoding.DecodeString(jws.Payload)
|
||||||
|
if nil != err {
|
||||||
|
return errors.New("invalid JWS payload base64Url encoding")
|
||||||
|
}
|
||||||
|
if err := json.Unmarshal([]byte(payload), &jws.Claims); nil != err {
|
||||||
|
return errors.New("invalid JWS claims")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
516
vendor/git.rootprojects.org/root/keypairs/keyfetch/fetch.go
generated
vendored
Normal file
516
vendor/git.rootprojects.org/root/keypairs/keyfetch/fetch.go
generated
vendored
Normal file
@ -0,0 +1,516 @@
|
|||||||
|
// Package keyfetch retrieve and cache PublicKeys
|
||||||
|
// from OIDC (https://example.com/.well-known/openid-configuration)
|
||||||
|
// and Auth0 (https://example.com/.well-known/jwks.json)
|
||||||
|
// JWKs URLs and expires them when `exp` is reached
|
||||||
|
// (or a default expiry if the key does not provide one).
|
||||||
|
// It uses the keypairs package to Unmarshal the JWKs into their
|
||||||
|
// native types (with a very thin shim to provide the type safety
|
||||||
|
// that Go's crypto.PublicKey and crypto.PrivateKey interfaces lack).
|
||||||
|
package keyfetch
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"git.rootprojects.org/root/keypairs"
|
||||||
|
"git.rootprojects.org/root/keypairs/keyfetch/uncached"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TODO should be ErrInvalidJWKURL
|
||||||
|
|
||||||
|
// EInvalidJWKURL means that the url did not provide JWKs
|
||||||
|
var EInvalidJWKURL = errors.New("url does not lead to valid JWKs")
|
||||||
|
|
||||||
|
// KeyCache is an in-memory key cache
|
||||||
|
var KeyCache = map[string]CachableKey{}
|
||||||
|
|
||||||
|
// KeyCacheMux is used to guard the in-memory cache
|
||||||
|
var KeyCacheMux = sync.Mutex{}
|
||||||
|
|
||||||
|
// ErrInsecureDomain means that plain http was used where https was expected
|
||||||
|
var ErrInsecureDomain = errors.New("Whitelists should only allow secure URLs (i.e. https://). To allow unsecured private networking (i.e. Docker) pass PrivateWhitelist as a list of private URLs")
|
||||||
|
|
||||||
|
// TODO Cacheable key (shouldn't this be private)?
|
||||||
|
|
||||||
|
// CachableKey represents
|
||||||
|
type CachableKey struct {
|
||||||
|
Key keypairs.PublicKey
|
||||||
|
Expiry time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
// maybe TODO use this poor-man's enum to allow kids thumbs to be accepted by the same method?
|
||||||
|
/*
|
||||||
|
type KeyID string
|
||||||
|
|
||||||
|
func (kid KeyID) ID() string {
|
||||||
|
return string(kid)
|
||||||
|
}
|
||||||
|
func (kid KeyID) isID() {}
|
||||||
|
|
||||||
|
type Thumbprint string
|
||||||
|
|
||||||
|
func (thumb Thumbprint) ID() string {
|
||||||
|
return string(thumb)
|
||||||
|
}
|
||||||
|
func (thumb Thumbprint) isID() {}
|
||||||
|
|
||||||
|
type ID interface {
|
||||||
|
ID() string
|
||||||
|
isID()
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
|
// StaleTime defines when public keys should be renewed (15 minutes by default)
|
||||||
|
var StaleTime = 15 * time.Minute
|
||||||
|
|
||||||
|
// DefaultKeyDuration defines how long a key should be considered fresh (48 hours by default)
|
||||||
|
var DefaultKeyDuration = 48 * time.Hour
|
||||||
|
|
||||||
|
// MinimumKeyDuration defines the minimum time that a key will be cached (1 hour by default)
|
||||||
|
var MinimumKeyDuration = time.Hour
|
||||||
|
|
||||||
|
// MaximumKeyDuration defines the maximum time that a key will be cached (72 hours by default)
|
||||||
|
var MaximumKeyDuration = 72 * time.Hour
|
||||||
|
|
||||||
|
// PublicKeysMap is a newtype for a map of keypairs.PublicKey
|
||||||
|
type PublicKeysMap map[string]keypairs.PublicKey
|
||||||
|
|
||||||
|
// OIDCJWKs fetches baseURL + ".well-known/openid-configuration" and then fetches and returns the Public Keys.
|
||||||
|
func OIDCJWKs(baseURL string) (PublicKeysMap, error) {
|
||||||
|
maps, keys, err := uncached.OIDCJWKs(baseURL)
|
||||||
|
|
||||||
|
if nil != err {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
cacheKeys(maps, keys, baseURL)
|
||||||
|
return keys, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// OIDCJWK fetches baseURL + ".well-known/openid-configuration" and then returns the key matching kid (or thumbprint)
|
||||||
|
func OIDCJWK(kidOrThumb, iss string) (keypairs.PublicKey, error) {
|
||||||
|
return immediateOneOrFetch(kidOrThumb, iss, uncached.OIDCJWKs)
|
||||||
|
}
|
||||||
|
|
||||||
|
// WellKnownJWKs fetches baseURL + ".well-known/jwks.json" and caches and returns the keys
|
||||||
|
func WellKnownJWKs(kidOrThumb, iss string) (PublicKeysMap, error) {
|
||||||
|
maps, keys, err := uncached.WellKnownJWKs(iss)
|
||||||
|
|
||||||
|
if nil != err {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
cacheKeys(maps, keys, iss)
|
||||||
|
return keys, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// WellKnownJWK fetches baseURL + ".well-known/jwks.json" and returns the key matching kid (or thumbprint)
|
||||||
|
func WellKnownJWK(kidOrThumb, iss string) (keypairs.PublicKey, error) {
|
||||||
|
return immediateOneOrFetch(kidOrThumb, iss, uncached.WellKnownJWKs)
|
||||||
|
}
|
||||||
|
|
||||||
|
// JWKs returns a map of keys identified by their thumbprint
|
||||||
|
// (since kid may or may not be present)
|
||||||
|
func JWKs(jwksurl string) (PublicKeysMap, error) {
|
||||||
|
maps, keys, err := uncached.JWKs(jwksurl)
|
||||||
|
|
||||||
|
if nil != err {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
iss := strings.Replace(jwksurl, ".well-known/jwks.json", "", 1)
|
||||||
|
cacheKeys(maps, keys, iss)
|
||||||
|
return keys, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// JWK tries to return a key from cache, falling back to the /.well-known/jwks.json of the issuer
|
||||||
|
func JWK(kidOrThumb, iss string) (keypairs.PublicKey, error) {
|
||||||
|
return immediateOneOrFetch(kidOrThumb, iss, uncached.JWKs)
|
||||||
|
}
|
||||||
|
|
||||||
|
// PEM tries to return a key from cache, falling back to the specified PEM url
|
||||||
|
func PEM(url string) (keypairs.PublicKey, error) {
|
||||||
|
// url is kid in this case
|
||||||
|
return immediateOneOrFetch(url, url, func(string) (map[string]map[string]string, map[string]keypairs.PublicKey, error) {
|
||||||
|
m, key, err := uncached.PEM(url)
|
||||||
|
if nil != err {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// put in a map, just for caching
|
||||||
|
maps := map[string]map[string]string{}
|
||||||
|
maps[key.Thumbprint()] = m
|
||||||
|
maps[url] = m
|
||||||
|
|
||||||
|
keys := map[string]keypairs.PublicKey{}
|
||||||
|
keys[key.Thumbprint()] = key
|
||||||
|
keys[url] = key
|
||||||
|
|
||||||
|
return maps, keys, nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch returns a key from cache, falling back to an exact url as the "issuer"
|
||||||
|
func Fetch(url string) (keypairs.PublicKey, error) {
|
||||||
|
// url is kid in this case
|
||||||
|
return immediateOneOrFetch(url, url, func(string) (map[string]map[string]string, map[string]keypairs.PublicKey, error) {
|
||||||
|
m, key, err := uncached.Fetch(url)
|
||||||
|
if nil != err {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// put in a map, just for caching
|
||||||
|
maps := map[string]map[string]string{}
|
||||||
|
maps[key.Thumbprint()] = m
|
||||||
|
|
||||||
|
keys := map[string]keypairs.PublicKey{}
|
||||||
|
keys[key.Thumbprint()] = key
|
||||||
|
|
||||||
|
return maps, keys, nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get retrieves a key from cache, or returns an error.
|
||||||
|
// The issuer string may be empty if using a thumbprint rather than a kid.
|
||||||
|
func Get(kidOrThumb, iss string) keypairs.PublicKey {
|
||||||
|
if pub := get(kidOrThumb, iss); nil != pub {
|
||||||
|
return pub.Key
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func get(kidOrThumb, iss string) *CachableKey {
|
||||||
|
iss = normalizeIssuer(iss)
|
||||||
|
KeyCacheMux.Lock()
|
||||||
|
defer KeyCacheMux.Unlock()
|
||||||
|
|
||||||
|
// we're safe to check the cache by kid alone
|
||||||
|
// by virtue that we never set it by kid alone
|
||||||
|
hit, ok := KeyCache[kidOrThumb]
|
||||||
|
if ok {
|
||||||
|
if now := time.Now(); hit.Expiry.Sub(now) > 0 {
|
||||||
|
// only return non-expired keys
|
||||||
|
return &hit
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
id := kidOrThumb + "@" + iss
|
||||||
|
hit, ok = KeyCache[id]
|
||||||
|
if ok {
|
||||||
|
if now := time.Now(); hit.Expiry.Sub(now) > 0 {
|
||||||
|
// only return non-expired keys
|
||||||
|
return &hit
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func immediateOneOrFetch(kidOrThumb, iss string, fetcher myfetcher) (keypairs.PublicKey, error) {
|
||||||
|
now := time.Now()
|
||||||
|
key := get(kidOrThumb, iss)
|
||||||
|
|
||||||
|
if nil == key {
|
||||||
|
return fetchAndSelect(kidOrThumb, iss, fetcher)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch just a little before the key actually expires
|
||||||
|
if key.Expiry.Sub(now) <= StaleTime {
|
||||||
|
go fetchAndSelect(kidOrThumb, iss, fetcher)
|
||||||
|
}
|
||||||
|
|
||||||
|
return key.Key, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type myfetcher func(string) (map[string]map[string]string, map[string]keypairs.PublicKey, error)
|
||||||
|
|
||||||
|
func fetchAndSelect(id, baseURL string, fetcher myfetcher) (keypairs.PublicKey, error) {
|
||||||
|
maps, keys, err := fetcher(baseURL)
|
||||||
|
if nil != err {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
cacheKeys(maps, keys, baseURL)
|
||||||
|
|
||||||
|
for i := range keys {
|
||||||
|
key := keys[i]
|
||||||
|
|
||||||
|
if id == key.Thumbprint() {
|
||||||
|
return key, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if id == key.KeyID() {
|
||||||
|
return key, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, fmt.Errorf("Key identified by '%s' was not found at %s", id, baseURL)
|
||||||
|
}
|
||||||
|
|
||||||
|
func cacheKeys(maps map[string]map[string]string, keys map[string]keypairs.PublicKey, issuer string) {
|
||||||
|
for i := range keys {
|
||||||
|
key := keys[i]
|
||||||
|
m := maps[i]
|
||||||
|
iss := issuer
|
||||||
|
if "" != m["iss"] {
|
||||||
|
iss = m["iss"]
|
||||||
|
}
|
||||||
|
iss = normalizeIssuer(iss)
|
||||||
|
cacheKey(m["kid"], iss, m["exp"], key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func cacheKey(kid, iss, expstr string, pub keypairs.PublicKey) error {
|
||||||
|
var expiry time.Time
|
||||||
|
iss = normalizeIssuer(iss)
|
||||||
|
|
||||||
|
exp, _ := strconv.ParseInt(expstr, 10, 64)
|
||||||
|
if 0 == exp {
|
||||||
|
// use default
|
||||||
|
expiry = time.Now().Add(DefaultKeyDuration)
|
||||||
|
} else if exp < time.Now().Add(MinimumKeyDuration).Unix() || exp > time.Now().Add(MaximumKeyDuration).Unix() {
|
||||||
|
// use at least one hour
|
||||||
|
expiry = time.Now().Add(MinimumKeyDuration)
|
||||||
|
} else {
|
||||||
|
expiry = time.Unix(exp, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
KeyCacheMux.Lock()
|
||||||
|
defer KeyCacheMux.Unlock()
|
||||||
|
// Put the key in the cache by both kid and thumbprint, and set the expiry
|
||||||
|
id := kid + "@" + iss
|
||||||
|
KeyCache[id] = CachableKey{
|
||||||
|
Key: pub,
|
||||||
|
Expiry: expiry,
|
||||||
|
}
|
||||||
|
// Since thumbprints are crypto secure, iss isn't needed
|
||||||
|
thumb := pub.Thumbprint()
|
||||||
|
KeyCache[thumb] = CachableKey{
|
||||||
|
Key: pub,
|
||||||
|
Expiry: expiry,
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func clear() {
|
||||||
|
KeyCacheMux.Lock()
|
||||||
|
defer KeyCacheMux.Unlock()
|
||||||
|
KeyCache = map[string]CachableKey{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func normalizeIssuer(iss string) string {
|
||||||
|
return strings.TrimRight(iss, "/")
|
||||||
|
}
|
||||||
|
|
||||||
|
func isTrustedIssuer(iss string, whitelist Whitelist, rs ...*http.Request) bool {
|
||||||
|
if "" == iss {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Normalize the http:// and https:// and parse
|
||||||
|
iss = strings.TrimRight(iss, "/") + "/"
|
||||||
|
if strings.HasPrefix(iss, "http://") {
|
||||||
|
// ignore
|
||||||
|
} else if strings.HasPrefix(iss, "//") {
|
||||||
|
return false // TODO
|
||||||
|
} else if !strings.HasPrefix(iss, "https://") {
|
||||||
|
iss = "https://" + iss
|
||||||
|
}
|
||||||
|
issURL, err := url.Parse(iss)
|
||||||
|
if nil != err {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check that
|
||||||
|
// * schemes match (https: == https:)
|
||||||
|
// * paths match (/foo/ == /foo/, always with trailing slash added)
|
||||||
|
// * hostnames are compatible (a == b or "sub.foo.com".HasSufix(".foo.com"))
|
||||||
|
for i := range []*url.URL(whitelist) {
|
||||||
|
u := whitelist[i]
|
||||||
|
|
||||||
|
if issURL.Scheme != u.Scheme {
|
||||||
|
continue
|
||||||
|
} else if u.Path != strings.TrimRight(issURL.Path, "/")+"/" {
|
||||||
|
continue
|
||||||
|
} else if issURL.Host != u.Host {
|
||||||
|
if '.' == u.Host[0] && strings.HasSuffix(issURL.Host, u.Host) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
// All failures have been handled
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if implicit issuer is available
|
||||||
|
if 0 == len(rs) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return hasImplicitTrust(issURL, rs[0])
|
||||||
|
}
|
||||||
|
|
||||||
|
// hasImplicitTrust relies on the security of DNS and TLS to determine if the
|
||||||
|
// headers of the request can be trusted as identifying the server itself as
|
||||||
|
// a valid issuer, without additional configuration.
|
||||||
|
//
|
||||||
|
// Helpful for testing, but in the wrong hands could easily lead to a zero-day.
|
||||||
|
func hasImplicitTrust(issURL *url.URL, r *http.Request) bool {
|
||||||
|
if nil == r {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sanity check that, if a load balancer exists, it isn't misconfigured
|
||||||
|
proto := r.Header.Get("X-Forwarded-Proto")
|
||||||
|
if "" != proto && proto != "https" {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the host
|
||||||
|
// * If TLS, block Domain Fronting
|
||||||
|
// * Otherwise assume trusted proxy
|
||||||
|
// * Otherwise assume test environment
|
||||||
|
var host string
|
||||||
|
if nil != r.TLS {
|
||||||
|
// Note that if this were to be implemented for HTTP/2 it would need to
|
||||||
|
// check all names on the certificate, not just the one with which the
|
||||||
|
// original connection was established. However, not our problem here.
|
||||||
|
// See https://serverfault.com/a/908087/93930
|
||||||
|
if r.TLS.ServerName != r.Host {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
host = r.Host
|
||||||
|
} else {
|
||||||
|
host = r.Header.Get("X-Forwarded-Host")
|
||||||
|
if "" == host {
|
||||||
|
host = r.Host
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Same tests as above, adjusted since it can't handle wildcards and, since
|
||||||
|
// the path is variable, we make the assumption that a child can trust a
|
||||||
|
// parent, but that a parent cannot trust a child.
|
||||||
|
if r.Host != issURL.Host {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if !strings.HasPrefix(strings.TrimRight(r.URL.Path, "/")+"/", issURL.Path) {
|
||||||
|
// Ex: Request URL Token Issuer
|
||||||
|
// !"https:example.com/johndoe/api/dothing".HasPrefix("https:example.com/")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Whitelist is a newtype for an array of URLs
|
||||||
|
type Whitelist []*url.URL
|
||||||
|
|
||||||
|
// NewWhitelist turns an array of URLs (such as https://example.com/) into
|
||||||
|
// a parsed array of *url.URLs that can be used by the IsTrustedIssuer function
|
||||||
|
func NewWhitelist(issuers []string, privateList ...[]string) (Whitelist, error) {
|
||||||
|
var err error
|
||||||
|
|
||||||
|
list := []*url.URL{}
|
||||||
|
if 0 != len(issuers) {
|
||||||
|
insecure := false
|
||||||
|
list, err = newWhitelist(list, issuers, insecure)
|
||||||
|
if nil != err {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if 0 != len(privateList) && 0 != len(privateList[0]) {
|
||||||
|
insecure := true
|
||||||
|
list, err = newWhitelist(list, privateList[0], insecure)
|
||||||
|
if nil != err {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Whitelist(list), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func newWhitelist(list []*url.URL, issuers []string, insecure bool) (Whitelist, error) {
|
||||||
|
for i := range issuers {
|
||||||
|
iss := issuers[i]
|
||||||
|
if "" == strings.TrimSpace(iss) {
|
||||||
|
fmt.Println("[Warning] You have an empty string in your keyfetch whitelist.")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Should have a valid http or https prefix
|
||||||
|
// TODO support custom prefixes (i.e. app://) ?
|
||||||
|
if strings.HasPrefix(iss, "http://") {
|
||||||
|
if !insecure {
|
||||||
|
log.Println("Oops! You have an insecure domain in your whitelist: ", iss)
|
||||||
|
return nil, ErrInsecureDomain
|
||||||
|
}
|
||||||
|
} else if strings.HasPrefix(iss, "//") {
|
||||||
|
// TODO
|
||||||
|
return nil, errors.New("Rather than prefixing with // to support multiple protocols, add them seperately:" + iss)
|
||||||
|
} else if !strings.HasPrefix(iss, "https://") {
|
||||||
|
iss = "https://" + iss
|
||||||
|
}
|
||||||
|
|
||||||
|
// trailing slash as a boundary character, which may or may not denote a directory
|
||||||
|
iss = strings.TrimRight(iss, "/") + "/"
|
||||||
|
u, err := url.Parse(iss)
|
||||||
|
if nil != err {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Strip any * prefix, for easier comparison later
|
||||||
|
// *.example.com => .example.com
|
||||||
|
if strings.HasPrefix(u.Host, "*.") {
|
||||||
|
u.Host = u.Host[1:]
|
||||||
|
}
|
||||||
|
|
||||||
|
list = append(list, u)
|
||||||
|
}
|
||||||
|
|
||||||
|
return list, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
IsTrustedIssuer returns true when the `iss` (i.e. from a token) matches one
|
||||||
|
in the provided whitelist (also matches wildcard domains).
|
||||||
|
|
||||||
|
You may explicitly allow insecure http (i.e. for automated testing) by
|
||||||
|
including http:// Otherwise the scheme in each item of the whitelist should
|
||||||
|
include the "https://" prefix.
|
||||||
|
|
||||||
|
SECURITY CONSIDERATIONS (Please Read)
|
||||||
|
|
||||||
|
You'll notice that *http.Request is optional. It should only be used under these
|
||||||
|
three circumstances:
|
||||||
|
|
||||||
|
1) Something else guarantees http -> https redirection happens before the
|
||||||
|
connection gets here AND this server directly handles TLS/SSL.
|
||||||
|
|
||||||
|
2) If you're using a load balancer or web server, and this doesn't handle
|
||||||
|
TLS/SSL directly, that server is _explicitly_ configured to protect
|
||||||
|
against Domain Fronting attacks. As of 2019, most web servers and load
|
||||||
|
balancers do not protect against that by default.
|
||||||
|
|
||||||
|
3) If you only use it to make your automated integration testing more
|
||||||
|
and it isn't enabled in production.
|
||||||
|
|
||||||
|
Otherwise, DO NOT pass in *http.Request as you will introduce a 0-day
|
||||||
|
vulnerability allowing an attacker to spoof any token issuer of their choice.
|
||||||
|
The only reason I allowed this in a public library where non-experts would
|
||||||
|
encounter it is to make testing easier.
|
||||||
|
*/
|
||||||
|
func (w Whitelist) IsTrustedIssuer(iss string, rs ...*http.Request) bool {
|
||||||
|
return isTrustedIssuer(iss, w, rs...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// String will generate a space-delimited list of whitelisted URLs
|
||||||
|
func (w Whitelist) String() string {
|
||||||
|
s := []string{}
|
||||||
|
for i := range w {
|
||||||
|
s = append(s, w[i].String())
|
||||||
|
}
|
||||||
|
return strings.Join(s, " ")
|
||||||
|
}
|
183
vendor/git.rootprojects.org/root/keypairs/keyfetch/uncached/fetch.go
generated
vendored
Normal file
183
vendor/git.rootprojects.org/root/keypairs/keyfetch/uncached/fetch.go
generated
vendored
Normal file
@ -0,0 +1,183 @@
|
|||||||
|
// Package uncached provides uncached versions of go-keypairs/keyfetch
|
||||||
|
package uncached
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"io"
|
||||||
|
"io/ioutil"
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"git.rootprojects.org/root/keypairs"
|
||||||
|
)
|
||||||
|
|
||||||
|
// OIDCJWKs gets the OpenID Connect configuration from the baseURL and then calls JWKs with the specified jwks_uri
|
||||||
|
func OIDCJWKs(baseURL string) (map[string]map[string]string, map[string]keypairs.PublicKey, error) {
|
||||||
|
baseURL = normalizeBaseURL(baseURL)
|
||||||
|
oidcConf := struct {
|
||||||
|
JWKSURI string `json:"jwks_uri"`
|
||||||
|
}{}
|
||||||
|
|
||||||
|
// must come in as https://<domain>/
|
||||||
|
url := baseURL + ".well-known/openid-configuration"
|
||||||
|
err := safeFetch(url, func(body io.Reader) error {
|
||||||
|
decoder := json.NewDecoder(body)
|
||||||
|
decoder.UseNumber()
|
||||||
|
return decoder.Decode(&oidcConf)
|
||||||
|
})
|
||||||
|
if nil != err {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return JWKs(oidcConf.JWKSURI)
|
||||||
|
}
|
||||||
|
|
||||||
|
// WellKnownJWKs calls JWKs with baseURL + /.well-known/jwks.json as constructs the jwks_uri
|
||||||
|
func WellKnownJWKs(baseURL string) (map[string]map[string]string, map[string]keypairs.PublicKey, error) {
|
||||||
|
baseURL = normalizeBaseURL(baseURL)
|
||||||
|
url := baseURL + ".well-known/jwks.json"
|
||||||
|
|
||||||
|
return JWKs(url)
|
||||||
|
}
|
||||||
|
|
||||||
|
// JWKs fetches and parses a jwks.json (assuming well-known format)
|
||||||
|
func JWKs(jwksurl string) (map[string]map[string]string, map[string]keypairs.PublicKey, error) {
|
||||||
|
keys := map[string]keypairs.PublicKey{}
|
||||||
|
maps := map[string]map[string]string{}
|
||||||
|
resp := struct {
|
||||||
|
Keys []map[string]interface{} `json:"keys"`
|
||||||
|
}{
|
||||||
|
Keys: make([]map[string]interface{}, 0, 1),
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := safeFetch(jwksurl, func(body io.Reader) error {
|
||||||
|
decoder := json.NewDecoder(body)
|
||||||
|
decoder.UseNumber()
|
||||||
|
return decoder.Decode(&resp)
|
||||||
|
}); nil != err {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := range resp.Keys {
|
||||||
|
k := resp.Keys[i]
|
||||||
|
m := getStringMap(k)
|
||||||
|
|
||||||
|
key, err := keypairs.NewJWKPublicKey(m)
|
||||||
|
|
||||||
|
if nil != err {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
keys[key.Thumbprint()] = key
|
||||||
|
maps[key.Thumbprint()] = m
|
||||||
|
}
|
||||||
|
|
||||||
|
return maps, keys, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// PEM fetches and parses a PEM (assuming well-known format)
|
||||||
|
func PEM(pemurl string) (map[string]string, keypairs.PublicKey, error) {
|
||||||
|
var pub keypairs.PublicKey
|
||||||
|
if err := safeFetch(pemurl, func(body io.Reader) error {
|
||||||
|
pem, err := ioutil.ReadAll(body)
|
||||||
|
if nil != err {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
pub, err = keypairs.ParsePublicKey(pem)
|
||||||
|
return err
|
||||||
|
}); nil != err {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
jwk := map[string]interface{}{}
|
||||||
|
body := bytes.NewBuffer(keypairs.MarshalJWKPublicKey(pub))
|
||||||
|
decoder := json.NewDecoder(body)
|
||||||
|
decoder.UseNumber()
|
||||||
|
_ = decoder.Decode(&jwk)
|
||||||
|
|
||||||
|
m := getStringMap(jwk)
|
||||||
|
m["kid"] = pemurl
|
||||||
|
|
||||||
|
switch p := pub.(type) {
|
||||||
|
case *keypairs.ECPublicKey:
|
||||||
|
p.KID = pemurl
|
||||||
|
case *keypairs.RSAPublicKey:
|
||||||
|
p.KID = pemurl
|
||||||
|
default:
|
||||||
|
return nil, nil, errors.New("impossible key type")
|
||||||
|
}
|
||||||
|
|
||||||
|
return m, pub, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch retrieves a single JWK (plain, bare jwk) from a URL (off-spec)
|
||||||
|
func Fetch(url string) (map[string]string, keypairs.PublicKey, error) {
|
||||||
|
var m map[string]interface{}
|
||||||
|
if err := safeFetch(url, func(body io.Reader) error {
|
||||||
|
decoder := json.NewDecoder(body)
|
||||||
|
decoder.UseNumber()
|
||||||
|
return decoder.Decode(&m)
|
||||||
|
}); nil != err {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
n := getStringMap(m)
|
||||||
|
key, err := keypairs.NewJWKPublicKey(n)
|
||||||
|
if nil != err {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return n, key, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func getStringMap(m map[string]interface{}) map[string]string {
|
||||||
|
n := make(map[string]string)
|
||||||
|
|
||||||
|
// TODO get issuer from x5c, if exists
|
||||||
|
|
||||||
|
// convert map[string]interface{} to map[string]string
|
||||||
|
for j := range m {
|
||||||
|
switch s := m[j].(type) {
|
||||||
|
case string:
|
||||||
|
n[j] = s
|
||||||
|
default:
|
||||||
|
// safely ignore
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return n
|
||||||
|
}
|
||||||
|
|
||||||
|
type decodeFunc func(io.Reader) error
|
||||||
|
|
||||||
|
// TODO: also limit the body size
|
||||||
|
func safeFetch(url string, decoder decodeFunc) error {
|
||||||
|
var netTransport = &http.Transport{
|
||||||
|
Dial: (&net.Dialer{
|
||||||
|
Timeout: 5 * time.Second,
|
||||||
|
}).Dial,
|
||||||
|
TLSHandshakeTimeout: 5 * time.Second,
|
||||||
|
}
|
||||||
|
var client = &http.Client{
|
||||||
|
Timeout: time.Second * 10,
|
||||||
|
Transport: netTransport,
|
||||||
|
}
|
||||||
|
|
||||||
|
req, err := http.NewRequest("GET", url, nil)
|
||||||
|
req.Header.Set("User-Agent", "go-keypairs/keyfetch")
|
||||||
|
req.Header.Set("Accept", "application/json;q=0.9,*/*;q=0.8")
|
||||||
|
res, err := client.Do(req)
|
||||||
|
if nil != err {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer res.Body.Close()
|
||||||
|
|
||||||
|
return decoder(res.Body)
|
||||||
|
}
|
||||||
|
|
||||||
|
func normalizeBaseURL(iss string) string {
|
||||||
|
return strings.TrimRight(iss, "/") + "/"
|
||||||
|
}
|
645
vendor/git.rootprojects.org/root/keypairs/keypairs.go
generated
vendored
Normal file
645
vendor/git.rootprojects.org/root/keypairs/keypairs.go
generated
vendored
Normal file
@ -0,0 +1,645 @@
|
|||||||
|
package keypairs
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"crypto"
|
||||||
|
"crypto/dsa"
|
||||||
|
"crypto/ecdsa"
|
||||||
|
"crypto/elliptic"
|
||||||
|
"crypto/rsa"
|
||||||
|
"crypto/sha256"
|
||||||
|
"crypto/x509"
|
||||||
|
"encoding/base64"
|
||||||
|
"encoding/json"
|
||||||
|
"encoding/pem"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"log"
|
||||||
|
"math/big"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ErrInvalidPrivateKey means that the key is not a valid Private Key
|
||||||
|
var ErrInvalidPrivateKey = errors.New("PrivateKey must be of type *rsa.PrivateKey or *ecdsa.PrivateKey")
|
||||||
|
|
||||||
|
// ErrInvalidPublicKey means that the key is not a valid Public Key
|
||||||
|
var ErrInvalidPublicKey = errors.New("PublicKey must be of type *rsa.PublicKey or *ecdsa.PublicKey")
|
||||||
|
|
||||||
|
// ErrParsePublicKey means that the bytes cannot be parsed in any known format
|
||||||
|
var ErrParsePublicKey = errors.New("PublicKey bytes could not be parsed as PEM or DER (PKIX/SPKI, PKCS1, or X509 Certificate) or JWK")
|
||||||
|
|
||||||
|
// ErrParsePrivateKey means that the bytes cannot be parsed in any known format
|
||||||
|
var ErrParsePrivateKey = errors.New("PrivateKey bytes could not be parsed as PEM or DER (PKCS8, SEC1, or PKCS1) or JWK")
|
||||||
|
|
||||||
|
// ErrParseJWK means that the JWK is valid JSON but not a valid JWK
|
||||||
|
var ErrParseJWK = errors.New("JWK is missing required base64-encoded JSON fields")
|
||||||
|
|
||||||
|
// ErrInvalidKeyType means that the key is not an acceptable type
|
||||||
|
var ErrInvalidKeyType = errors.New("The JWK's 'kty' must be either 'RSA' or 'EC'")
|
||||||
|
|
||||||
|
// ErrInvalidCurve means that a non-standard curve was used
|
||||||
|
var ErrInvalidCurve = errors.New("The JWK's 'crv' must be either of the NIST standards 'P-256' or 'P-384'")
|
||||||
|
|
||||||
|
// ErrUnexpectedPublicKey means that a Private Key was expected
|
||||||
|
var ErrUnexpectedPublicKey = errors.New("PrivateKey was given where PublicKey was expected")
|
||||||
|
|
||||||
|
// ErrUnexpectedPrivateKey means that a Public Key was expected
|
||||||
|
var ErrUnexpectedPrivateKey = errors.New("PublicKey was given where PrivateKey was expected")
|
||||||
|
|
||||||
|
// ErrDevSwapPrivatePublic means that the developer compiled bad code that swapped public and private keys
|
||||||
|
const ErrDevSwapPrivatePublic = "[Developer Error] You passed either crypto.PrivateKey or crypto.PublicKey where the other was expected."
|
||||||
|
|
||||||
|
// ErrDevBadKeyType means that the developer compiled bad code that passes the wrong type
|
||||||
|
const ErrDevBadKeyType = "[Developer Error] crypto.PublicKey and crypto.PrivateKey are somewhat deceptive. They're actually empty interfaces that accept any object, even non-crypto objects. You passed an object of type '%T' by mistake."
|
||||||
|
|
||||||
|
// PrivateKey is a zero-cost typesafe substitue for crypto.PrivateKey
|
||||||
|
type PrivateKey interface {
|
||||||
|
Public() crypto.PublicKey
|
||||||
|
}
|
||||||
|
|
||||||
|
// PublicKey thinly veils crypto.PublicKey for type safety
|
||||||
|
type PublicKey interface {
|
||||||
|
crypto.PublicKey
|
||||||
|
Thumbprint() string
|
||||||
|
KeyID() string
|
||||||
|
Key() crypto.PublicKey
|
||||||
|
ExpiresAt() time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
// ECPublicKey adds common methods to *ecdsa.PublicKey for type safety
|
||||||
|
type ECPublicKey struct {
|
||||||
|
PublicKey *ecdsa.PublicKey // empty interface
|
||||||
|
KID string
|
||||||
|
Expiry time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
// RSAPublicKey adds common methods to *rsa.PublicKey for type safety
|
||||||
|
type RSAPublicKey struct {
|
||||||
|
PublicKey *rsa.PublicKey // empty interface
|
||||||
|
KID string
|
||||||
|
Expiry time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
// Thumbprint returns a JWK thumbprint. See https://stackoverflow.com/questions/42588786/how-to-fingerprint-a-jwk
|
||||||
|
func (p *ECPublicKey) Thumbprint() string {
|
||||||
|
return ThumbprintUntypedPublicKey(p.PublicKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
// KeyID returns the JWK `kid`, which will be the Thumbprint for keys generated with this library
|
||||||
|
func (p *ECPublicKey) KeyID() string {
|
||||||
|
return p.KID
|
||||||
|
}
|
||||||
|
|
||||||
|
// Key returns the PublicKey
|
||||||
|
func (p *ECPublicKey) Key() crypto.PublicKey {
|
||||||
|
return p.PublicKey
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExpireAt sets the time at which this Public Key should be considered invalid
|
||||||
|
func (p *ECPublicKey) ExpireAt(t time.Time) {
|
||||||
|
p.Expiry = t
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExpiresAt gets the time at which this Public Key should be considered invalid
|
||||||
|
func (p *ECPublicKey) ExpiresAt() time.Time {
|
||||||
|
return p.Expiry
|
||||||
|
}
|
||||||
|
|
||||||
|
// Thumbprint returns a JWK thumbprint. See https://stackoverflow.com/questions/42588786/how-to-fingerprint-a-jwk
|
||||||
|
func (p *RSAPublicKey) Thumbprint() string {
|
||||||
|
return ThumbprintUntypedPublicKey(p.PublicKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
// KeyID returns the JWK `kid`, which will be the Thumbprint for keys generated with this library
|
||||||
|
func (p *RSAPublicKey) KeyID() string {
|
||||||
|
return p.KID
|
||||||
|
}
|
||||||
|
|
||||||
|
// Key returns the PublicKey
|
||||||
|
func (p *RSAPublicKey) Key() crypto.PublicKey {
|
||||||
|
return p.PublicKey
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExpireAt sets the time at which this Public Key should be considered invalid
|
||||||
|
func (p *RSAPublicKey) ExpireAt(t time.Time) {
|
||||||
|
p.Expiry = t
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExpiresAt gets the time at which this Public Key should be considered invalid
|
||||||
|
func (p *RSAPublicKey) ExpiresAt() time.Time {
|
||||||
|
return p.Expiry
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewPublicKey wraps a crypto.PublicKey to make it typesafe.
|
||||||
|
func NewPublicKey(pub crypto.PublicKey, kid ...string) PublicKey {
|
||||||
|
var k PublicKey
|
||||||
|
switch p := pub.(type) {
|
||||||
|
case *ecdsa.PublicKey:
|
||||||
|
eckey := &ECPublicKey{
|
||||||
|
PublicKey: p,
|
||||||
|
}
|
||||||
|
if 0 != len(kid) {
|
||||||
|
eckey.KID = kid[0]
|
||||||
|
} else {
|
||||||
|
eckey.KID = ThumbprintECPublicKey(p)
|
||||||
|
}
|
||||||
|
k = eckey
|
||||||
|
case *rsa.PublicKey:
|
||||||
|
rsakey := &RSAPublicKey{
|
||||||
|
PublicKey: p,
|
||||||
|
}
|
||||||
|
if 0 != len(kid) {
|
||||||
|
rsakey.KID = kid[0]
|
||||||
|
} else {
|
||||||
|
rsakey.KID = ThumbprintRSAPublicKey(p)
|
||||||
|
}
|
||||||
|
k = rsakey
|
||||||
|
case *ecdsa.PrivateKey:
|
||||||
|
panic(errors.New(ErrDevSwapPrivatePublic))
|
||||||
|
case *rsa.PrivateKey:
|
||||||
|
panic(errors.New(ErrDevSwapPrivatePublic))
|
||||||
|
case *dsa.PublicKey:
|
||||||
|
panic(ErrInvalidPublicKey)
|
||||||
|
case *dsa.PrivateKey:
|
||||||
|
panic(ErrInvalidPrivateKey)
|
||||||
|
default:
|
||||||
|
panic(fmt.Errorf(ErrDevBadKeyType, pub))
|
||||||
|
}
|
||||||
|
|
||||||
|
return k
|
||||||
|
}
|
||||||
|
|
||||||
|
// MarshalJWKPublicKey outputs a JWK with its key id (kid) and an optional expiration,
|
||||||
|
// making it suitable for use as an OIDC public key.
|
||||||
|
func MarshalJWKPublicKey(key PublicKey, exp ...time.Time) []byte {
|
||||||
|
// thumbprint keys are alphabetically sorted and only include the necessary public parts
|
||||||
|
switch k := key.Key().(type) {
|
||||||
|
case *rsa.PublicKey:
|
||||||
|
return MarshalRSAPublicKey(k, exp...)
|
||||||
|
case *ecdsa.PublicKey:
|
||||||
|
return MarshalECPublicKey(k, exp...)
|
||||||
|
case *dsa.PublicKey:
|
||||||
|
panic(ErrInvalidPublicKey)
|
||||||
|
default:
|
||||||
|
// this is unreachable because we know the types that we pass in
|
||||||
|
log.Printf("keytype: %t, %+v\n", key, key)
|
||||||
|
panic(ErrInvalidPublicKey)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ThumbprintPublicKey returns the SHA256 RFC-spec JWK thumbprint
|
||||||
|
func ThumbprintPublicKey(pub PublicKey) string {
|
||||||
|
return ThumbprintUntypedPublicKey(pub.Key())
|
||||||
|
}
|
||||||
|
|
||||||
|
// ThumbprintUntypedPublicKey is a non-typesafe version of ThumbprintPublicKey
|
||||||
|
// (but will still panic, to help you discover bugs in development rather than production).
|
||||||
|
func ThumbprintUntypedPublicKey(pub crypto.PublicKey) string {
|
||||||
|
switch p := pub.(type) {
|
||||||
|
case PublicKey:
|
||||||
|
return ThumbprintUntypedPublicKey(p.Key())
|
||||||
|
case *ecdsa.PublicKey:
|
||||||
|
return ThumbprintECPublicKey(p)
|
||||||
|
case *rsa.PublicKey:
|
||||||
|
return ThumbprintRSAPublicKey(p)
|
||||||
|
default:
|
||||||
|
panic(ErrInvalidPublicKey)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MarshalECPublicKey will take an EC key and output a JWK, with optional expiration date
|
||||||
|
func MarshalECPublicKey(k *ecdsa.PublicKey, exp ...time.Time) []byte {
|
||||||
|
thumb := ThumbprintECPublicKey(k)
|
||||||
|
crv := k.Curve.Params().Name
|
||||||
|
x := base64.RawURLEncoding.EncodeToString(k.X.Bytes())
|
||||||
|
y := base64.RawURLEncoding.EncodeToString(k.Y.Bytes())
|
||||||
|
expstr := ""
|
||||||
|
if 0 != len(exp) {
|
||||||
|
expstr = fmt.Sprintf(`"exp":%d,`, exp[0].Unix())
|
||||||
|
}
|
||||||
|
return []byte(fmt.Sprintf(`{"kid":%q,"use":"sig",%s"crv":%q,"kty":"EC","x":%q,"y":%q}`, thumb, expstr, crv, x, y))
|
||||||
|
}
|
||||||
|
|
||||||
|
// MarshalECPublicKeyWithoutKeyID will output the most minimal version of an EC JWK (no key id, no "use" flag, nada)
|
||||||
|
func MarshalECPublicKeyWithoutKeyID(k *ecdsa.PublicKey) []byte {
|
||||||
|
crv := k.Curve.Params().Name
|
||||||
|
x := base64.RawURLEncoding.EncodeToString(k.X.Bytes())
|
||||||
|
y := base64.RawURLEncoding.EncodeToString(k.Y.Bytes())
|
||||||
|
return []byte(fmt.Sprintf(`{"crv":%q,"kty":"EC","x":%q,"y":%q}`, crv, x, y))
|
||||||
|
}
|
||||||
|
|
||||||
|
// ThumbprintECPublicKey will output a RFC-spec SHA256 JWK thumbprint of an EC public key
|
||||||
|
func ThumbprintECPublicKey(k *ecdsa.PublicKey) string {
|
||||||
|
thumbprintable := MarshalECPublicKeyWithoutKeyID(k)
|
||||||
|
sha := sha256.Sum256(thumbprintable)
|
||||||
|
return base64.RawURLEncoding.EncodeToString(sha[:])
|
||||||
|
}
|
||||||
|
|
||||||
|
// MarshalRSAPublicKey will take an RSA key and output a JWK, with optional expiration date
|
||||||
|
func MarshalRSAPublicKey(p *rsa.PublicKey, exp ...time.Time) []byte {
|
||||||
|
thumb := ThumbprintRSAPublicKey(p)
|
||||||
|
e := base64.RawURLEncoding.EncodeToString(big.NewInt(int64(p.E)).Bytes())
|
||||||
|
n := base64.RawURLEncoding.EncodeToString(p.N.Bytes())
|
||||||
|
expstr := ""
|
||||||
|
if 0 != len(exp) {
|
||||||
|
expstr = fmt.Sprintf(`"exp":%d,`, exp[0].Unix())
|
||||||
|
}
|
||||||
|
return []byte(fmt.Sprintf(`{"kid":%q,"use":"sig",%s"e":%q,"kty":"RSA","n":%q}`, thumb, expstr, e, n))
|
||||||
|
}
|
||||||
|
|
||||||
|
// MarshalRSAPublicKeyWithoutKeyID will output the most minimal version of an RSA JWK (no key id, no "use" flag, nada)
|
||||||
|
func MarshalRSAPublicKeyWithoutKeyID(p *rsa.PublicKey) []byte {
|
||||||
|
e := base64.RawURLEncoding.EncodeToString(big.NewInt(int64(p.E)).Bytes())
|
||||||
|
n := base64.RawURLEncoding.EncodeToString(p.N.Bytes())
|
||||||
|
return []byte(fmt.Sprintf(`{"e":%q,"kty":"RSA","n":%q}`, e, n))
|
||||||
|
}
|
||||||
|
|
||||||
|
// ThumbprintRSAPublicKey will output a RFC-spec SHA256 JWK thumbprint of an EC public key
|
||||||
|
func ThumbprintRSAPublicKey(p *rsa.PublicKey) string {
|
||||||
|
thumbprintable := MarshalRSAPublicKeyWithoutKeyID(p)
|
||||||
|
sha := sha256.Sum256([]byte(thumbprintable))
|
||||||
|
return base64.RawURLEncoding.EncodeToString(sha[:])
|
||||||
|
}
|
||||||
|
|
||||||
|
// ParsePrivateKey will try to parse the bytes you give it
|
||||||
|
// in any of the supported formats: PEM, DER, PKCS8, PKCS1, SEC1, and JWK
|
||||||
|
func ParsePrivateKey(block []byte) (PrivateKey, error) {
|
||||||
|
blocks, err := getPEMBytes(block)
|
||||||
|
if nil != err {
|
||||||
|
return nil, ErrParsePrivateKey
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse PEM blocks (openssl generates junk metadata blocks for ECs)
|
||||||
|
// or the original DER, or the JWK
|
||||||
|
for i := range blocks {
|
||||||
|
block = blocks[i]
|
||||||
|
if key, err := parsePrivateKey(block); nil == err {
|
||||||
|
return key, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := range blocks {
|
||||||
|
block = blocks[i]
|
||||||
|
if _, err := parsePublicKey(block); nil == err {
|
||||||
|
return nil, ErrUnexpectedPublicKey
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we didn't parse a key arleady, we failed
|
||||||
|
return nil, ErrParsePrivateKey
|
||||||
|
}
|
||||||
|
|
||||||
|
// ParsePrivateKeyString calls ParsePrivateKey([]byte(key)) for all you lazy folk.
|
||||||
|
func ParsePrivateKeyString(block string) (PrivateKey, error) {
|
||||||
|
return ParsePrivateKey([]byte(block))
|
||||||
|
}
|
||||||
|
|
||||||
|
func parsePrivateKey(der []byte) (PrivateKey, error) {
|
||||||
|
var key PrivateKey
|
||||||
|
|
||||||
|
//fmt.Println("1. ParsePKCS8PrivateKey")
|
||||||
|
xkey, err := x509.ParsePKCS8PrivateKey(der)
|
||||||
|
if nil == err {
|
||||||
|
switch k := xkey.(type) {
|
||||||
|
case *rsa.PrivateKey:
|
||||||
|
key = k
|
||||||
|
case *ecdsa.PrivateKey:
|
||||||
|
key = k
|
||||||
|
default:
|
||||||
|
err = errors.New("Only RSA and ECDSA (EC) Private Keys are supported")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if nil != err {
|
||||||
|
//fmt.Println("2. ParseECPrivateKey")
|
||||||
|
key, err = x509.ParseECPrivateKey(der)
|
||||||
|
if nil != err {
|
||||||
|
//fmt.Println("3. ParsePKCS1PrivateKey")
|
||||||
|
key, err = x509.ParsePKCS1PrivateKey(der)
|
||||||
|
if nil != err {
|
||||||
|
//fmt.Println("4. ParseJWKPrivateKey")
|
||||||
|
key, err = ParseJWKPrivateKey(der)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// But did you know?
|
||||||
|
// You must return nil explicitly for interfaces
|
||||||
|
// https://golang.org/doc/faq#nil_error
|
||||||
|
if nil != err {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return key, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func getPEMBytes(block []byte) ([][]byte, error) {
|
||||||
|
var pemblock *pem.Block
|
||||||
|
var blocks = make([][]byte, 0, 1)
|
||||||
|
|
||||||
|
// Parse the PEM, if it's a pem
|
||||||
|
for {
|
||||||
|
pemblock, block = pem.Decode(block)
|
||||||
|
if nil != pemblock {
|
||||||
|
// got one block, there may be more
|
||||||
|
blocks = append(blocks, pemblock.Bytes)
|
||||||
|
} else {
|
||||||
|
// the last block was not a PEM block
|
||||||
|
// therefore the next isn't either
|
||||||
|
if 0 != len(block) {
|
||||||
|
blocks = append(blocks, block)
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(blocks) > 0 {
|
||||||
|
return blocks, nil
|
||||||
|
}
|
||||||
|
return nil, errors.New("no PEM blocks found")
|
||||||
|
}
|
||||||
|
|
||||||
|
// ParsePublicKey will try to parse the bytes you give it
|
||||||
|
// in any of the supported formats: PEM, DER, PKIX/SPKI, PKCS1, x509 Certificate, and JWK
|
||||||
|
func ParsePublicKey(block []byte) (PublicKey, error) {
|
||||||
|
blocks, err := getPEMBytes(block)
|
||||||
|
if nil != err {
|
||||||
|
return nil, ErrParsePublicKey
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse PEM blocks (openssl generates junk metadata blocks for ECs)
|
||||||
|
// or the original DER, or the JWK
|
||||||
|
for i := range blocks {
|
||||||
|
block = blocks[i]
|
||||||
|
if key, err := parsePublicKey(block); nil == err {
|
||||||
|
return key, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := range blocks {
|
||||||
|
block = blocks[i]
|
||||||
|
if _, err := parsePrivateKey(block); nil == err {
|
||||||
|
return nil, ErrUnexpectedPrivateKey
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we didn't parse a key arleady, we failed
|
||||||
|
return nil, ErrParsePublicKey
|
||||||
|
}
|
||||||
|
|
||||||
|
// ParsePublicKeyString calls ParsePublicKey([]byte(key)) for all you lazy folk.
|
||||||
|
func ParsePublicKeyString(block string) (PublicKey, error) {
|
||||||
|
return ParsePublicKey([]byte(block))
|
||||||
|
}
|
||||||
|
|
||||||
|
func parsePublicKey(der []byte) (PublicKey, error) {
|
||||||
|
cert, err := x509.ParseCertificate(der)
|
||||||
|
if nil == err {
|
||||||
|
switch k := cert.PublicKey.(type) {
|
||||||
|
case *rsa.PublicKey:
|
||||||
|
return NewPublicKey(k), nil
|
||||||
|
case *ecdsa.PublicKey:
|
||||||
|
return NewPublicKey(k), nil
|
||||||
|
default:
|
||||||
|
return nil, errors.New("Only RSA and ECDSA (EC) Public Keys are supported")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//fmt.Println("1. ParsePKIXPublicKey")
|
||||||
|
xkey, err := x509.ParsePKIXPublicKey(der)
|
||||||
|
if nil == err {
|
||||||
|
switch k := xkey.(type) {
|
||||||
|
case *rsa.PublicKey:
|
||||||
|
return NewPublicKey(k), nil
|
||||||
|
case *ecdsa.PublicKey:
|
||||||
|
return NewPublicKey(k), nil
|
||||||
|
default:
|
||||||
|
return nil, errors.New("Only RSA and ECDSA (EC) Public Keys are supported")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//fmt.Println("3. ParsePKCS1PrublicKey")
|
||||||
|
rkey, err := x509.ParsePKCS1PublicKey(der)
|
||||||
|
if nil == err {
|
||||||
|
//fmt.Println("4. ParseJWKPublicKey")
|
||||||
|
return NewPublicKey(rkey), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return ParseJWKPublicKey(der)
|
||||||
|
|
||||||
|
/*
|
||||||
|
// But did you know?
|
||||||
|
// You must return nil explicitly for interfaces
|
||||||
|
// https://golang.org/doc/faq#nil_error
|
||||||
|
if nil != err {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewJWKPublicKey contstructs a PublicKey from the relevant pieces a map[string]string (generic JSON)
|
||||||
|
func NewJWKPublicKey(m map[string]string) (PublicKey, error) {
|
||||||
|
switch m["kty"] {
|
||||||
|
case "RSA":
|
||||||
|
return parseRSAPublicKey(m)
|
||||||
|
case "EC":
|
||||||
|
return parseECPublicKey(m)
|
||||||
|
default:
|
||||||
|
return nil, ErrInvalidKeyType
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ParseJWKPublicKey parses a JSON-encoded JWK and returns a PublicKey, or a (hopefully) helpful error message
|
||||||
|
func ParseJWKPublicKey(b []byte) (PublicKey, error) {
|
||||||
|
// RSA and EC have "d" as a private part
|
||||||
|
if bytes.Contains(b, []byte(`"d"`)) {
|
||||||
|
return nil, ErrUnexpectedPrivateKey
|
||||||
|
}
|
||||||
|
return newJWKPublicKey(b)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ParseJWKPublicKeyString calls ParseJWKPublicKey([]byte(key)) for all you lazy folk.
|
||||||
|
func ParseJWKPublicKeyString(s string) (PublicKey, error) {
|
||||||
|
if strings.Contains(s, `"d"`) {
|
||||||
|
return nil, ErrUnexpectedPrivateKey
|
||||||
|
}
|
||||||
|
return newJWKPublicKey(s)
|
||||||
|
}
|
||||||
|
|
||||||
|
// DecodeJWKPublicKey stream-decodes a JSON-encoded JWK and returns a PublicKey, or a (hopefully) helpful error message
|
||||||
|
func DecodeJWKPublicKey(r io.Reader) (PublicKey, error) {
|
||||||
|
m := make(map[string]string)
|
||||||
|
if err := json.NewDecoder(r).Decode(&m); nil != err {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if d := m["d"]; "" != d {
|
||||||
|
return nil, ErrUnexpectedPrivateKey
|
||||||
|
}
|
||||||
|
return newJWKPublicKey(m)
|
||||||
|
}
|
||||||
|
|
||||||
|
// the underpinnings of the parser as used by the typesafe wrappers
|
||||||
|
func newJWKPublicKey(data interface{}) (PublicKey, error) {
|
||||||
|
var m map[string]string
|
||||||
|
|
||||||
|
switch d := data.(type) {
|
||||||
|
case map[string]string:
|
||||||
|
m = d
|
||||||
|
case string:
|
||||||
|
if err := json.Unmarshal([]byte(d), &m); nil != err {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
case []byte:
|
||||||
|
if err := json.Unmarshal(d, &m); nil != err {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
panic("Developer Error: unsupported interface type")
|
||||||
|
}
|
||||||
|
|
||||||
|
return NewJWKPublicKey(m)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ParseJWKPrivateKey parses a JSON-encoded JWK and returns a PrivateKey, or a (hopefully) helpful error message
|
||||||
|
func ParseJWKPrivateKey(b []byte) (PrivateKey, error) {
|
||||||
|
var m map[string]string
|
||||||
|
if err := json.Unmarshal(b, &m); nil != err {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
switch m["kty"] {
|
||||||
|
case "RSA":
|
||||||
|
return parseRSAPrivateKey(m)
|
||||||
|
case "EC":
|
||||||
|
return parseECPrivateKey(m)
|
||||||
|
default:
|
||||||
|
return nil, ErrInvalidKeyType
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseRSAPublicKey(m map[string]string) (*RSAPublicKey, error) {
|
||||||
|
// TODO grab expiry?
|
||||||
|
kid, _ := m["kid"]
|
||||||
|
n, _ := base64.RawURLEncoding.DecodeString(m["n"])
|
||||||
|
e, _ := base64.RawURLEncoding.DecodeString(m["e"])
|
||||||
|
if 0 == len(n) || 0 == len(e) {
|
||||||
|
return nil, ErrParseJWK
|
||||||
|
}
|
||||||
|
ni := &big.Int{}
|
||||||
|
ni.SetBytes(n)
|
||||||
|
ei := &big.Int{}
|
||||||
|
ei.SetBytes(e)
|
||||||
|
|
||||||
|
pub := &rsa.PublicKey{
|
||||||
|
N: ni,
|
||||||
|
E: int(ei.Int64()),
|
||||||
|
}
|
||||||
|
|
||||||
|
return &RSAPublicKey{
|
||||||
|
PublicKey: pub,
|
||||||
|
KID: kid,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseRSAPrivateKey(m map[string]string) (key *rsa.PrivateKey, err error) {
|
||||||
|
pub, err := parseRSAPublicKey(m)
|
||||||
|
if nil != err {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
d, _ := base64.RawURLEncoding.DecodeString(m["d"])
|
||||||
|
p, _ := base64.RawURLEncoding.DecodeString(m["p"])
|
||||||
|
q, _ := base64.RawURLEncoding.DecodeString(m["q"])
|
||||||
|
dp, _ := base64.RawURLEncoding.DecodeString(m["dp"])
|
||||||
|
dq, _ := base64.RawURLEncoding.DecodeString(m["dq"])
|
||||||
|
qinv, _ := base64.RawURLEncoding.DecodeString(m["qi"])
|
||||||
|
if 0 == len(d) || 0 == len(p) || 0 == len(dp) || 0 == len(dq) || 0 == len(qinv) {
|
||||||
|
return nil, ErrParseJWK
|
||||||
|
}
|
||||||
|
|
||||||
|
di := &big.Int{}
|
||||||
|
di.SetBytes(d)
|
||||||
|
pi := &big.Int{}
|
||||||
|
pi.SetBytes(p)
|
||||||
|
qi := &big.Int{}
|
||||||
|
qi.SetBytes(q)
|
||||||
|
dpi := &big.Int{}
|
||||||
|
dpi.SetBytes(dp)
|
||||||
|
dqi := &big.Int{}
|
||||||
|
dqi.SetBytes(dq)
|
||||||
|
qinvi := &big.Int{}
|
||||||
|
qinvi.SetBytes(qinv)
|
||||||
|
|
||||||
|
key = &rsa.PrivateKey{
|
||||||
|
PublicKey: *pub.PublicKey,
|
||||||
|
D: di,
|
||||||
|
Primes: []*big.Int{pi, qi},
|
||||||
|
Precomputed: rsa.PrecomputedValues{
|
||||||
|
Dp: dpi,
|
||||||
|
Dq: dqi,
|
||||||
|
Qinv: qinvi,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseECPublicKey(m map[string]string) (*ECPublicKey, error) {
|
||||||
|
// TODO grab expiry?
|
||||||
|
kid, _ := m["kid"]
|
||||||
|
x, _ := base64.RawURLEncoding.DecodeString(m["x"])
|
||||||
|
y, _ := base64.RawURLEncoding.DecodeString(m["y"])
|
||||||
|
if 0 == len(x) || 0 == len(y) || 0 == len(m["crv"]) {
|
||||||
|
return nil, ErrParseJWK
|
||||||
|
}
|
||||||
|
|
||||||
|
xi := &big.Int{}
|
||||||
|
xi.SetBytes(x)
|
||||||
|
|
||||||
|
yi := &big.Int{}
|
||||||
|
yi.SetBytes(y)
|
||||||
|
|
||||||
|
var crv elliptic.Curve
|
||||||
|
switch m["crv"] {
|
||||||
|
case "P-256":
|
||||||
|
crv = elliptic.P256()
|
||||||
|
case "P-384":
|
||||||
|
crv = elliptic.P384()
|
||||||
|
case "P-521":
|
||||||
|
crv = elliptic.P521()
|
||||||
|
default:
|
||||||
|
return nil, ErrInvalidCurve
|
||||||
|
}
|
||||||
|
|
||||||
|
pub := &ecdsa.PublicKey{
|
||||||
|
Curve: crv,
|
||||||
|
X: xi,
|
||||||
|
Y: yi,
|
||||||
|
}
|
||||||
|
|
||||||
|
return &ECPublicKey{
|
||||||
|
PublicKey: pub,
|
||||||
|
KID: kid,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseECPrivateKey(m map[string]string) (*ecdsa.PrivateKey, error) {
|
||||||
|
pub, err := parseECPublicKey(m)
|
||||||
|
if nil != err {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
d, _ := base64.RawURLEncoding.DecodeString(m["d"])
|
||||||
|
if 0 == len(d) {
|
||||||
|
return nil, ErrParseJWK
|
||||||
|
}
|
||||||
|
di := &big.Int{}
|
||||||
|
di.SetBytes(d)
|
||||||
|
|
||||||
|
return &ecdsa.PrivateKey{
|
||||||
|
PublicKey: *pub.PublicKey,
|
||||||
|
D: di,
|
||||||
|
}, nil
|
||||||
|
}
|
171
vendor/git.rootprojects.org/root/keypairs/marshal.go
generated
vendored
Normal file
171
vendor/git.rootprojects.org/root/keypairs/marshal.go
generated
vendored
Normal file
@ -0,0 +1,171 @@
|
|||||||
|
package keypairs
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto"
|
||||||
|
"crypto/ecdsa"
|
||||||
|
"crypto/rsa"
|
||||||
|
"crypto/x509"
|
||||||
|
"encoding/base64"
|
||||||
|
"encoding/pem"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"math/big"
|
||||||
|
mathrand "math/rand"
|
||||||
|
)
|
||||||
|
|
||||||
|
// MarshalPEMPublicKey outputs the given public key as JWK
|
||||||
|
func MarshalPEMPublicKey(pubkey crypto.PublicKey) ([]byte, error) {
|
||||||
|
block, err := marshalDERPublicKey(pubkey)
|
||||||
|
if nil != err {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return pem.EncodeToMemory(block), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// MarshalDERPublicKey outputs the given public key as JWK
|
||||||
|
func MarshalDERPublicKey(pubkey crypto.PublicKey) ([]byte, error) {
|
||||||
|
block, err := marshalDERPublicKey(pubkey)
|
||||||
|
if nil != err {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return block.Bytes, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// marshalDERPublicKey outputs the given public key as JWK
|
||||||
|
func marshalDERPublicKey(pubkey crypto.PublicKey) (*pem.Block, error) {
|
||||||
|
|
||||||
|
var der []byte
|
||||||
|
var typ string
|
||||||
|
var err error
|
||||||
|
switch k := pubkey.(type) {
|
||||||
|
case *rsa.PublicKey:
|
||||||
|
der = x509.MarshalPKCS1PublicKey(k)
|
||||||
|
typ = "RSA PUBLIC KEY"
|
||||||
|
case *ecdsa.PublicKey:
|
||||||
|
typ = "PUBLIC KEY"
|
||||||
|
der, err = x509.MarshalPKIXPublicKey(k)
|
||||||
|
if nil != err {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
panic("Developer Error: impossible key type")
|
||||||
|
}
|
||||||
|
|
||||||
|
return &pem.Block{
|
||||||
|
Bytes: der,
|
||||||
|
Type: typ,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// MarshalJWKPrivateKey outputs the given private key as JWK
|
||||||
|
func MarshalJWKPrivateKey(privkey PrivateKey) []byte {
|
||||||
|
// thumbprint keys are alphabetically sorted and only include the necessary public parts
|
||||||
|
switch k := privkey.(type) {
|
||||||
|
case *rsa.PrivateKey:
|
||||||
|
return MarshalRSAPrivateKey(k)
|
||||||
|
case *ecdsa.PrivateKey:
|
||||||
|
return MarshalECPrivateKey(k)
|
||||||
|
default:
|
||||||
|
// this is unreachable because we know the types that we pass in
|
||||||
|
log.Printf("keytype: %t, %+v\n", privkey, privkey)
|
||||||
|
panic(ErrInvalidPublicKey)
|
||||||
|
//return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MarshalDERPrivateKey outputs the given private key as ASN.1 DER
|
||||||
|
func MarshalDERPrivateKey(privkey PrivateKey) ([]byte, error) {
|
||||||
|
// thumbprint keys are alphabetically sorted and only include the necessary public parts
|
||||||
|
switch k := privkey.(type) {
|
||||||
|
case *rsa.PrivateKey:
|
||||||
|
return x509.MarshalPKCS1PrivateKey(k), nil
|
||||||
|
case *ecdsa.PrivateKey:
|
||||||
|
return x509.MarshalECPrivateKey(k)
|
||||||
|
default:
|
||||||
|
// this is unreachable because we know the types that we pass in
|
||||||
|
log.Printf("keytype: %t, %+v\n", privkey, privkey)
|
||||||
|
panic(ErrInvalidPublicKey)
|
||||||
|
//return nil, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func marshalDERPrivateKey(privkey PrivateKey) (*pem.Block, error) {
|
||||||
|
var typ string
|
||||||
|
var bytes []byte
|
||||||
|
var err error
|
||||||
|
|
||||||
|
switch k := privkey.(type) {
|
||||||
|
case *rsa.PrivateKey:
|
||||||
|
if 0 == mathrand.Intn(2) {
|
||||||
|
typ = "PRIVATE KEY"
|
||||||
|
bytes, err = x509.MarshalPKCS8PrivateKey(k)
|
||||||
|
if nil != err {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
typ = "RSA PRIVATE KEY"
|
||||||
|
bytes = x509.MarshalPKCS1PrivateKey(k)
|
||||||
|
}
|
||||||
|
return &pem.Block{
|
||||||
|
Type: typ,
|
||||||
|
Bytes: bytes,
|
||||||
|
}, nil
|
||||||
|
case *ecdsa.PrivateKey:
|
||||||
|
if 0 == mathrand.Intn(2) {
|
||||||
|
typ = "PRIVATE KEY"
|
||||||
|
bytes, err = x509.MarshalPKCS8PrivateKey(k)
|
||||||
|
} else {
|
||||||
|
typ = "EC PRIVATE KEY"
|
||||||
|
bytes, err = x509.MarshalECPrivateKey(k)
|
||||||
|
}
|
||||||
|
if nil != err {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &pem.Block{
|
||||||
|
Type: typ,
|
||||||
|
Bytes: bytes,
|
||||||
|
}, nil
|
||||||
|
default:
|
||||||
|
// this is unreachable because we know the types that we pass in
|
||||||
|
log.Printf("keytype: %t, %+v\n", privkey, privkey)
|
||||||
|
panic(ErrInvalidPublicKey)
|
||||||
|
//return nil, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MarshalPEMPrivateKey outputs the given private key as ASN.1 PEM
|
||||||
|
func MarshalPEMPrivateKey(privkey PrivateKey) ([]byte, error) {
|
||||||
|
block, err := marshalDERPrivateKey(privkey)
|
||||||
|
if nil != err {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return pem.EncodeToMemory(block), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// MarshalECPrivateKey will output the given private key as JWK
|
||||||
|
func MarshalECPrivateKey(k *ecdsa.PrivateKey) []byte {
|
||||||
|
crv := k.Curve.Params().Name
|
||||||
|
d := base64.RawURLEncoding.EncodeToString(k.D.Bytes())
|
||||||
|
x := base64.RawURLEncoding.EncodeToString(k.X.Bytes())
|
||||||
|
y := base64.RawURLEncoding.EncodeToString(k.Y.Bytes())
|
||||||
|
return []byte(fmt.Sprintf(
|
||||||
|
`{"crv":%q,"d":%q,"kty":"EC","x":%q,"y":%q}`,
|
||||||
|
crv, d, x, y,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
// MarshalRSAPrivateKey will output the given private key as JWK
|
||||||
|
func MarshalRSAPrivateKey(pk *rsa.PrivateKey) []byte {
|
||||||
|
e := base64.RawURLEncoding.EncodeToString(big.NewInt(int64(pk.E)).Bytes())
|
||||||
|
n := base64.RawURLEncoding.EncodeToString(pk.N.Bytes())
|
||||||
|
d := base64.RawURLEncoding.EncodeToString(pk.D.Bytes())
|
||||||
|
p := base64.RawURLEncoding.EncodeToString(pk.Primes[0].Bytes())
|
||||||
|
q := base64.RawURLEncoding.EncodeToString(pk.Primes[1].Bytes())
|
||||||
|
dp := base64.RawURLEncoding.EncodeToString(pk.Precomputed.Dp.Bytes())
|
||||||
|
dq := base64.RawURLEncoding.EncodeToString(pk.Precomputed.Dq.Bytes())
|
||||||
|
qi := base64.RawURLEncoding.EncodeToString(pk.Precomputed.Qinv.Bytes())
|
||||||
|
return []byte(fmt.Sprintf(
|
||||||
|
`{"d":%q,"dp":%q,"dq":%q,"e":%q,"kty":"RSA","n":%q,"p":%q,"q":%q,"qi":%q}`,
|
||||||
|
d, dp, dq, e, n, p, q, qi,
|
||||||
|
))
|
||||||
|
}
|
46
vendor/git.rootprojects.org/root/keypairs/mock.go
generated
vendored
Normal file
46
vendor/git.rootprojects.org/root/keypairs/mock.go
generated
vendored
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
package keypairs
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/rsa"
|
||||||
|
"io"
|
||||||
|
"log"
|
||||||
|
mathrand "math/rand"
|
||||||
|
)
|
||||||
|
|
||||||
|
// this shananigans is only for testing and debug API stuff
|
||||||
|
func (o *keyOptions) maybeMockReader() io.Reader {
|
||||||
|
if !allowMocking {
|
||||||
|
panic("mock method called when mocking is not allowed")
|
||||||
|
}
|
||||||
|
|
||||||
|
if 0 == o.mockSeed {
|
||||||
|
return randReader
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Println("WARNING: MOCK: using insecure reader")
|
||||||
|
return mathrand.New(mathrand.NewSource(o.mockSeed))
|
||||||
|
}
|
||||||
|
|
||||||
|
const maxRetry = 16
|
||||||
|
|
||||||
|
func maybeDerandomizeMockKey(privkey PrivateKey, keylen int, opts *keyOptions) PrivateKey {
|
||||||
|
if 0 != opts.mockSeed {
|
||||||
|
for i := 0; i < maxRetry; i++ {
|
||||||
|
otherkey, _ := rsa.GenerateKey(opts.nextReader(), keylen)
|
||||||
|
otherCmp := otherkey.D.Cmp(privkey.(*rsa.PrivateKey).D)
|
||||||
|
if 0 != otherCmp {
|
||||||
|
// There are two possible keys, choose the lesser D value
|
||||||
|
// See https://github.com/square/go-jose/issues/189
|
||||||
|
if otherCmp < 0 {
|
||||||
|
privkey = otherkey
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if maxRetry == i-1 {
|
||||||
|
log.Printf("error: coinflip landed on heads %d times", maxRetry)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return privkey
|
||||||
|
}
|
165
vendor/git.rootprojects.org/root/keypairs/sign.go
generated
vendored
Normal file
165
vendor/git.rootprojects.org/root/keypairs/sign.go
generated
vendored
Normal file
@ -0,0 +1,165 @@
|
|||||||
|
package keypairs
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto"
|
||||||
|
"crypto/ecdsa"
|
||||||
|
"crypto/rsa"
|
||||||
|
"crypto/sha256"
|
||||||
|
"encoding/base64"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
mathrand "math/rand" // to be used for good, not evil
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Object is a type alias representing generic JSON data
|
||||||
|
type Object = map[string]interface{}
|
||||||
|
|
||||||
|
// SignClaims adds `typ`, `kid` (or `jwk`), and `alg` in the header and expects claims for `jti`, `exp`, `iss`, and `iat`
|
||||||
|
func SignClaims(privkey PrivateKey, header Object, claims Object) (*JWS, error) {
|
||||||
|
var randsrc io.Reader = randReader
|
||||||
|
seed, _ := header["_seed"].(int64)
|
||||||
|
if 0 != seed {
|
||||||
|
randsrc = mathrand.New(mathrand.NewSource(seed))
|
||||||
|
//delete(header, "_seed")
|
||||||
|
}
|
||||||
|
|
||||||
|
protected, header, err := headerToProtected(NewPublicKey(privkey.Public()), header)
|
||||||
|
if nil != err {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
protected64 := base64.RawURLEncoding.EncodeToString(protected)
|
||||||
|
|
||||||
|
payload, err := claimsToPayload(claims)
|
||||||
|
if nil != err {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
payload64 := base64.RawURLEncoding.EncodeToString(payload)
|
||||||
|
|
||||||
|
signable := fmt.Sprintf(`%s.%s`, protected64, payload64)
|
||||||
|
hash := sha256.Sum256([]byte(signable))
|
||||||
|
|
||||||
|
sig := Sign(privkey, hash[:], randsrc)
|
||||||
|
sig64 := base64.RawURLEncoding.EncodeToString(sig)
|
||||||
|
//log.Printf("\n(Sign)\nSignable: %s", signable)
|
||||||
|
//log.Printf("Hash: %s", hash)
|
||||||
|
//log.Printf("Sig: %s", sig64)
|
||||||
|
|
||||||
|
return &JWS{
|
||||||
|
Header: header,
|
||||||
|
Claims: claims,
|
||||||
|
Protected: protected64,
|
||||||
|
Payload: payload64,
|
||||||
|
Signature: sig64,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func headerToProtected(pub PublicKey, header Object) ([]byte, Object, error) {
|
||||||
|
if nil == header {
|
||||||
|
header = Object{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only supporting 2048-bit and P256 keys right now
|
||||||
|
// because that's all that's practical and well-supported.
|
||||||
|
// No security theatre here.
|
||||||
|
alg := "ES256"
|
||||||
|
switch pub.Key().(type) {
|
||||||
|
case *rsa.PublicKey:
|
||||||
|
alg = "RS256"
|
||||||
|
}
|
||||||
|
|
||||||
|
if selfSign, _ := header["_jwk"].(bool); selfSign {
|
||||||
|
delete(header, "_jwk")
|
||||||
|
any := Object{}
|
||||||
|
_ = json.Unmarshal(MarshalJWKPublicKey(pub), &any)
|
||||||
|
header["jwk"] = any
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO what are the acceptable values? JWT. JWS? others?
|
||||||
|
header["typ"] = "JWT"
|
||||||
|
if _, ok := header["jwk"]; !ok {
|
||||||
|
thumbprint := ThumbprintPublicKey(pub)
|
||||||
|
kid, _ := header["kid"].(string)
|
||||||
|
if "" != kid && thumbprint != kid {
|
||||||
|
return nil, nil, errors.New("'kid' should be the key's thumbprint")
|
||||||
|
}
|
||||||
|
header["kid"] = thumbprint
|
||||||
|
}
|
||||||
|
header["alg"] = alg
|
||||||
|
|
||||||
|
protected, err := json.Marshal(header)
|
||||||
|
if nil != err {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
return protected, header, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func claimsToPayload(claims Object) ([]byte, error) {
|
||||||
|
if nil == claims {
|
||||||
|
claims = Object{}
|
||||||
|
}
|
||||||
|
|
||||||
|
var dur time.Duration
|
||||||
|
jti, _ := claims["jti"].(string)
|
||||||
|
insecure, _ := claims["insecure"].(bool)
|
||||||
|
|
||||||
|
switch exp := claims["exp"].(type) {
|
||||||
|
case time.Duration:
|
||||||
|
// TODO: MUST this go first?
|
||||||
|
// int64(time.Duration) vs time.Duration(int64)
|
||||||
|
dur = exp
|
||||||
|
case string:
|
||||||
|
var err error
|
||||||
|
dur, err = time.ParseDuration(exp)
|
||||||
|
// TODO s, err := time.ParseDuration(dur)
|
||||||
|
if nil != err {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
case int:
|
||||||
|
dur = time.Second * time.Duration(exp)
|
||||||
|
case int64:
|
||||||
|
dur = time.Second * time.Duration(exp)
|
||||||
|
case float64:
|
||||||
|
dur = time.Second * time.Duration(exp)
|
||||||
|
default:
|
||||||
|
dur = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
if "" == jti && 0 == dur && !insecure {
|
||||||
|
return nil, errors.New("token must have jti or exp as to be expirable / cancellable")
|
||||||
|
}
|
||||||
|
claims["exp"] = time.Now().Add(dur).Unix()
|
||||||
|
|
||||||
|
return json.Marshal(claims)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sign signs both RSA and ECDSA. Use `nil` or `crypto/rand.Reader` except for debugging.
|
||||||
|
func Sign(privkey PrivateKey, hash []byte, rand io.Reader) []byte {
|
||||||
|
if nil == rand {
|
||||||
|
rand = randReader
|
||||||
|
}
|
||||||
|
var sig []byte
|
||||||
|
|
||||||
|
if len(hash) != 32 {
|
||||||
|
panic("only 256-bit hashes for 2048-bit and 256-bit keys are supported")
|
||||||
|
}
|
||||||
|
|
||||||
|
switch k := privkey.(type) {
|
||||||
|
case *rsa.PrivateKey:
|
||||||
|
sig, _ = rsa.SignPKCS1v15(rand, k, crypto.SHA256, hash)
|
||||||
|
case *ecdsa.PrivateKey:
|
||||||
|
r, s, _ := ecdsa.Sign(rand, k, hash[:])
|
||||||
|
rb := r.Bytes()
|
||||||
|
for len(rb) < 32 {
|
||||||
|
rb = append([]byte{0}, rb...)
|
||||||
|
}
|
||||||
|
sb := s.Bytes()
|
||||||
|
for len(rb) < 32 {
|
||||||
|
sb = append([]byte{0}, sb...)
|
||||||
|
}
|
||||||
|
sig = append(rb, sb...)
|
||||||
|
}
|
||||||
|
return sig
|
||||||
|
}
|
174
vendor/git.rootprojects.org/root/keypairs/verify.go
generated
vendored
Normal file
174
vendor/git.rootprojects.org/root/keypairs/verify.go
generated
vendored
Normal file
@ -0,0 +1,174 @@
|
|||||||
|
package keypairs
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto"
|
||||||
|
"crypto/ecdsa"
|
||||||
|
"crypto/rsa"
|
||||||
|
"crypto/sha256"
|
||||||
|
"crypto/subtle"
|
||||||
|
"encoding/base64"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"math/big"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// VerifyClaims will check the signature of a parsed JWT
|
||||||
|
func VerifyClaims(pubkey PublicKey, jws *JWS) (errs []error) {
|
||||||
|
kid, _ := jws.Header["kid"].(string)
|
||||||
|
jwkmap, hasJWK := jws.Header["jwk"].(Object)
|
||||||
|
//var jwk JWK = nil
|
||||||
|
|
||||||
|
seed, _ := jws.Header["_seed"].(int64)
|
||||||
|
seedf64, _ := jws.Header["_seed"].(float64)
|
||||||
|
kty, _ := jws.Header["_kty"].(string)
|
||||||
|
if 0 == seed {
|
||||||
|
seed = int64(seedf64)
|
||||||
|
}
|
||||||
|
|
||||||
|
var pub PublicKey = nil
|
||||||
|
if hasJWK {
|
||||||
|
pub, errs = selfsignCheck(jwkmap, errs)
|
||||||
|
} else {
|
||||||
|
opts := &keyOptions{mockSeed: seed, KeyType: kty}
|
||||||
|
pub, errs = pubkeyCheck(pubkey, kid, opts, errs)
|
||||||
|
}
|
||||||
|
|
||||||
|
jti, _ := jws.Claims["jti"].(string)
|
||||||
|
expf64, _ := jws.Claims["exp"].(float64)
|
||||||
|
exp := int64(expf64)
|
||||||
|
if 0 == exp {
|
||||||
|
if "" == jti {
|
||||||
|
err := errors.New("one of 'jti' or 'exp' must exist for token expiry")
|
||||||
|
errs = append(errs, err)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if time.Now().Unix() > exp {
|
||||||
|
err := fmt.Errorf("token expired at %d (%s)", exp, time.Unix(exp, 0))
|
||||||
|
errs = append(errs, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
signable := fmt.Sprintf("%s.%s", jws.Protected, jws.Payload)
|
||||||
|
hash := sha256.Sum256([]byte(signable))
|
||||||
|
sig, err := base64.RawURLEncoding.DecodeString(jws.Signature)
|
||||||
|
if nil != err {
|
||||||
|
err := fmt.Errorf("could not decode signature: %w", err)
|
||||||
|
errs = append(errs, err)
|
||||||
|
return errs
|
||||||
|
}
|
||||||
|
|
||||||
|
//log.Printf("\n(Verify)\nSignable: %s", signable)
|
||||||
|
//log.Printf("Hash: %s", hash)
|
||||||
|
//log.Printf("Sig: %s", jws.Signature)
|
||||||
|
if nil == pub {
|
||||||
|
err := fmt.Errorf("token signature could not be verified")
|
||||||
|
errs = append(errs, err)
|
||||||
|
} else if !Verify(pub, hash[:], sig) {
|
||||||
|
err := fmt.Errorf("token signature is not valid")
|
||||||
|
errs = append(errs, err)
|
||||||
|
}
|
||||||
|
return errs
|
||||||
|
}
|
||||||
|
|
||||||
|
func selfsignCheck(jwkmap Object, errs []error) (PublicKey, []error) {
|
||||||
|
var pub PublicKey = nil
|
||||||
|
log.Println("Security TODO: did not check jws.Claims[\"sub\"] against 'jwk'")
|
||||||
|
log.Println("Security TODO: did not check jws.Claims[\"iss\"]")
|
||||||
|
kty := jwkmap["kty"]
|
||||||
|
var err error
|
||||||
|
if "RSA" == kty {
|
||||||
|
e, _ := jwkmap["e"].(string)
|
||||||
|
n, _ := jwkmap["n"].(string)
|
||||||
|
k, _ := (&RSAJWK{
|
||||||
|
Exp: e,
|
||||||
|
N: n,
|
||||||
|
}).marshalJWK()
|
||||||
|
pub, err = ParseJWKPublicKey(k)
|
||||||
|
if nil != err {
|
||||||
|
return nil, append(errs, err)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
crv, _ := jwkmap["crv"].(string)
|
||||||
|
x, _ := jwkmap["x"].(string)
|
||||||
|
y, _ := jwkmap["y"].(string)
|
||||||
|
k, _ := (&ECJWK{
|
||||||
|
Curve: crv,
|
||||||
|
X: x,
|
||||||
|
Y: y,
|
||||||
|
}).marshalJWK()
|
||||||
|
pub, err = ParseJWKPublicKey(k)
|
||||||
|
if nil != err {
|
||||||
|
return nil, append(errs, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return pub, errs
|
||||||
|
}
|
||||||
|
|
||||||
|
func pubkeyCheck(pubkey PublicKey, kid string, opts *keyOptions, errs []error) (PublicKey, []error) {
|
||||||
|
var pub PublicKey = nil
|
||||||
|
|
||||||
|
if "" == kid {
|
||||||
|
err := errors.New("token should have 'kid' or 'jwk' in header to identify the public key")
|
||||||
|
errs = append(errs, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if nil == pubkey {
|
||||||
|
if allowMocking {
|
||||||
|
if 0 == opts.mockSeed {
|
||||||
|
err := errors.New("the debug API requires '_seed' to accompany 'kid'")
|
||||||
|
errs = append(errs, err)
|
||||||
|
}
|
||||||
|
if "" == opts.KeyType {
|
||||||
|
err := errors.New("the debug API requires '_kty' to accompany '_seed'")
|
||||||
|
errs = append(errs, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if 0 == opts.mockSeed || "" == opts.KeyType {
|
||||||
|
return nil, errs
|
||||||
|
}
|
||||||
|
privkey := newPrivateKey(opts)
|
||||||
|
pub = NewPublicKey(privkey.Public())
|
||||||
|
return pub, errs
|
||||||
|
}
|
||||||
|
err := errors.New("no matching public key")
|
||||||
|
errs = append(errs, err)
|
||||||
|
} else {
|
||||||
|
pub = pubkey
|
||||||
|
}
|
||||||
|
|
||||||
|
if nil != pub && "" != kid {
|
||||||
|
if 1 != subtle.ConstantTimeCompare([]byte(kid), []byte(pub.Thumbprint())) {
|
||||||
|
err := errors.New("'kid' does not match the public key thumbprint")
|
||||||
|
errs = append(errs, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return pub, errs
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify will check the signature of a hash
|
||||||
|
func Verify(pubkey PublicKey, hash []byte, sig []byte) bool {
|
||||||
|
|
||||||
|
switch pub := pubkey.Key().(type) {
|
||||||
|
case *rsa.PublicKey:
|
||||||
|
//log.Printf("RSA VERIFY")
|
||||||
|
// TODO Size(key) to detect key size ?
|
||||||
|
//alg := "SHA256"
|
||||||
|
// TODO: this hasn't been tested yet
|
||||||
|
if err := rsa.VerifyPKCS1v15(pub, crypto.SHA256, hash, sig); nil != err {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
case *ecdsa.PublicKey:
|
||||||
|
r := &big.Int{}
|
||||||
|
r.SetBytes(sig[0:32])
|
||||||
|
s := &big.Int{}
|
||||||
|
s.SetBytes(sig[32:])
|
||||||
|
return ecdsa.Verify(pub, hash, r, s)
|
||||||
|
default:
|
||||||
|
panic("impossible condition: non-rsa/non-ecdsa key")
|
||||||
|
//return false
|
||||||
|
}
|
||||||
|
}
|
3
vendor/github.com/go-chi/chi/.gitignore
generated
vendored
Normal file
3
vendor/github.com/go-chi/chi/.gitignore
generated
vendored
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
.idea
|
||||||
|
*.sw?
|
||||||
|
.vscode
|
17
vendor/github.com/go-chi/chi/.travis.yml
generated
vendored
Normal file
17
vendor/github.com/go-chi/chi/.travis.yml
generated
vendored
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
language: go
|
||||||
|
|
||||||
|
go:
|
||||||
|
- 1.10.x
|
||||||
|
- 1.11.x
|
||||||
|
|
||||||
|
script:
|
||||||
|
- go get -d -t ./...
|
||||||
|
- go vet ./...
|
||||||
|
- go test ./...
|
||||||
|
- >
|
||||||
|
go_version=$(go version);
|
||||||
|
if [ ${go_version:13:4} = "1.11" ]; then
|
||||||
|
go get -u golang.org/x/tools/cmd/goimports;
|
||||||
|
goimports -d -e ./ | grep '.*' && { echo; echo "Aborting due to non-empty goimports output."; exit 1; } || :;
|
||||||
|
fi
|
||||||
|
|
139
vendor/github.com/go-chi/chi/CHANGELOG.md
generated
vendored
Normal file
139
vendor/github.com/go-chi/chi/CHANGELOG.md
generated
vendored
Normal file
@ -0,0 +1,139 @@
|
|||||||
|
# Changelog
|
||||||
|
|
||||||
|
## v4.0.0 (2019-01-10)
|
||||||
|
|
||||||
|
- chi v4 requires Go 1.10.3+ (or Go 1.9.7+) - we have deprecated support for Go 1.7 and 1.8
|
||||||
|
- router: respond with 404 on router with no routes (#362)
|
||||||
|
- router: additional check to ensure wildcard is at the end of a url pattern (#333)
|
||||||
|
- middleware: deprecate use of http.CloseNotifier (#347)
|
||||||
|
- middleware: fix RedirectSlashes to include query params on redirect (#334)
|
||||||
|
- History of changes: see https://github.com/go-chi/chi/compare/v3.3.4...v4.0.0
|
||||||
|
|
||||||
|
|
||||||
|
## v3.3.4 (2019-01-07)
|
||||||
|
|
||||||
|
- Minor middleware improvements. No changes to core library/router. Moving v3 into its
|
||||||
|
- own branch as a version of chi for Go 1.7, 1.8, 1.9, 1.10, 1.11
|
||||||
|
- History of changes: see https://github.com/go-chi/chi/compare/v3.3.3...v3.3.4
|
||||||
|
|
||||||
|
|
||||||
|
## v3.3.3 (2018-08-27)
|
||||||
|
|
||||||
|
- Minor release
|
||||||
|
- See https://github.com/go-chi/chi/compare/v3.3.2...v3.3.3
|
||||||
|
|
||||||
|
|
||||||
|
## v3.3.2 (2017-12-22)
|
||||||
|
|
||||||
|
- Support to route trailing slashes on mounted sub-routers (#281)
|
||||||
|
- middleware: new `ContentCharset` to check matching charsets. Thank you
|
||||||
|
@csucu for your community contribution!
|
||||||
|
|
||||||
|
|
||||||
|
## v3.3.1 (2017-11-20)
|
||||||
|
|
||||||
|
- middleware: new `AllowContentType` handler for explicit whitelist of accepted request Content-Types
|
||||||
|
- middleware: new `SetHeader` handler for short-hand middleware to set a response header key/value
|
||||||
|
- Minor bug fixes
|
||||||
|
|
||||||
|
|
||||||
|
## v3.3.0 (2017-10-10)
|
||||||
|
|
||||||
|
- New chi.RegisterMethod(method) to add support for custom HTTP methods, see _examples/custom-method for usage
|
||||||
|
- Deprecated LINK and UNLINK methods from the default list, please use `chi.RegisterMethod("LINK")` and `chi.RegisterMethod("UNLINK")` in an `init()` function
|
||||||
|
|
||||||
|
|
||||||
|
## v3.2.1 (2017-08-31)
|
||||||
|
|
||||||
|
- Add new `Match(rctx *Context, method, path string) bool` method to `Routes` interface
|
||||||
|
and `Mux`. Match searches the mux's routing tree for a handler that matches the method/path
|
||||||
|
- Add new `RouteMethod` to `*Context`
|
||||||
|
- Add new `Routes` pointer to `*Context`
|
||||||
|
- Add new `middleware.GetHead` to route missing HEAD requests to GET handler
|
||||||
|
- Updated benchmarks (see README)
|
||||||
|
|
||||||
|
|
||||||
|
## v3.1.5 (2017-08-02)
|
||||||
|
|
||||||
|
- Setup golint and go vet for the project
|
||||||
|
- As per golint, we've redefined `func ServerBaseContext(h http.Handler, baseCtx context.Context) http.Handler`
|
||||||
|
to `func ServerBaseContext(baseCtx context.Context, h http.Handler) http.Handler`
|
||||||
|
|
||||||
|
|
||||||
|
## v3.1.0 (2017-07-10)
|
||||||
|
|
||||||
|
- Fix a few minor issues after v3 release
|
||||||
|
- Move `docgen` sub-pkg to https://github.com/go-chi/docgen
|
||||||
|
- Move `render` sub-pkg to https://github.com/go-chi/render
|
||||||
|
- Add new `URLFormat` handler to chi/middleware sub-pkg to make working with url mime
|
||||||
|
suffixes easier, ie. parsing `/articles/1.json` and `/articles/1.xml`. See comments in
|
||||||
|
https://github.com/go-chi/chi/blob/master/middleware/url_format.go for example usage.
|
||||||
|
|
||||||
|
|
||||||
|
## v3.0.0 (2017-06-21)
|
||||||
|
|
||||||
|
- Major update to chi library with many exciting updates, but also some *breaking changes*
|
||||||
|
- URL parameter syntax changed from `/:id` to `/{id}` for even more flexible routing, such as
|
||||||
|
`/articles/{month}-{day}-{year}-{slug}`, `/articles/{id}`, and `/articles/{id}.{ext}` on the
|
||||||
|
same router
|
||||||
|
- Support for regexp for routing patterns, in the form of `/{paramKey:regExp}` for example:
|
||||||
|
`r.Get("/articles/{name:[a-z]+}", h)` and `chi.URLParam(r, "name")`
|
||||||
|
- Add `Method` and `MethodFunc` to `chi.Router` to allow routing definitions such as
|
||||||
|
`r.Method("GET", "/", h)` which provides a cleaner interface for custom handlers like
|
||||||
|
in `_examples/custom-handler`
|
||||||
|
- Deprecating `mux#FileServer` helper function. Instead, we encourage users to create their
|
||||||
|
own using file handler with the stdlib, see `_examples/fileserver` for an example
|
||||||
|
- Add support for LINK/UNLINK http methods via `r.Method()` and `r.MethodFunc()`
|
||||||
|
- Moved the chi project to its own organization, to allow chi-related community packages to
|
||||||
|
be easily discovered and supported, at: https://github.com/go-chi
|
||||||
|
- *NOTE:* please update your import paths to `"github.com/go-chi/chi"`
|
||||||
|
- *NOTE:* chi v2 is still available at https://github.com/go-chi/chi/tree/v2
|
||||||
|
|
||||||
|
|
||||||
|
## v2.1.0 (2017-03-30)
|
||||||
|
|
||||||
|
- Minor improvements and update to the chi core library
|
||||||
|
- Introduced a brand new `chi/render` sub-package to complete the story of building
|
||||||
|
APIs to offer a pattern for managing well-defined request / response payloads. Please
|
||||||
|
check out the updated `_examples/rest` example for how it works.
|
||||||
|
- Added `MethodNotAllowed(h http.HandlerFunc)` to chi.Router interface
|
||||||
|
|
||||||
|
|
||||||
|
## v2.0.0 (2017-01-06)
|
||||||
|
|
||||||
|
- After many months of v2 being in an RC state with many companies and users running it in
|
||||||
|
production, the inclusion of some improvements to the middlewares, we are very pleased to
|
||||||
|
announce v2.0.0 of chi.
|
||||||
|
|
||||||
|
|
||||||
|
## v2.0.0-rc1 (2016-07-26)
|
||||||
|
|
||||||
|
- Huge update! chi v2 is a large refactor targetting Go 1.7+. As of Go 1.7, the popular
|
||||||
|
community `"net/context"` package has been included in the standard library as `"context"` and
|
||||||
|
utilized by `"net/http"` and `http.Request` to managing deadlines, cancelation signals and other
|
||||||
|
request-scoped values. We're very excited about the new context addition and are proud to
|
||||||
|
introduce chi v2, a minimal and powerful routing package for building large HTTP services,
|
||||||
|
with zero external dependencies. Chi focuses on idiomatic design and encourages the use of
|
||||||
|
stdlib HTTP handlers and middlwares.
|
||||||
|
- chi v2 deprecates its `chi.Handler` interface and requires `http.Handler` or `http.HandlerFunc`
|
||||||
|
- chi v2 stores URL routing parameters and patterns in the standard request context: `r.Context()`
|
||||||
|
- chi v2 lower-level routing context is accessible by `chi.RouteContext(r.Context()) *chi.Context`,
|
||||||
|
which provides direct access to URL routing parameters, the routing path and the matching
|
||||||
|
routing patterns.
|
||||||
|
- Users upgrading from chi v1 to v2, need to:
|
||||||
|
1. Update the old chi.Handler signature, `func(ctx context.Context, w http.ResponseWriter, r *http.Request)` to
|
||||||
|
the standard http.Handler: `func(w http.ResponseWriter, r *http.Request)`
|
||||||
|
2. Use `chi.URLParam(r *http.Request, paramKey string) string`
|
||||||
|
or `URLParamFromCtx(ctx context.Context, paramKey string) string` to access a url parameter value
|
||||||
|
|
||||||
|
|
||||||
|
## v1.0.0 (2016-07-01)
|
||||||
|
|
||||||
|
- Released chi v1 stable https://github.com/go-chi/chi/tree/v1.0.0 for Go 1.6 and older.
|
||||||
|
|
||||||
|
|
||||||
|
## v0.9.0 (2016-03-31)
|
||||||
|
|
||||||
|
- Reuse context objects via sync.Pool for zero-allocation routing [#33](https://github.com/go-chi/chi/pull/33)
|
||||||
|
- BREAKING NOTE: due to subtle API changes, previously `chi.URLParams(ctx)["id"]` used to access url parameters
|
||||||
|
has changed to: `chi.URLParam(ctx, "id")`
|
31
vendor/github.com/go-chi/chi/CONTRIBUTING.md
generated
vendored
Normal file
31
vendor/github.com/go-chi/chi/CONTRIBUTING.md
generated
vendored
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
# Contributing
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
1. [Install Go][go-install].
|
||||||
|
2. Download the sources and switch the working directory:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
go get -u -d github.com/go-chi/chi
|
||||||
|
cd $GOPATH/src/github.com/go-chi/chi
|
||||||
|
```
|
||||||
|
|
||||||
|
## Submitting a Pull Request
|
||||||
|
|
||||||
|
A typical workflow is:
|
||||||
|
|
||||||
|
1. [Fork the repository.][fork] [This tip maybe also helpful.][go-fork-tip]
|
||||||
|
2. [Create a topic branch.][branch]
|
||||||
|
3. Add tests for your change.
|
||||||
|
4. Run `go test`. If your tests pass, return to the step 3.
|
||||||
|
5. Implement the change and ensure the steps from the previous step pass.
|
||||||
|
6. Run `goimports -w .`, to ensure the new code conforms to Go formatting guideline.
|
||||||
|
7. [Add, commit and push your changes.][git-help]
|
||||||
|
8. [Submit a pull request.][pull-req]
|
||||||
|
|
||||||
|
[go-install]: https://golang.org/doc/install
|
||||||
|
[go-fork-tip]: http://blog.campoy.cat/2014/03/github-and-go-forking-pull-requests-and.html
|
||||||
|
[fork]: https://help.github.com/articles/fork-a-repo
|
||||||
|
[branch]: http://learn.github.com/p/branching.html
|
||||||
|
[git-help]: https://guides.github.com
|
||||||
|
[pull-req]: https://help.github.com/articles/using-pull-requests
|
20
vendor/github.com/go-chi/chi/LICENSE
generated
vendored
Normal file
20
vendor/github.com/go-chi/chi/LICENSE
generated
vendored
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
Copyright (c) 2015-present Peter Kieltyka (https://github.com/pkieltyka), Google Inc.
|
||||||
|
|
||||||
|
MIT License
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||||
|
this software and associated documentation files (the "Software"), to deal in
|
||||||
|
the Software without restriction, including without limitation the rights to
|
||||||
|
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||||
|
the Software, and to permit persons to whom the Software is furnished to do so,
|
||||||
|
subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||||
|
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||||
|
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||||
|
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||||
|
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
438
vendor/github.com/go-chi/chi/README.md
generated
vendored
Normal file
438
vendor/github.com/go-chi/chi/README.md
generated
vendored
Normal file
@ -0,0 +1,438 @@
|
|||||||
|
# <img alt="chi" src="https://cdn.rawgit.com/go-chi/chi/master/_examples/chi.svg" width="220" />
|
||||||
|
|
||||||
|
|
||||||
|
[![GoDoc Widget]][GoDoc] [![Travis Widget]][Travis]
|
||||||
|
|
||||||
|
`chi` is a lightweight, idiomatic and composable router for building Go HTTP services. It's
|
||||||
|
especially good at helping you write large REST API services that are kept maintainable as your
|
||||||
|
project grows and changes. `chi` is built on the new `context` package introduced in Go 1.7 to
|
||||||
|
handle signaling, cancelation and request-scoped values across a handler chain.
|
||||||
|
|
||||||
|
The focus of the project has been to seek out an elegant and comfortable design for writing
|
||||||
|
REST API servers, written during the development of the Pressly API service that powers our
|
||||||
|
public API service, which in turn powers all of our client-side applications.
|
||||||
|
|
||||||
|
The key considerations of chi's design are: project structure, maintainability, standard http
|
||||||
|
handlers (stdlib-only), developer productivity, and deconstructing a large system into many small
|
||||||
|
parts. The core router `github.com/go-chi/chi` is quite small (less than 1000 LOC), but we've also
|
||||||
|
included some useful/optional subpackages: [middleware](/middleware), [render](https://github.com/go-chi/render) and [docgen](https://github.com/go-chi/docgen). We hope you enjoy it too!
|
||||||
|
|
||||||
|
## Install
|
||||||
|
|
||||||
|
`go get -u github.com/go-chi/chi`
|
||||||
|
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
* **Lightweight** - cloc'd in ~1000 LOC for the chi router
|
||||||
|
* **Fast** - yes, see [benchmarks](#benchmarks)
|
||||||
|
* **100% compatible with net/http** - use any http or middleware pkg in the ecosystem that is also compatible with `net/http`
|
||||||
|
* **Designed for modular/composable APIs** - middlewares, inline middlewares, route groups and subrouter mounting
|
||||||
|
* **Context control** - built on new `context` package, providing value chaining, cancelations and timeouts
|
||||||
|
* **Robust** - in production at Pressly, CloudFlare, Heroku, 99Designs, and many others (see [discussion](https://github.com/go-chi/chi/issues/91))
|
||||||
|
* **Doc generation** - `docgen` auto-generates routing documentation from your source to JSON or Markdown
|
||||||
|
* **No external dependencies** - plain ol' Go stdlib + net/http
|
||||||
|
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
See [_examples/](https://github.com/go-chi/chi/blob/master/_examples/) for a variety of examples.
|
||||||
|
|
||||||
|
|
||||||
|
**As easy as:**
|
||||||
|
|
||||||
|
```go
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"github.com/go-chi/chi"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
r := chi.NewRouter()
|
||||||
|
r.Get("/", func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.Write([]byte("welcome"))
|
||||||
|
})
|
||||||
|
http.ListenAndServe(":3000", r)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**REST Preview:**
|
||||||
|
|
||||||
|
Here is a little preview of how routing looks like with chi. Also take a look at the generated routing docs
|
||||||
|
in JSON ([routes.json](https://github.com/go-chi/chi/blob/master/_examples/rest/routes.json)) and in
|
||||||
|
Markdown ([routes.md](https://github.com/go-chi/chi/blob/master/_examples/rest/routes.md)).
|
||||||
|
|
||||||
|
I highly recommend reading the source of the [examples](https://github.com/go-chi/chi/blob/master/_examples/) listed
|
||||||
|
above, they will show you all the features of chi and serve as a good form of documentation.
|
||||||
|
|
||||||
|
```go
|
||||||
|
import (
|
||||||
|
//...
|
||||||
|
"context"
|
||||||
|
"github.com/go-chi/chi"
|
||||||
|
"github.com/go-chi/chi/middleware"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
r := chi.NewRouter()
|
||||||
|
|
||||||
|
// A good base middleware stack
|
||||||
|
r.Use(middleware.RequestID)
|
||||||
|
r.Use(middleware.RealIP)
|
||||||
|
r.Use(middleware.Logger)
|
||||||
|
r.Use(middleware.Recoverer)
|
||||||
|
|
||||||
|
// Set a timeout value on the request context (ctx), that will signal
|
||||||
|
// through ctx.Done() that the request has timed out and further
|
||||||
|
// processing should be stopped.
|
||||||
|
r.Use(middleware.Timeout(60 * time.Second))
|
||||||
|
|
||||||
|
r.Get("/", func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.Write([]byte("hi"))
|
||||||
|
})
|
||||||
|
|
||||||
|
// RESTy routes for "articles" resource
|
||||||
|
r.Route("/articles", func(r chi.Router) {
|
||||||
|
r.With(paginate).Get("/", listArticles) // GET /articles
|
||||||
|
r.With(paginate).Get("/{month}-{day}-{year}", listArticlesByDate) // GET /articles/01-16-2017
|
||||||
|
|
||||||
|
r.Post("/", createArticle) // POST /articles
|
||||||
|
r.Get("/search", searchArticles) // GET /articles/search
|
||||||
|
|
||||||
|
// Regexp url parameters:
|
||||||
|
r.Get("/{articleSlug:[a-z-]+}", getArticleBySlug) // GET /articles/home-is-toronto
|
||||||
|
|
||||||
|
// Subrouters:
|
||||||
|
r.Route("/{articleID}", func(r chi.Router) {
|
||||||
|
r.Use(ArticleCtx)
|
||||||
|
r.Get("/", getArticle) // GET /articles/123
|
||||||
|
r.Put("/", updateArticle) // PUT /articles/123
|
||||||
|
r.Delete("/", deleteArticle) // DELETE /articles/123
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
// Mount the admin sub-router
|
||||||
|
r.Mount("/admin", adminRouter())
|
||||||
|
|
||||||
|
http.ListenAndServe(":3333", r)
|
||||||
|
}
|
||||||
|
|
||||||
|
func ArticleCtx(next http.Handler) http.Handler {
|
||||||
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
articleID := chi.URLParam(r, "articleID")
|
||||||
|
article, err := dbGetArticle(articleID)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, http.StatusText(404), 404)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ctx := context.WithValue(r.Context(), "article", article)
|
||||||
|
next.ServeHTTP(w, r.WithContext(ctx))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func getArticle(w http.ResponseWriter, r *http.Request) {
|
||||||
|
ctx := r.Context()
|
||||||
|
article, ok := ctx.Value("article").(*Article)
|
||||||
|
if !ok {
|
||||||
|
http.Error(w, http.StatusText(422), 422)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
w.Write([]byte(fmt.Sprintf("title:%s", article.Title)))
|
||||||
|
}
|
||||||
|
|
||||||
|
// A completely separate router for administrator routes
|
||||||
|
func adminRouter() http.Handler {
|
||||||
|
r := chi.NewRouter()
|
||||||
|
r.Use(AdminOnly)
|
||||||
|
r.Get("/", adminIndex)
|
||||||
|
r.Get("/accounts", adminListAccounts)
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
|
func AdminOnly(next http.Handler) http.Handler {
|
||||||
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
ctx := r.Context()
|
||||||
|
perm, ok := ctx.Value("acl.permission").(YourPermissionType)
|
||||||
|
if !ok || !perm.IsAdmin() {
|
||||||
|
http.Error(w, http.StatusText(403), 403)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
next.ServeHTTP(w, r)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
## Router design
|
||||||
|
|
||||||
|
chi's router is based on a kind of [Patricia Radix trie](https://en.wikipedia.org/wiki/Radix_tree).
|
||||||
|
The router is fully compatible with `net/http`.
|
||||||
|
|
||||||
|
Built on top of the tree is the `Router` interface:
|
||||||
|
|
||||||
|
```go
|
||||||
|
// Router consisting of the core routing methods used by chi's Mux,
|
||||||
|
// using only the standard net/http.
|
||||||
|
type Router interface {
|
||||||
|
http.Handler
|
||||||
|
Routes
|
||||||
|
|
||||||
|
// Use appends one of more middlewares onto the Router stack.
|
||||||
|
Use(middlewares ...func(http.Handler) http.Handler)
|
||||||
|
|
||||||
|
// With adds inline middlewares for an endpoint handler.
|
||||||
|
With(middlewares ...func(http.Handler) http.Handler) Router
|
||||||
|
|
||||||
|
// Group adds a new inline-Router along the current routing
|
||||||
|
// path, with a fresh middleware stack for the inline-Router.
|
||||||
|
Group(fn func(r Router)) Router
|
||||||
|
|
||||||
|
// Route mounts a sub-Router along a `pattern`` string.
|
||||||
|
Route(pattern string, fn func(r Router)) Router
|
||||||
|
|
||||||
|
// Mount attaches another http.Handler along ./pattern/*
|
||||||
|
Mount(pattern string, h http.Handler)
|
||||||
|
|
||||||
|
// Handle and HandleFunc adds routes for `pattern` that matches
|
||||||
|
// all HTTP methods.
|
||||||
|
Handle(pattern string, h http.Handler)
|
||||||
|
HandleFunc(pattern string, h http.HandlerFunc)
|
||||||
|
|
||||||
|
// Method and MethodFunc adds routes for `pattern` that matches
|
||||||
|
// the `method` HTTP method.
|
||||||
|
Method(method, pattern string, h http.Handler)
|
||||||
|
MethodFunc(method, pattern string, h http.HandlerFunc)
|
||||||
|
|
||||||
|
// HTTP-method routing along `pattern`
|
||||||
|
Connect(pattern string, h http.HandlerFunc)
|
||||||
|
Delete(pattern string, h http.HandlerFunc)
|
||||||
|
Get(pattern string, h http.HandlerFunc)
|
||||||
|
Head(pattern string, h http.HandlerFunc)
|
||||||
|
Options(pattern string, h http.HandlerFunc)
|
||||||
|
Patch(pattern string, h http.HandlerFunc)
|
||||||
|
Post(pattern string, h http.HandlerFunc)
|
||||||
|
Put(pattern string, h http.HandlerFunc)
|
||||||
|
Trace(pattern string, h http.HandlerFunc)
|
||||||
|
|
||||||
|
// NotFound defines a handler to respond whenever a route could
|
||||||
|
// not be found.
|
||||||
|
NotFound(h http.HandlerFunc)
|
||||||
|
|
||||||
|
// MethodNotAllowed defines a handler to respond whenever a method is
|
||||||
|
// not allowed.
|
||||||
|
MethodNotAllowed(h http.HandlerFunc)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Routes interface adds two methods for router traversal, which is also
|
||||||
|
// used by the github.com/go-chi/docgen package to generate documentation for Routers.
|
||||||
|
type Routes interface {
|
||||||
|
// Routes returns the routing tree in an easily traversable structure.
|
||||||
|
Routes() []Route
|
||||||
|
|
||||||
|
// Middlewares returns the list of middlewares in use by the router.
|
||||||
|
Middlewares() Middlewares
|
||||||
|
|
||||||
|
// Match searches the routing tree for a handler that matches
|
||||||
|
// the method/path - similar to routing a http request, but without
|
||||||
|
// executing the handler thereafter.
|
||||||
|
Match(rctx *Context, method, path string) bool
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Each routing method accepts a URL `pattern` and chain of `handlers`. The URL pattern
|
||||||
|
supports named params (ie. `/users/{userID}`) and wildcards (ie. `/admin/*`). URL parameters
|
||||||
|
can be fetched at runtime by calling `chi.URLParam(r, "userID")` for named parameters
|
||||||
|
and `chi.URLParam(r, "*")` for a wildcard parameter.
|
||||||
|
|
||||||
|
|
||||||
|
### Middleware handlers
|
||||||
|
|
||||||
|
chi's middlewares are just stdlib net/http middleware handlers. There is nothing special
|
||||||
|
about them, which means the router and all the tooling is designed to be compatible and
|
||||||
|
friendly with any middleware in the community. This offers much better extensibility and reuse
|
||||||
|
of packages and is at the heart of chi's purpose.
|
||||||
|
|
||||||
|
Here is an example of a standard net/http middleware handler using the new request context
|
||||||
|
available in Go. This middleware sets a hypothetical user identifier on the request
|
||||||
|
context and calls the next handler in the chain.
|
||||||
|
|
||||||
|
```go
|
||||||
|
// HTTP middleware setting a value on the request context
|
||||||
|
func MyMiddleware(next http.Handler) http.Handler {
|
||||||
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
ctx := context.WithValue(r.Context(), "user", "123")
|
||||||
|
next.ServeHTTP(w, r.WithContext(ctx))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
### Request handlers
|
||||||
|
|
||||||
|
chi uses standard net/http request handlers. This little snippet is an example of a http.Handler
|
||||||
|
func that reads a user identifier from the request context - hypothetically, identifying
|
||||||
|
the user sending an authenticated request, validated+set by a previous middleware handler.
|
||||||
|
|
||||||
|
```go
|
||||||
|
// HTTP handler accessing data from the request context.
|
||||||
|
func MyRequestHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
|
user := r.Context().Value("user").(string)
|
||||||
|
w.Write([]byte(fmt.Sprintf("hi %s", user)))
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
### URL parameters
|
||||||
|
|
||||||
|
chi's router parses and stores URL parameters right onto the request context. Here is
|
||||||
|
an example of how to access URL params in your net/http handlers. And of course, middlewares
|
||||||
|
are able to access the same information.
|
||||||
|
|
||||||
|
```go
|
||||||
|
// HTTP handler accessing the url routing parameters.
|
||||||
|
func MyRequestHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
|
userID := chi.URLParam(r, "userID") // from a route like /users/{userID}
|
||||||
|
|
||||||
|
ctx := r.Context()
|
||||||
|
key := ctx.Value("key").(string)
|
||||||
|
|
||||||
|
w.Write([]byte(fmt.Sprintf("hi %v, %v", userID, key)))
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
## Middlewares
|
||||||
|
|
||||||
|
chi comes equipped with an optional `middleware` package, providing a suite of standard
|
||||||
|
`net/http` middlewares. Please note, any middleware in the ecosystem that is also compatible
|
||||||
|
with `net/http` can be used with chi's mux.
|
||||||
|
|
||||||
|
### Core middlewares
|
||||||
|
|
||||||
|
-----------------------------------------------------------------------------------------------------------
|
||||||
|
| chi/middleware Handler | description |
|
||||||
|
|:----------------------|:---------------------------------------------------------------------------------
|
||||||
|
| AllowContentType | Explicit whitelist of accepted request Content-Types |
|
||||||
|
| Compress | Gzip compression for clients that accept compressed responses |
|
||||||
|
| GetHead | Automatically route undefined HEAD requests to GET handlers |
|
||||||
|
| Heartbeat | Monitoring endpoint to check the servers pulse |
|
||||||
|
| Logger | Logs the start and end of each request with the elapsed processing time |
|
||||||
|
| NoCache | Sets response headers to prevent clients from caching |
|
||||||
|
| Profiler | Easily attach net/http/pprof to your routers |
|
||||||
|
| RealIP | Sets a http.Request's RemoteAddr to either X-Forwarded-For or X-Real-IP |
|
||||||
|
| Recoverer | Gracefully absorb panics and prints the stack trace |
|
||||||
|
| RequestID | Injects a request ID into the context of each request |
|
||||||
|
| RedirectSlashes | Redirect slashes on routing paths |
|
||||||
|
| SetHeader | Short-hand middleware to set a response header key/value |
|
||||||
|
| StripSlashes | Strip slashes on routing paths |
|
||||||
|
| Throttle | Puts a ceiling on the number of concurrent requests |
|
||||||
|
| Timeout | Signals to the request context when the timeout deadline is reached |
|
||||||
|
| URLFormat | Parse extension from url and put it on request context |
|
||||||
|
| WithValue | Short-hand middleware to set a key/value on the request context |
|
||||||
|
-----------------------------------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
### Auxiliary middlewares & packages
|
||||||
|
|
||||||
|
Please see https://github.com/go-chi for additional packages.
|
||||||
|
|
||||||
|
--------------------------------------------------------------------------------------------------------------------
|
||||||
|
| package | description |
|
||||||
|
|:---------------------------------------------------|:-------------------------------------------------------------
|
||||||
|
| [cors](https://github.com/go-chi/cors) | Cross-origin resource sharing (CORS) |
|
||||||
|
| [docgen](https://github.com/go-chi/docgen) | Print chi.Router routes at runtime |
|
||||||
|
| [jwtauth](https://github.com/go-chi/jwtauth) | JWT authentication |
|
||||||
|
| [hostrouter](https://github.com/go-chi/hostrouter) | Domain/host based request routing |
|
||||||
|
| [httpcoala](https://github.com/go-chi/httpcoala) | HTTP request coalescer |
|
||||||
|
| [chi-authz](https://github.com/casbin/chi-authz) | Request ACL via https://github.com/hsluoyz/casbin |
|
||||||
|
| [phi](https://github.com/fate-lovely/phi) | Port chi to [fasthttp](https://github.com/valyala/fasthttp) |
|
||||||
|
--------------------------------------------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
please [submit a PR](./CONTRIBUTING.md) if you'd like to include a link to a chi-compatible middleware
|
||||||
|
|
||||||
|
|
||||||
|
## context?
|
||||||
|
|
||||||
|
`context` is a tiny pkg that provides simple interface to signal context across call stacks
|
||||||
|
and goroutines. It was originally written by [Sameer Ajmani](https://github.com/Sajmani)
|
||||||
|
and is available in stdlib since go1.7.
|
||||||
|
|
||||||
|
Learn more at https://blog.golang.org/context
|
||||||
|
|
||||||
|
and..
|
||||||
|
* Docs: https://golang.org/pkg/context
|
||||||
|
* Source: https://github.com/golang/go/tree/master/src/context
|
||||||
|
|
||||||
|
|
||||||
|
## Benchmarks
|
||||||
|
|
||||||
|
The benchmark suite: https://github.com/pkieltyka/go-http-routing-benchmark
|
||||||
|
|
||||||
|
Results as of Jan 9, 2019 with Go 1.11.4 on Linux X1 Carbon laptop
|
||||||
|
|
||||||
|
```shell
|
||||||
|
BenchmarkChi_Param 3000000 475 ns/op 432 B/op 3 allocs/op
|
||||||
|
BenchmarkChi_Param5 2000000 696 ns/op 432 B/op 3 allocs/op
|
||||||
|
BenchmarkChi_Param20 1000000 1275 ns/op 432 B/op 3 allocs/op
|
||||||
|
BenchmarkChi_ParamWrite 3000000 505 ns/op 432 B/op 3 allocs/op
|
||||||
|
BenchmarkChi_GithubStatic 3000000 508 ns/op 432 B/op 3 allocs/op
|
||||||
|
BenchmarkChi_GithubParam 2000000 669 ns/op 432 B/op 3 allocs/op
|
||||||
|
BenchmarkChi_GithubAll 10000 134627 ns/op 87699 B/op 609 allocs/op
|
||||||
|
BenchmarkChi_GPlusStatic 3000000 402 ns/op 432 B/op 3 allocs/op
|
||||||
|
BenchmarkChi_GPlusParam 3000000 500 ns/op 432 B/op 3 allocs/op
|
||||||
|
BenchmarkChi_GPlus2Params 3000000 586 ns/op 432 B/op 3 allocs/op
|
||||||
|
BenchmarkChi_GPlusAll 200000 7237 ns/op 5616 B/op 39 allocs/op
|
||||||
|
BenchmarkChi_ParseStatic 3000000 408 ns/op 432 B/op 3 allocs/op
|
||||||
|
BenchmarkChi_ParseParam 3000000 488 ns/op 432 B/op 3 allocs/op
|
||||||
|
BenchmarkChi_Parse2Params 3000000 551 ns/op 432 B/op 3 allocs/op
|
||||||
|
BenchmarkChi_ParseAll 100000 13508 ns/op 11232 B/op 78 allocs/op
|
||||||
|
BenchmarkChi_StaticAll 20000 81933 ns/op 67826 B/op 471 allocs/op
|
||||||
|
```
|
||||||
|
|
||||||
|
Comparison with other routers: https://gist.github.com/pkieltyka/123032f12052520aaccab752bd3e78cc
|
||||||
|
|
||||||
|
NOTE: the allocs in the benchmark above are from the calls to http.Request's
|
||||||
|
`WithContext(context.Context)` method that clones the http.Request, sets the `Context()`
|
||||||
|
on the duplicated (alloc'd) request and returns it the new request object. This is just
|
||||||
|
how setting context on a request in Go works.
|
||||||
|
|
||||||
|
|
||||||
|
## Credits
|
||||||
|
|
||||||
|
* Carl Jackson for https://github.com/zenazn/goji
|
||||||
|
* Parts of chi's thinking comes from goji, and chi's middleware package
|
||||||
|
sources from goji.
|
||||||
|
* Armon Dadgar for https://github.com/armon/go-radix
|
||||||
|
* Contributions: [@VojtechVitek](https://github.com/VojtechVitek)
|
||||||
|
|
||||||
|
We'll be more than happy to see [your contributions](./CONTRIBUTING.md)!
|
||||||
|
|
||||||
|
|
||||||
|
## Beyond REST
|
||||||
|
|
||||||
|
chi is just a http router that lets you decompose request handling into many smaller layers.
|
||||||
|
Many companies including Pressly.com (of course) use chi to write REST services for their public
|
||||||
|
APIs. But, REST is just a convention for managing state via HTTP, and there's a lot of other pieces
|
||||||
|
required to write a complete client-server system or network of microservices.
|
||||||
|
|
||||||
|
Looking ahead beyond REST, I also recommend some newer works in the field coming from
|
||||||
|
[gRPC](https://github.com/grpc/grpc-go), [NATS](https://nats.io), [go-kit](https://github.com/go-kit/kit)
|
||||||
|
and even [graphql](https://github.com/graphql-go/graphql). They're all pretty cool with their
|
||||||
|
own unique approaches and benefits. Specifically, I'd look at gRPC since it makes client-server
|
||||||
|
communication feel like a single program on a single computer, no need to hand-write a client library
|
||||||
|
and the request/response payloads are typed contracts. NATS is pretty amazing too as a super
|
||||||
|
fast and lightweight pub-sub transport that can speak protobufs, with nice service discovery -
|
||||||
|
an excellent combination with gRPC.
|
||||||
|
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
Copyright (c) 2015-present [Peter Kieltyka](https://github.com/pkieltyka)
|
||||||
|
|
||||||
|
Licensed under [MIT License](./LICENSE)
|
||||||
|
|
||||||
|
[GoDoc]: https://godoc.org/github.com/go-chi/chi
|
||||||
|
[GoDoc Widget]: https://godoc.org/github.com/go-chi/chi?status.svg
|
||||||
|
[Travis]: https://travis-ci.org/go-chi/chi
|
||||||
|
[Travis Widget]: https://travis-ci.org/go-chi/chi.svg?branch=master
|
49
vendor/github.com/go-chi/chi/chain.go
generated
vendored
Normal file
49
vendor/github.com/go-chi/chi/chain.go
generated
vendored
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
package chi
|
||||||
|
|
||||||
|
import "net/http"
|
||||||
|
|
||||||
|
// Chain returns a Middlewares type from a slice of middleware handlers.
|
||||||
|
func Chain(middlewares ...func(http.Handler) http.Handler) Middlewares {
|
||||||
|
return Middlewares(middlewares)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handler builds and returns a http.Handler from the chain of middlewares,
|
||||||
|
// with `h http.Handler` as the final handler.
|
||||||
|
func (mws Middlewares) Handler(h http.Handler) http.Handler {
|
||||||
|
return &ChainHandler{mws, h, chain(mws, h)}
|
||||||
|
}
|
||||||
|
|
||||||
|
// HandlerFunc builds and returns a http.Handler from the chain of middlewares,
|
||||||
|
// with `h http.Handler` as the final handler.
|
||||||
|
func (mws Middlewares) HandlerFunc(h http.HandlerFunc) http.Handler {
|
||||||
|
return &ChainHandler{mws, h, chain(mws, h)}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ChainHandler is a http.Handler with support for handler composition and
|
||||||
|
// execution.
|
||||||
|
type ChainHandler struct {
|
||||||
|
Middlewares Middlewares
|
||||||
|
Endpoint http.Handler
|
||||||
|
chain http.Handler
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *ChainHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||||
|
c.chain.ServeHTTP(w, r)
|
||||||
|
}
|
||||||
|
|
||||||
|
// chain builds a http.Handler composed of an inline middleware stack and endpoint
|
||||||
|
// handler in the order they are passed.
|
||||||
|
func chain(middlewares []func(http.Handler) http.Handler, endpoint http.Handler) http.Handler {
|
||||||
|
// Return ahead of time if there aren't any middlewares for the chain
|
||||||
|
if len(middlewares) == 0 {
|
||||||
|
return endpoint
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wrap the end handler with the middleware chain
|
||||||
|
h := middlewares[len(middlewares)-1](endpoint)
|
||||||
|
for i := len(middlewares) - 2; i >= 0; i-- {
|
||||||
|
h = middlewares[i](h)
|
||||||
|
}
|
||||||
|
|
||||||
|
return h
|
||||||
|
}
|
134
vendor/github.com/go-chi/chi/chi.go
generated
vendored
Normal file
134
vendor/github.com/go-chi/chi/chi.go
generated
vendored
Normal file
@ -0,0 +1,134 @@
|
|||||||
|
//
|
||||||
|
// Package chi is a small, idiomatic and composable router for building HTTP services.
|
||||||
|
//
|
||||||
|
// chi requires Go 1.7 or newer.
|
||||||
|
//
|
||||||
|
// Example:
|
||||||
|
// package main
|
||||||
|
//
|
||||||
|
// import (
|
||||||
|
// "net/http"
|
||||||
|
//
|
||||||
|
// "github.com/go-chi/chi"
|
||||||
|
// "github.com/go-chi/chi/middleware"
|
||||||
|
// )
|
||||||
|
//
|
||||||
|
// func main() {
|
||||||
|
// r := chi.NewRouter()
|
||||||
|
// r.Use(middleware.Logger)
|
||||||
|
// r.Use(middleware.Recoverer)
|
||||||
|
//
|
||||||
|
// r.Get("/", func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
// w.Write([]byte("root."))
|
||||||
|
// })
|
||||||
|
//
|
||||||
|
// http.ListenAndServe(":3333", r)
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// See github.com/go-chi/chi/_examples/ for more in-depth examples.
|
||||||
|
//
|
||||||
|
// URL patterns allow for easy matching of path components in HTTP
|
||||||
|
// requests. The matching components can then be accessed using
|
||||||
|
// chi.URLParam(). All patterns must begin with a slash.
|
||||||
|
//
|
||||||
|
// A simple named placeholder {name} matches any sequence of characters
|
||||||
|
// up to the next / or the end of the URL. Trailing slashes on paths must
|
||||||
|
// be handled explicitly.
|
||||||
|
//
|
||||||
|
// A placeholder with a name followed by a colon allows a regular
|
||||||
|
// expression match, for example {number:\\d+}. The regular expression
|
||||||
|
// syntax is Go's normal regexp RE2 syntax, except that regular expressions
|
||||||
|
// including { or } are not supported, and / will never be
|
||||||
|
// matched. An anonymous regexp pattern is allowed, using an empty string
|
||||||
|
// before the colon in the placeholder, such as {:\\d+}
|
||||||
|
//
|
||||||
|
// The special placeholder of asterisk matches the rest of the requested
|
||||||
|
// URL. Any trailing characters in the pattern are ignored. This is the only
|
||||||
|
// placeholder which will match / characters.
|
||||||
|
//
|
||||||
|
// Examples:
|
||||||
|
// "/user/{name}" matches "/user/jsmith" but not "/user/jsmith/info" or "/user/jsmith/"
|
||||||
|
// "/user/{name}/info" matches "/user/jsmith/info"
|
||||||
|
// "/page/*" matches "/page/intro/latest"
|
||||||
|
// "/page/*/index" also matches "/page/intro/latest"
|
||||||
|
// "/date/{yyyy:\\d\\d\\d\\d}/{mm:\\d\\d}/{dd:\\d\\d}" matches "/date/2017/04/01"
|
||||||
|
//
|
||||||
|
package chi
|
||||||
|
|
||||||
|
import "net/http"
|
||||||
|
|
||||||
|
// NewRouter returns a new Mux object that implements the Router interface.
|
||||||
|
func NewRouter() *Mux {
|
||||||
|
return NewMux()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Router consisting of the core routing methods used by chi's Mux,
|
||||||
|
// using only the standard net/http.
|
||||||
|
type Router interface {
|
||||||
|
http.Handler
|
||||||
|
Routes
|
||||||
|
|
||||||
|
// Use appends one of more middlewares onto the Router stack.
|
||||||
|
Use(middlewares ...func(http.Handler) http.Handler)
|
||||||
|
|
||||||
|
// With adds inline middlewares for an endpoint handler.
|
||||||
|
With(middlewares ...func(http.Handler) http.Handler) Router
|
||||||
|
|
||||||
|
// Group adds a new inline-Router along the current routing
|
||||||
|
// path, with a fresh middleware stack for the inline-Router.
|
||||||
|
Group(fn func(r Router)) Router
|
||||||
|
|
||||||
|
// Route mounts a sub-Router along a `pattern`` string.
|
||||||
|
Route(pattern string, fn func(r Router)) Router
|
||||||
|
|
||||||
|
// Mount attaches another http.Handler along ./pattern/*
|
||||||
|
Mount(pattern string, h http.Handler)
|
||||||
|
|
||||||
|
// Handle and HandleFunc adds routes for `pattern` that matches
|
||||||
|
// all HTTP methods.
|
||||||
|
Handle(pattern string, h http.Handler)
|
||||||
|
HandleFunc(pattern string, h http.HandlerFunc)
|
||||||
|
|
||||||
|
// Method and MethodFunc adds routes for `pattern` that matches
|
||||||
|
// the `method` HTTP method.
|
||||||
|
Method(method, pattern string, h http.Handler)
|
||||||
|
MethodFunc(method, pattern string, h http.HandlerFunc)
|
||||||
|
|
||||||
|
// HTTP-method routing along `pattern`
|
||||||
|
Connect(pattern string, h http.HandlerFunc)
|
||||||
|
Delete(pattern string, h http.HandlerFunc)
|
||||||
|
Get(pattern string, h http.HandlerFunc)
|
||||||
|
Head(pattern string, h http.HandlerFunc)
|
||||||
|
Options(pattern string, h http.HandlerFunc)
|
||||||
|
Patch(pattern string, h http.HandlerFunc)
|
||||||
|
Post(pattern string, h http.HandlerFunc)
|
||||||
|
Put(pattern string, h http.HandlerFunc)
|
||||||
|
Trace(pattern string, h http.HandlerFunc)
|
||||||
|
|
||||||
|
// NotFound defines a handler to respond whenever a route could
|
||||||
|
// not be found.
|
||||||
|
NotFound(h http.HandlerFunc)
|
||||||
|
|
||||||
|
// MethodNotAllowed defines a handler to respond whenever a method is
|
||||||
|
// not allowed.
|
||||||
|
MethodNotAllowed(h http.HandlerFunc)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Routes interface adds two methods for router traversal, which is also
|
||||||
|
// used by the `docgen` subpackage to generation documentation for Routers.
|
||||||
|
type Routes interface {
|
||||||
|
// Routes returns the routing tree in an easily traversable structure.
|
||||||
|
Routes() []Route
|
||||||
|
|
||||||
|
// Middlewares returns the list of middlewares in use by the router.
|
||||||
|
Middlewares() Middlewares
|
||||||
|
|
||||||
|
// Match searches the routing tree for a handler that matches
|
||||||
|
// the method/path - similar to routing a http request, but without
|
||||||
|
// executing the handler thereafter.
|
||||||
|
Match(rctx *Context, method, path string) bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// Middlewares type is a slice of standard middleware handlers with methods
|
||||||
|
// to compose middleware chains and http.Handler's.
|
||||||
|
type Middlewares []func(http.Handler) http.Handler
|
161
vendor/github.com/go-chi/chi/context.go
generated
vendored
Normal file
161
vendor/github.com/go-chi/chi/context.go
generated
vendored
Normal file
@ -0,0 +1,161 @@
|
|||||||
|
package chi
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
// RouteCtxKey is the context.Context key to store the request context.
|
||||||
|
RouteCtxKey = &contextKey{"RouteContext"}
|
||||||
|
)
|
||||||
|
|
||||||
|
// Context is the default routing context set on the root node of a
|
||||||
|
// request context to track route patterns, URL parameters and
|
||||||
|
// an optional routing path.
|
||||||
|
type Context struct {
|
||||||
|
Routes Routes
|
||||||
|
|
||||||
|
// Routing path/method override used during the route search.
|
||||||
|
// See Mux#routeHTTP method.
|
||||||
|
RoutePath string
|
||||||
|
RouteMethod string
|
||||||
|
|
||||||
|
// Routing pattern stack throughout the lifecycle of the request,
|
||||||
|
// across all connected routers. It is a record of all matching
|
||||||
|
// patterns across a stack of sub-routers.
|
||||||
|
RoutePatterns []string
|
||||||
|
|
||||||
|
// URLParams are the stack of routeParams captured during the
|
||||||
|
// routing lifecycle across a stack of sub-routers.
|
||||||
|
URLParams RouteParams
|
||||||
|
|
||||||
|
// The endpoint routing pattern that matched the request URI path
|
||||||
|
// or `RoutePath` of the current sub-router. This value will update
|
||||||
|
// during the lifecycle of a request passing through a stack of
|
||||||
|
// sub-routers.
|
||||||
|
routePattern string
|
||||||
|
|
||||||
|
// Route parameters matched for the current sub-router. It is
|
||||||
|
// intentionally unexported so it cant be tampered.
|
||||||
|
routeParams RouteParams
|
||||||
|
|
||||||
|
// methodNotAllowed hint
|
||||||
|
methodNotAllowed bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewRouteContext returns a new routing Context object.
|
||||||
|
func NewRouteContext() *Context {
|
||||||
|
return &Context{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reset a routing context to its initial state.
|
||||||
|
func (x *Context) Reset() {
|
||||||
|
x.Routes = nil
|
||||||
|
x.RoutePath = ""
|
||||||
|
x.RouteMethod = ""
|
||||||
|
x.RoutePatterns = x.RoutePatterns[:0]
|
||||||
|
x.URLParams.Keys = x.URLParams.Keys[:0]
|
||||||
|
x.URLParams.Values = x.URLParams.Values[:0]
|
||||||
|
|
||||||
|
x.routePattern = ""
|
||||||
|
x.routeParams.Keys = x.routeParams.Keys[:0]
|
||||||
|
x.routeParams.Values = x.routeParams.Values[:0]
|
||||||
|
x.methodNotAllowed = false
|
||||||
|
}
|
||||||
|
|
||||||
|
// URLParam returns the corresponding URL parameter value from the request
|
||||||
|
// routing context.
|
||||||
|
func (x *Context) URLParam(key string) string {
|
||||||
|
for k := len(x.URLParams.Keys) - 1; k >= 0; k-- {
|
||||||
|
if x.URLParams.Keys[k] == key {
|
||||||
|
return x.URLParams.Values[k]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// RoutePattern builds the routing pattern string for the particular
|
||||||
|
// request, at the particular point during routing. This means, the value
|
||||||
|
// will change throughout the execution of a request in a router. That is
|
||||||
|
// why its advised to only use this value after calling the next handler.
|
||||||
|
//
|
||||||
|
// For example,
|
||||||
|
//
|
||||||
|
// func Instrument(next http.Handler) http.Handler {
|
||||||
|
// return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
// next.ServeHTTP(w, r)
|
||||||
|
// routePattern := chi.RouteContext(r.Context()).RoutePattern()
|
||||||
|
// measure(w, r, routePattern)
|
||||||
|
// })
|
||||||
|
// }
|
||||||
|
func (x *Context) RoutePattern() string {
|
||||||
|
routePattern := strings.Join(x.RoutePatterns, "")
|
||||||
|
return strings.Replace(routePattern, "/*/", "/", -1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// RouteContext returns chi's routing Context object from a
|
||||||
|
// http.Request Context.
|
||||||
|
func RouteContext(ctx context.Context) *Context {
|
||||||
|
return ctx.Value(RouteCtxKey).(*Context)
|
||||||
|
}
|
||||||
|
|
||||||
|
// URLParam returns the url parameter from a http.Request object.
|
||||||
|
func URLParam(r *http.Request, key string) string {
|
||||||
|
if rctx := RouteContext(r.Context()); rctx != nil {
|
||||||
|
return rctx.URLParam(key)
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// URLParamFromCtx returns the url parameter from a http.Request Context.
|
||||||
|
func URLParamFromCtx(ctx context.Context, key string) string {
|
||||||
|
if rctx := RouteContext(ctx); rctx != nil {
|
||||||
|
return rctx.URLParam(key)
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// RouteParams is a structure to track URL routing parameters efficiently.
|
||||||
|
type RouteParams struct {
|
||||||
|
Keys, Values []string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add will append a URL parameter to the end of the route param
|
||||||
|
func (s *RouteParams) Add(key, value string) {
|
||||||
|
(*s).Keys = append((*s).Keys, key)
|
||||||
|
(*s).Values = append((*s).Values, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ServerBaseContext wraps an http.Handler to set the request context to the
|
||||||
|
// `baseCtx`.
|
||||||
|
func ServerBaseContext(baseCtx context.Context, h http.Handler) http.Handler {
|
||||||
|
fn := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
ctx := r.Context()
|
||||||
|
baseCtx := baseCtx
|
||||||
|
|
||||||
|
// Copy over default net/http server context keys
|
||||||
|
if v, ok := ctx.Value(http.ServerContextKey).(*http.Server); ok {
|
||||||
|
baseCtx = context.WithValue(baseCtx, http.ServerContextKey, v)
|
||||||
|
}
|
||||||
|
if v, ok := ctx.Value(http.LocalAddrContextKey).(net.Addr); ok {
|
||||||
|
baseCtx = context.WithValue(baseCtx, http.LocalAddrContextKey, v)
|
||||||
|
}
|
||||||
|
|
||||||
|
h.ServeHTTP(w, r.WithContext(baseCtx))
|
||||||
|
})
|
||||||
|
return fn
|
||||||
|
}
|
||||||
|
|
||||||
|
// contextKey is a value for use with context.WithValue. It's used as
|
||||||
|
// a pointer so it fits in an interface{} without allocation. This technique
|
||||||
|
// for defining context keys was copied from Go 1.7's new use of context in net/http.
|
||||||
|
type contextKey struct {
|
||||||
|
name string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (k *contextKey) String() string {
|
||||||
|
return "chi context value " + k.name
|
||||||
|
}
|
460
vendor/github.com/go-chi/chi/mux.go
generated
vendored
Normal file
460
vendor/github.com/go-chi/chi/mux.go
generated
vendored
Normal file
@ -0,0 +1,460 @@
|
|||||||
|
package chi
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
)
|
||||||
|
|
||||||
|
var _ Router = &Mux{}
|
||||||
|
|
||||||
|
// Mux is a simple HTTP route multiplexer that parses a request path,
|
||||||
|
// records any URL params, and executes an end handler. It implements
|
||||||
|
// the http.Handler interface and is friendly with the standard library.
|
||||||
|
//
|
||||||
|
// Mux is designed to be fast, minimal and offer a powerful API for building
|
||||||
|
// modular and composable HTTP services with a large set of handlers. It's
|
||||||
|
// particularly useful for writing large REST API services that break a handler
|
||||||
|
// into many smaller parts composed of middlewares and end handlers.
|
||||||
|
type Mux struct {
|
||||||
|
// The radix trie router
|
||||||
|
tree *node
|
||||||
|
|
||||||
|
// The middleware stack
|
||||||
|
middlewares []func(http.Handler) http.Handler
|
||||||
|
|
||||||
|
// Controls the behaviour of middleware chain generation when a mux
|
||||||
|
// is registered as an inline group inside another mux.
|
||||||
|
inline bool
|
||||||
|
parent *Mux
|
||||||
|
|
||||||
|
// The computed mux handler made of the chained middleware stack and
|
||||||
|
// the tree router
|
||||||
|
handler http.Handler
|
||||||
|
|
||||||
|
// Routing context pool
|
||||||
|
pool *sync.Pool
|
||||||
|
|
||||||
|
// Custom route not found handler
|
||||||
|
notFoundHandler http.HandlerFunc
|
||||||
|
|
||||||
|
// Custom method not allowed handler
|
||||||
|
methodNotAllowedHandler http.HandlerFunc
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewMux returns a newly initialized Mux object that implements the Router
|
||||||
|
// interface.
|
||||||
|
func NewMux() *Mux {
|
||||||
|
mux := &Mux{tree: &node{}, pool: &sync.Pool{}}
|
||||||
|
mux.pool.New = func() interface{} {
|
||||||
|
return NewRouteContext()
|
||||||
|
}
|
||||||
|
return mux
|
||||||
|
}
|
||||||
|
|
||||||
|
// ServeHTTP is the single method of the http.Handler interface that makes
|
||||||
|
// Mux interoperable with the standard library. It uses a sync.Pool to get and
|
||||||
|
// reuse routing contexts for each request.
|
||||||
|
func (mx *Mux) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||||
|
// Ensure the mux has some routes defined on the mux
|
||||||
|
if mx.handler == nil {
|
||||||
|
mx.NotFoundHandler().ServeHTTP(w, r)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if a routing context already exists from a parent router.
|
||||||
|
rctx, _ := r.Context().Value(RouteCtxKey).(*Context)
|
||||||
|
if rctx != nil {
|
||||||
|
mx.handler.ServeHTTP(w, r)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch a RouteContext object from the sync pool, and call the computed
|
||||||
|
// mx.handler that is comprised of mx.middlewares + mx.routeHTTP.
|
||||||
|
// Once the request is finished, reset the routing context and put it back
|
||||||
|
// into the pool for reuse from another request.
|
||||||
|
rctx = mx.pool.Get().(*Context)
|
||||||
|
rctx.Reset()
|
||||||
|
rctx.Routes = mx
|
||||||
|
r = r.WithContext(context.WithValue(r.Context(), RouteCtxKey, rctx))
|
||||||
|
mx.handler.ServeHTTP(w, r)
|
||||||
|
mx.pool.Put(rctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use appends a middleware handler to the Mux middleware stack.
|
||||||
|
//
|
||||||
|
// The middleware stack for any Mux will execute before searching for a matching
|
||||||
|
// route to a specific handler, which provides opportunity to respond early,
|
||||||
|
// change the course of the request execution, or set request-scoped values for
|
||||||
|
// the next http.Handler.
|
||||||
|
func (mx *Mux) Use(middlewares ...func(http.Handler) http.Handler) {
|
||||||
|
if mx.handler != nil {
|
||||||
|
panic("chi: all middlewares must be defined before routes on a mux")
|
||||||
|
}
|
||||||
|
mx.middlewares = append(mx.middlewares, middlewares...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle adds the route `pattern` that matches any http method to
|
||||||
|
// execute the `handler` http.Handler.
|
||||||
|
func (mx *Mux) Handle(pattern string, handler http.Handler) {
|
||||||
|
mx.handle(mALL, pattern, handler)
|
||||||
|
}
|
||||||
|
|
||||||
|
// HandleFunc adds the route `pattern` that matches any http method to
|
||||||
|
// execute the `handlerFn` http.HandlerFunc.
|
||||||
|
func (mx *Mux) HandleFunc(pattern string, handlerFn http.HandlerFunc) {
|
||||||
|
mx.handle(mALL, pattern, handlerFn)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Method adds the route `pattern` that matches `method` http method to
|
||||||
|
// execute the `handler` http.Handler.
|
||||||
|
func (mx *Mux) Method(method, pattern string, handler http.Handler) {
|
||||||
|
m, ok := methodMap[strings.ToUpper(method)]
|
||||||
|
if !ok {
|
||||||
|
panic(fmt.Sprintf("chi: '%s' http method is not supported.", method))
|
||||||
|
}
|
||||||
|
mx.handle(m, pattern, handler)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MethodFunc adds the route `pattern` that matches `method` http method to
|
||||||
|
// execute the `handlerFn` http.HandlerFunc.
|
||||||
|
func (mx *Mux) MethodFunc(method, pattern string, handlerFn http.HandlerFunc) {
|
||||||
|
mx.Method(method, pattern, handlerFn)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Connect adds the route `pattern` that matches a CONNECT http method to
|
||||||
|
// execute the `handlerFn` http.HandlerFunc.
|
||||||
|
func (mx *Mux) Connect(pattern string, handlerFn http.HandlerFunc) {
|
||||||
|
mx.handle(mCONNECT, pattern, handlerFn)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete adds the route `pattern` that matches a DELETE http method to
|
||||||
|
// execute the `handlerFn` http.HandlerFunc.
|
||||||
|
func (mx *Mux) Delete(pattern string, handlerFn http.HandlerFunc) {
|
||||||
|
mx.handle(mDELETE, pattern, handlerFn)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get adds the route `pattern` that matches a GET http method to
|
||||||
|
// execute the `handlerFn` http.HandlerFunc.
|
||||||
|
func (mx *Mux) Get(pattern string, handlerFn http.HandlerFunc) {
|
||||||
|
mx.handle(mGET, pattern, handlerFn)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Head adds the route `pattern` that matches a HEAD http method to
|
||||||
|
// execute the `handlerFn` http.HandlerFunc.
|
||||||
|
func (mx *Mux) Head(pattern string, handlerFn http.HandlerFunc) {
|
||||||
|
mx.handle(mHEAD, pattern, handlerFn)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Options adds the route `pattern` that matches a OPTIONS http method to
|
||||||
|
// execute the `handlerFn` http.HandlerFunc.
|
||||||
|
func (mx *Mux) Options(pattern string, handlerFn http.HandlerFunc) {
|
||||||
|
mx.handle(mOPTIONS, pattern, handlerFn)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Patch adds the route `pattern` that matches a PATCH http method to
|
||||||
|
// execute the `handlerFn` http.HandlerFunc.
|
||||||
|
func (mx *Mux) Patch(pattern string, handlerFn http.HandlerFunc) {
|
||||||
|
mx.handle(mPATCH, pattern, handlerFn)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Post adds the route `pattern` that matches a POST http method to
|
||||||
|
// execute the `handlerFn` http.HandlerFunc.
|
||||||
|
func (mx *Mux) Post(pattern string, handlerFn http.HandlerFunc) {
|
||||||
|
mx.handle(mPOST, pattern, handlerFn)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Put adds the route `pattern` that matches a PUT http method to
|
||||||
|
// execute the `handlerFn` http.HandlerFunc.
|
||||||
|
func (mx *Mux) Put(pattern string, handlerFn http.HandlerFunc) {
|
||||||
|
mx.handle(mPUT, pattern, handlerFn)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Trace adds the route `pattern` that matches a TRACE http method to
|
||||||
|
// execute the `handlerFn` http.HandlerFunc.
|
||||||
|
func (mx *Mux) Trace(pattern string, handlerFn http.HandlerFunc) {
|
||||||
|
mx.handle(mTRACE, pattern, handlerFn)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NotFound sets a custom http.HandlerFunc for routing paths that could
|
||||||
|
// not be found. The default 404 handler is `http.NotFound`.
|
||||||
|
func (mx *Mux) NotFound(handlerFn http.HandlerFunc) {
|
||||||
|
// Build NotFound handler chain
|
||||||
|
m := mx
|
||||||
|
hFn := handlerFn
|
||||||
|
if mx.inline && mx.parent != nil {
|
||||||
|
m = mx.parent
|
||||||
|
hFn = Chain(mx.middlewares...).HandlerFunc(hFn).ServeHTTP
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update the notFoundHandler from this point forward
|
||||||
|
m.notFoundHandler = hFn
|
||||||
|
m.updateSubRoutes(func(subMux *Mux) {
|
||||||
|
if subMux.notFoundHandler == nil {
|
||||||
|
subMux.NotFound(hFn)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// MethodNotAllowed sets a custom http.HandlerFunc for routing paths where the
|
||||||
|
// method is unresolved. The default handler returns a 405 with an empty body.
|
||||||
|
func (mx *Mux) MethodNotAllowed(handlerFn http.HandlerFunc) {
|
||||||
|
// Build MethodNotAllowed handler chain
|
||||||
|
m := mx
|
||||||
|
hFn := handlerFn
|
||||||
|
if mx.inline && mx.parent != nil {
|
||||||
|
m = mx.parent
|
||||||
|
hFn = Chain(mx.middlewares...).HandlerFunc(hFn).ServeHTTP
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update the methodNotAllowedHandler from this point forward
|
||||||
|
m.methodNotAllowedHandler = hFn
|
||||||
|
m.updateSubRoutes(func(subMux *Mux) {
|
||||||
|
if subMux.methodNotAllowedHandler == nil {
|
||||||
|
subMux.MethodNotAllowed(hFn)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// With adds inline middlewares for an endpoint handler.
|
||||||
|
func (mx *Mux) With(middlewares ...func(http.Handler) http.Handler) Router {
|
||||||
|
// Similarly as in handle(), we must build the mux handler once further
|
||||||
|
// middleware registration isn't allowed for this stack, like now.
|
||||||
|
if !mx.inline && mx.handler == nil {
|
||||||
|
mx.buildRouteHandler()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copy middlewares from parent inline muxs
|
||||||
|
var mws Middlewares
|
||||||
|
if mx.inline {
|
||||||
|
mws = make(Middlewares, len(mx.middlewares))
|
||||||
|
copy(mws, mx.middlewares)
|
||||||
|
}
|
||||||
|
mws = append(mws, middlewares...)
|
||||||
|
|
||||||
|
im := &Mux{pool: mx.pool, inline: true, parent: mx, tree: mx.tree, middlewares: mws}
|
||||||
|
|
||||||
|
return im
|
||||||
|
}
|
||||||
|
|
||||||
|
// Group creates a new inline-Mux with a fresh middleware stack. It's useful
|
||||||
|
// for a group of handlers along the same routing path that use an additional
|
||||||
|
// set of middlewares. See _examples/.
|
||||||
|
func (mx *Mux) Group(fn func(r Router)) Router {
|
||||||
|
im := mx.With().(*Mux)
|
||||||
|
if fn != nil {
|
||||||
|
fn(im)
|
||||||
|
}
|
||||||
|
return im
|
||||||
|
}
|
||||||
|
|
||||||
|
// Route creates a new Mux with a fresh middleware stack and mounts it
|
||||||
|
// along the `pattern` as a subrouter. Effectively, this is a short-hand
|
||||||
|
// call to Mount. See _examples/.
|
||||||
|
func (mx *Mux) Route(pattern string, fn func(r Router)) Router {
|
||||||
|
subRouter := NewRouter()
|
||||||
|
if fn != nil {
|
||||||
|
fn(subRouter)
|
||||||
|
}
|
||||||
|
mx.Mount(pattern, subRouter)
|
||||||
|
return subRouter
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mount attaches another http.Handler or chi Router as a subrouter along a routing
|
||||||
|
// path. It's very useful to split up a large API as many independent routers and
|
||||||
|
// compose them as a single service using Mount. See _examples/.
|
||||||
|
//
|
||||||
|
// Note that Mount() simply sets a wildcard along the `pattern` that will continue
|
||||||
|
// routing at the `handler`, which in most cases is another chi.Router. As a result,
|
||||||
|
// if you define two Mount() routes on the exact same pattern the mount will panic.
|
||||||
|
func (mx *Mux) Mount(pattern string, handler http.Handler) {
|
||||||
|
// Provide runtime safety for ensuring a pattern isn't mounted on an existing
|
||||||
|
// routing pattern.
|
||||||
|
if mx.tree.findPattern(pattern+"*") || mx.tree.findPattern(pattern+"/*") {
|
||||||
|
panic(fmt.Sprintf("chi: attempting to Mount() a handler on an existing path, '%s'", pattern))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Assign sub-Router's with the parent not found & method not allowed handler if not specified.
|
||||||
|
subr, ok := handler.(*Mux)
|
||||||
|
if ok && subr.notFoundHandler == nil && mx.notFoundHandler != nil {
|
||||||
|
subr.NotFound(mx.notFoundHandler)
|
||||||
|
}
|
||||||
|
if ok && subr.methodNotAllowedHandler == nil && mx.methodNotAllowedHandler != nil {
|
||||||
|
subr.MethodNotAllowed(mx.methodNotAllowedHandler)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wrap the sub-router in a handlerFunc to scope the request path for routing.
|
||||||
|
mountHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
rctx := RouteContext(r.Context())
|
||||||
|
rctx.RoutePath = mx.nextRoutePath(rctx)
|
||||||
|
handler.ServeHTTP(w, r)
|
||||||
|
})
|
||||||
|
|
||||||
|
if pattern == "" || pattern[len(pattern)-1] != '/' {
|
||||||
|
mx.handle(mALL|mSTUB, pattern, mountHandler)
|
||||||
|
mx.handle(mALL|mSTUB, pattern+"/", mountHandler)
|
||||||
|
pattern += "/"
|
||||||
|
}
|
||||||
|
|
||||||
|
method := mALL
|
||||||
|
subroutes, _ := handler.(Routes)
|
||||||
|
if subroutes != nil {
|
||||||
|
method |= mSTUB
|
||||||
|
}
|
||||||
|
n := mx.handle(method, pattern+"*", mountHandler)
|
||||||
|
|
||||||
|
if subroutes != nil {
|
||||||
|
n.subroutes = subroutes
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Routes returns a slice of routing information from the tree,
|
||||||
|
// useful for traversing available routes of a router.
|
||||||
|
func (mx *Mux) Routes() []Route {
|
||||||
|
return mx.tree.routes()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Middlewares returns a slice of middleware handler functions.
|
||||||
|
func (mx *Mux) Middlewares() Middlewares {
|
||||||
|
return mx.middlewares
|
||||||
|
}
|
||||||
|
|
||||||
|
// Match searches the routing tree for a handler that matches the method/path.
|
||||||
|
// It's similar to routing a http request, but without executing the handler
|
||||||
|
// thereafter.
|
||||||
|
//
|
||||||
|
// Note: the *Context state is updated during execution, so manage
|
||||||
|
// the state carefully or make a NewRouteContext().
|
||||||
|
func (mx *Mux) Match(rctx *Context, method, path string) bool {
|
||||||
|
m, ok := methodMap[method]
|
||||||
|
if !ok {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
node, _, h := mx.tree.FindRoute(rctx, m, path)
|
||||||
|
|
||||||
|
if node != nil && node.subroutes != nil {
|
||||||
|
rctx.RoutePath = mx.nextRoutePath(rctx)
|
||||||
|
return node.subroutes.Match(rctx, method, rctx.RoutePath)
|
||||||
|
}
|
||||||
|
|
||||||
|
return h != nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// NotFoundHandler returns the default Mux 404 responder whenever a route
|
||||||
|
// cannot be found.
|
||||||
|
func (mx *Mux) NotFoundHandler() http.HandlerFunc {
|
||||||
|
if mx.notFoundHandler != nil {
|
||||||
|
return mx.notFoundHandler
|
||||||
|
}
|
||||||
|
return http.NotFound
|
||||||
|
}
|
||||||
|
|
||||||
|
// MethodNotAllowedHandler returns the default Mux 405 responder whenever
|
||||||
|
// a method cannot be resolved for a route.
|
||||||
|
func (mx *Mux) MethodNotAllowedHandler() http.HandlerFunc {
|
||||||
|
if mx.methodNotAllowedHandler != nil {
|
||||||
|
return mx.methodNotAllowedHandler
|
||||||
|
}
|
||||||
|
return methodNotAllowedHandler
|
||||||
|
}
|
||||||
|
|
||||||
|
// buildRouteHandler builds the single mux handler that is a chain of the middleware
|
||||||
|
// stack, as defined by calls to Use(), and the tree router (Mux) itself. After this
|
||||||
|
// point, no other middlewares can be registered on this Mux's stack. But you can still
|
||||||
|
// compose additional middlewares via Group()'s or using a chained middleware handler.
|
||||||
|
func (mx *Mux) buildRouteHandler() {
|
||||||
|
mx.handler = chain(mx.middlewares, http.HandlerFunc(mx.routeHTTP))
|
||||||
|
}
|
||||||
|
|
||||||
|
// handle registers a http.Handler in the routing tree for a particular http method
|
||||||
|
// and routing pattern.
|
||||||
|
func (mx *Mux) handle(method methodTyp, pattern string, handler http.Handler) *node {
|
||||||
|
if len(pattern) == 0 || pattern[0] != '/' {
|
||||||
|
panic(fmt.Sprintf("chi: routing pattern must begin with '/' in '%s'", pattern))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build the final routing handler for this Mux.
|
||||||
|
if !mx.inline && mx.handler == nil {
|
||||||
|
mx.buildRouteHandler()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build endpoint handler with inline middlewares for the route
|
||||||
|
var h http.Handler
|
||||||
|
if mx.inline {
|
||||||
|
mx.handler = http.HandlerFunc(mx.routeHTTP)
|
||||||
|
h = Chain(mx.middlewares...).Handler(handler)
|
||||||
|
} else {
|
||||||
|
h = handler
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add the endpoint to the tree and return the node
|
||||||
|
return mx.tree.InsertRoute(method, pattern, h)
|
||||||
|
}
|
||||||
|
|
||||||
|
// routeHTTP routes a http.Request through the Mux routing tree to serve
|
||||||
|
// the matching handler for a particular http method.
|
||||||
|
func (mx *Mux) routeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||||
|
// Grab the route context object
|
||||||
|
rctx := r.Context().Value(RouteCtxKey).(*Context)
|
||||||
|
|
||||||
|
// The request routing path
|
||||||
|
routePath := rctx.RoutePath
|
||||||
|
if routePath == "" {
|
||||||
|
if r.URL.RawPath != "" {
|
||||||
|
routePath = r.URL.RawPath
|
||||||
|
} else {
|
||||||
|
routePath = r.URL.Path
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if method is supported by chi
|
||||||
|
if rctx.RouteMethod == "" {
|
||||||
|
rctx.RouteMethod = r.Method
|
||||||
|
}
|
||||||
|
method, ok := methodMap[rctx.RouteMethod]
|
||||||
|
if !ok {
|
||||||
|
mx.MethodNotAllowedHandler().ServeHTTP(w, r)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find the route
|
||||||
|
if _, _, h := mx.tree.FindRoute(rctx, method, routePath); h != nil {
|
||||||
|
h.ServeHTTP(w, r)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if rctx.methodNotAllowed {
|
||||||
|
mx.MethodNotAllowedHandler().ServeHTTP(w, r)
|
||||||
|
} else {
|
||||||
|
mx.NotFoundHandler().ServeHTTP(w, r)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (mx *Mux) nextRoutePath(rctx *Context) string {
|
||||||
|
routePath := "/"
|
||||||
|
nx := len(rctx.routeParams.Keys) - 1 // index of last param in list
|
||||||
|
if nx >= 0 && rctx.routeParams.Keys[nx] == "*" && len(rctx.routeParams.Values) > nx {
|
||||||
|
routePath += rctx.routeParams.Values[nx]
|
||||||
|
}
|
||||||
|
return routePath
|
||||||
|
}
|
||||||
|
|
||||||
|
// Recursively update data on child routers.
|
||||||
|
func (mx *Mux) updateSubRoutes(fn func(subMux *Mux)) {
|
||||||
|
for _, r := range mx.tree.routes() {
|
||||||
|
subMux, ok := r.SubRoutes.(*Mux)
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
fn(subMux)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// methodNotAllowedHandler is a helper function to respond with a 405,
|
||||||
|
// method not allowed.
|
||||||
|
func methodNotAllowedHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.WriteHeader(405)
|
||||||
|
w.Write(nil)
|
||||||
|
}
|
847
vendor/github.com/go-chi/chi/tree.go
generated
vendored
Normal file
847
vendor/github.com/go-chi/chi/tree.go
generated
vendored
Normal file
@ -0,0 +1,847 @@
|
|||||||
|
package chi
|
||||||
|
|
||||||
|
// Radix tree implementation below is a based on the original work by
|
||||||
|
// Armon Dadgar in https://github.com/armon/go-radix/blob/master/radix.go
|
||||||
|
// (MIT licensed). It's been heavily modified for use as a HTTP routing tree.
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"math"
|
||||||
|
"net/http"
|
||||||
|
"regexp"
|
||||||
|
"sort"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
type methodTyp int
|
||||||
|
|
||||||
|
const (
|
||||||
|
mSTUB methodTyp = 1 << iota
|
||||||
|
mCONNECT
|
||||||
|
mDELETE
|
||||||
|
mGET
|
||||||
|
mHEAD
|
||||||
|
mOPTIONS
|
||||||
|
mPATCH
|
||||||
|
mPOST
|
||||||
|
mPUT
|
||||||
|
mTRACE
|
||||||
|
)
|
||||||
|
|
||||||
|
var mALL = mCONNECT | mDELETE | mGET | mHEAD |
|
||||||
|
mOPTIONS | mPATCH | mPOST | mPUT | mTRACE
|
||||||
|
|
||||||
|
var methodMap = map[string]methodTyp{
|
||||||
|
http.MethodConnect: mCONNECT,
|
||||||
|
http.MethodDelete: mDELETE,
|
||||||
|
http.MethodGet: mGET,
|
||||||
|
http.MethodHead: mHEAD,
|
||||||
|
http.MethodOptions: mOPTIONS,
|
||||||
|
http.MethodPatch: mPATCH,
|
||||||
|
http.MethodPost: mPOST,
|
||||||
|
http.MethodPut: mPUT,
|
||||||
|
http.MethodTrace: mTRACE,
|
||||||
|
}
|
||||||
|
|
||||||
|
// RegisterMethod adds support for custom HTTP method handlers, available
|
||||||
|
// via Router#Method and Router#MethodFunc
|
||||||
|
func RegisterMethod(method string) {
|
||||||
|
if method == "" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
method = strings.ToUpper(method)
|
||||||
|
if _, ok := methodMap[method]; ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
n := len(methodMap)
|
||||||
|
if n > strconv.IntSize {
|
||||||
|
panic(fmt.Sprintf("chi: max number of methods reached (%d)", strconv.IntSize))
|
||||||
|
}
|
||||||
|
mt := methodTyp(math.Exp2(float64(n)))
|
||||||
|
methodMap[method] = mt
|
||||||
|
mALL |= mt
|
||||||
|
}
|
||||||
|
|
||||||
|
type nodeTyp uint8
|
||||||
|
|
||||||
|
const (
|
||||||
|
ntStatic nodeTyp = iota // /home
|
||||||
|
ntRegexp // /{id:[0-9]+}
|
||||||
|
ntParam // /{user}
|
||||||
|
ntCatchAll // /api/v1/*
|
||||||
|
)
|
||||||
|
|
||||||
|
type node struct {
|
||||||
|
// node type: static, regexp, param, catchAll
|
||||||
|
typ nodeTyp
|
||||||
|
|
||||||
|
// first byte of the prefix
|
||||||
|
label byte
|
||||||
|
|
||||||
|
// first byte of the child prefix
|
||||||
|
tail byte
|
||||||
|
|
||||||
|
// prefix is the common prefix we ignore
|
||||||
|
prefix string
|
||||||
|
|
||||||
|
// regexp matcher for regexp nodes
|
||||||
|
rex *regexp.Regexp
|
||||||
|
|
||||||
|
// HTTP handler endpoints on the leaf node
|
||||||
|
endpoints endpoints
|
||||||
|
|
||||||
|
// subroutes on the leaf node
|
||||||
|
subroutes Routes
|
||||||
|
|
||||||
|
// child nodes should be stored in-order for iteration,
|
||||||
|
// in groups of the node type.
|
||||||
|
children [ntCatchAll + 1]nodes
|
||||||
|
}
|
||||||
|
|
||||||
|
// endpoints is a mapping of http method constants to handlers
|
||||||
|
// for a given route.
|
||||||
|
type endpoints map[methodTyp]*endpoint
|
||||||
|
|
||||||
|
type endpoint struct {
|
||||||
|
// endpoint handler
|
||||||
|
handler http.Handler
|
||||||
|
|
||||||
|
// pattern is the routing pattern for handler nodes
|
||||||
|
pattern string
|
||||||
|
|
||||||
|
// parameter keys recorded on handler nodes
|
||||||
|
paramKeys []string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s endpoints) Value(method methodTyp) *endpoint {
|
||||||
|
mh, ok := s[method]
|
||||||
|
if !ok {
|
||||||
|
mh = &endpoint{}
|
||||||
|
s[method] = mh
|
||||||
|
}
|
||||||
|
return mh
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *node) InsertRoute(method methodTyp, pattern string, handler http.Handler) *node {
|
||||||
|
var parent *node
|
||||||
|
search := pattern
|
||||||
|
|
||||||
|
for {
|
||||||
|
// Handle key exhaustion
|
||||||
|
if len(search) == 0 {
|
||||||
|
// Insert or update the node's leaf handler
|
||||||
|
n.setEndpoint(method, handler, pattern)
|
||||||
|
return n
|
||||||
|
}
|
||||||
|
|
||||||
|
// We're going to be searching for a wild node next,
|
||||||
|
// in this case, we need to get the tail
|
||||||
|
var label = search[0]
|
||||||
|
var segTail byte
|
||||||
|
var segEndIdx int
|
||||||
|
var segTyp nodeTyp
|
||||||
|
var segRexpat string
|
||||||
|
if label == '{' || label == '*' {
|
||||||
|
segTyp, _, segRexpat, segTail, _, segEndIdx = patNextSegment(search)
|
||||||
|
}
|
||||||
|
|
||||||
|
var prefix string
|
||||||
|
if segTyp == ntRegexp {
|
||||||
|
prefix = segRexpat
|
||||||
|
}
|
||||||
|
|
||||||
|
// Look for the edge to attach to
|
||||||
|
parent = n
|
||||||
|
n = n.getEdge(segTyp, label, segTail, prefix)
|
||||||
|
|
||||||
|
// No edge, create one
|
||||||
|
if n == nil {
|
||||||
|
child := &node{label: label, tail: segTail, prefix: search}
|
||||||
|
hn := parent.addChild(child, search)
|
||||||
|
hn.setEndpoint(method, handler, pattern)
|
||||||
|
|
||||||
|
return hn
|
||||||
|
}
|
||||||
|
|
||||||
|
// Found an edge to match the pattern
|
||||||
|
|
||||||
|
if n.typ > ntStatic {
|
||||||
|
// We found a param node, trim the param from the search path and continue.
|
||||||
|
// This param/wild pattern segment would already be on the tree from a previous
|
||||||
|
// call to addChild when creating a new node.
|
||||||
|
search = search[segEndIdx:]
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Static nodes fall below here.
|
||||||
|
// Determine longest prefix of the search key on match.
|
||||||
|
commonPrefix := longestPrefix(search, n.prefix)
|
||||||
|
if commonPrefix == len(n.prefix) {
|
||||||
|
// the common prefix is as long as the current node's prefix we're attempting to insert.
|
||||||
|
// keep the search going.
|
||||||
|
search = search[commonPrefix:]
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Split the node
|
||||||
|
child := &node{
|
||||||
|
typ: ntStatic,
|
||||||
|
prefix: search[:commonPrefix],
|
||||||
|
}
|
||||||
|
parent.replaceChild(search[0], segTail, child)
|
||||||
|
|
||||||
|
// Restore the existing node
|
||||||
|
n.label = n.prefix[commonPrefix]
|
||||||
|
n.prefix = n.prefix[commonPrefix:]
|
||||||
|
child.addChild(n, n.prefix)
|
||||||
|
|
||||||
|
// If the new key is a subset, set the method/handler on this node and finish.
|
||||||
|
search = search[commonPrefix:]
|
||||||
|
if len(search) == 0 {
|
||||||
|
child.setEndpoint(method, handler, pattern)
|
||||||
|
return child
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a new edge for the node
|
||||||
|
subchild := &node{
|
||||||
|
typ: ntStatic,
|
||||||
|
label: search[0],
|
||||||
|
prefix: search,
|
||||||
|
}
|
||||||
|
hn := child.addChild(subchild, search)
|
||||||
|
hn.setEndpoint(method, handler, pattern)
|
||||||
|
return hn
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// addChild appends the new `child` node to the tree using the `pattern` as the trie key.
|
||||||
|
// For a URL router like chi's, we split the static, param, regexp and wildcard segments
|
||||||
|
// into different nodes. In addition, addChild will recursively call itself until every
|
||||||
|
// pattern segment is added to the url pattern tree as individual nodes, depending on type.
|
||||||
|
func (n *node) addChild(child *node, prefix string) *node {
|
||||||
|
search := prefix
|
||||||
|
|
||||||
|
// handler leaf node added to the tree is the child.
|
||||||
|
// this may be overridden later down the flow
|
||||||
|
hn := child
|
||||||
|
|
||||||
|
// Parse next segment
|
||||||
|
segTyp, _, segRexpat, segTail, segStartIdx, segEndIdx := patNextSegment(search)
|
||||||
|
|
||||||
|
// Add child depending on next up segment
|
||||||
|
switch segTyp {
|
||||||
|
|
||||||
|
case ntStatic:
|
||||||
|
// Search prefix is all static (that is, has no params in path)
|
||||||
|
// noop
|
||||||
|
|
||||||
|
default:
|
||||||
|
// Search prefix contains a param, regexp or wildcard
|
||||||
|
|
||||||
|
if segTyp == ntRegexp {
|
||||||
|
rex, err := regexp.Compile(segRexpat)
|
||||||
|
if err != nil {
|
||||||
|
panic(fmt.Sprintf("chi: invalid regexp pattern '%s' in route param", segRexpat))
|
||||||
|
}
|
||||||
|
child.prefix = segRexpat
|
||||||
|
child.rex = rex
|
||||||
|
}
|
||||||
|
|
||||||
|
if segStartIdx == 0 {
|
||||||
|
// Route starts with a param
|
||||||
|
child.typ = segTyp
|
||||||
|
|
||||||
|
if segTyp == ntCatchAll {
|
||||||
|
segStartIdx = -1
|
||||||
|
} else {
|
||||||
|
segStartIdx = segEndIdx
|
||||||
|
}
|
||||||
|
if segStartIdx < 0 {
|
||||||
|
segStartIdx = len(search)
|
||||||
|
}
|
||||||
|
child.tail = segTail // for params, we set the tail
|
||||||
|
|
||||||
|
if segStartIdx != len(search) {
|
||||||
|
// add static edge for the remaining part, split the end.
|
||||||
|
// its not possible to have adjacent param nodes, so its certainly
|
||||||
|
// going to be a static node next.
|
||||||
|
|
||||||
|
search = search[segStartIdx:] // advance search position
|
||||||
|
|
||||||
|
nn := &node{
|
||||||
|
typ: ntStatic,
|
||||||
|
label: search[0],
|
||||||
|
prefix: search,
|
||||||
|
}
|
||||||
|
hn = child.addChild(nn, search)
|
||||||
|
}
|
||||||
|
|
||||||
|
} else if segStartIdx > 0 {
|
||||||
|
// Route has some param
|
||||||
|
|
||||||
|
// starts with a static segment
|
||||||
|
child.typ = ntStatic
|
||||||
|
child.prefix = search[:segStartIdx]
|
||||||
|
child.rex = nil
|
||||||
|
|
||||||
|
// add the param edge node
|
||||||
|
search = search[segStartIdx:]
|
||||||
|
|
||||||
|
nn := &node{
|
||||||
|
typ: segTyp,
|
||||||
|
label: search[0],
|
||||||
|
tail: segTail,
|
||||||
|
}
|
||||||
|
hn = child.addChild(nn, search)
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
n.children[child.typ] = append(n.children[child.typ], child)
|
||||||
|
n.children[child.typ].Sort()
|
||||||
|
return hn
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *node) replaceChild(label, tail byte, child *node) {
|
||||||
|
for i := 0; i < len(n.children[child.typ]); i++ {
|
||||||
|
if n.children[child.typ][i].label == label && n.children[child.typ][i].tail == tail {
|
||||||
|
n.children[child.typ][i] = child
|
||||||
|
n.children[child.typ][i].label = label
|
||||||
|
n.children[child.typ][i].tail = tail
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
panic("chi: replacing missing child")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *node) getEdge(ntyp nodeTyp, label, tail byte, prefix string) *node {
|
||||||
|
nds := n.children[ntyp]
|
||||||
|
for i := 0; i < len(nds); i++ {
|
||||||
|
if nds[i].label == label && nds[i].tail == tail {
|
||||||
|
if ntyp == ntRegexp && nds[i].prefix != prefix {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
return nds[i]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *node) setEndpoint(method methodTyp, handler http.Handler, pattern string) {
|
||||||
|
// Set the handler for the method type on the node
|
||||||
|
if n.endpoints == nil {
|
||||||
|
n.endpoints = make(endpoints, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
paramKeys := patParamKeys(pattern)
|
||||||
|
|
||||||
|
if method&mSTUB == mSTUB {
|
||||||
|
n.endpoints.Value(mSTUB).handler = handler
|
||||||
|
}
|
||||||
|
if method&mALL == mALL {
|
||||||
|
h := n.endpoints.Value(mALL)
|
||||||
|
h.handler = handler
|
||||||
|
h.pattern = pattern
|
||||||
|
h.paramKeys = paramKeys
|
||||||
|
for _, m := range methodMap {
|
||||||
|
h := n.endpoints.Value(m)
|
||||||
|
h.handler = handler
|
||||||
|
h.pattern = pattern
|
||||||
|
h.paramKeys = paramKeys
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
h := n.endpoints.Value(method)
|
||||||
|
h.handler = handler
|
||||||
|
h.pattern = pattern
|
||||||
|
h.paramKeys = paramKeys
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *node) FindRoute(rctx *Context, method methodTyp, path string) (*node, endpoints, http.Handler) {
|
||||||
|
// Reset the context routing pattern and params
|
||||||
|
rctx.routePattern = ""
|
||||||
|
rctx.routeParams.Keys = rctx.routeParams.Keys[:0]
|
||||||
|
rctx.routeParams.Values = rctx.routeParams.Values[:0]
|
||||||
|
|
||||||
|
// Find the routing handlers for the path
|
||||||
|
rn := n.findRoute(rctx, method, path)
|
||||||
|
if rn == nil {
|
||||||
|
return nil, nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Record the routing params in the request lifecycle
|
||||||
|
rctx.URLParams.Keys = append(rctx.URLParams.Keys, rctx.routeParams.Keys...)
|
||||||
|
rctx.URLParams.Values = append(rctx.URLParams.Values, rctx.routeParams.Values...)
|
||||||
|
|
||||||
|
// Record the routing pattern in the request lifecycle
|
||||||
|
if rn.endpoints[method].pattern != "" {
|
||||||
|
rctx.routePattern = rn.endpoints[method].pattern
|
||||||
|
rctx.RoutePatterns = append(rctx.RoutePatterns, rctx.routePattern)
|
||||||
|
}
|
||||||
|
|
||||||
|
return rn, rn.endpoints, rn.endpoints[method].handler
|
||||||
|
}
|
||||||
|
|
||||||
|
// Recursive edge traversal by checking all nodeTyp groups along the way.
|
||||||
|
// It's like searching through a multi-dimensional radix trie.
|
||||||
|
func (n *node) findRoute(rctx *Context, method methodTyp, path string) *node {
|
||||||
|
nn := n
|
||||||
|
search := path
|
||||||
|
|
||||||
|
for t, nds := range nn.children {
|
||||||
|
ntyp := nodeTyp(t)
|
||||||
|
if len(nds) == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
var xn *node
|
||||||
|
xsearch := search
|
||||||
|
|
||||||
|
var label byte
|
||||||
|
if search != "" {
|
||||||
|
label = search[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
switch ntyp {
|
||||||
|
case ntStatic:
|
||||||
|
xn = nds.findEdge(label)
|
||||||
|
if xn == nil || !strings.HasPrefix(xsearch, xn.prefix) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
xsearch = xsearch[len(xn.prefix):]
|
||||||
|
|
||||||
|
case ntParam, ntRegexp:
|
||||||
|
// short-circuit and return no matching route for empty param values
|
||||||
|
if xsearch == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// serially loop through each node grouped by the tail delimiter
|
||||||
|
for idx := 0; idx < len(nds); idx++ {
|
||||||
|
xn = nds[idx]
|
||||||
|
|
||||||
|
// label for param nodes is the delimiter byte
|
||||||
|
p := strings.IndexByte(xsearch, xn.tail)
|
||||||
|
|
||||||
|
if p < 0 {
|
||||||
|
if xn.tail == '/' {
|
||||||
|
p = len(xsearch)
|
||||||
|
} else {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ntyp == ntRegexp && xn.rex != nil {
|
||||||
|
if xn.rex.Match([]byte(xsearch[:p])) == false {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
} else if strings.IndexByte(xsearch[:p], '/') != -1 {
|
||||||
|
// avoid a match across path segments
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
rctx.routeParams.Values = append(rctx.routeParams.Values, xsearch[:p])
|
||||||
|
xsearch = xsearch[p:]
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
// catch-all nodes
|
||||||
|
rctx.routeParams.Values = append(rctx.routeParams.Values, search)
|
||||||
|
xn = nds[0]
|
||||||
|
xsearch = ""
|
||||||
|
}
|
||||||
|
|
||||||
|
if xn == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// did we find it yet?
|
||||||
|
if len(xsearch) == 0 {
|
||||||
|
if xn.isLeaf() {
|
||||||
|
h, _ := xn.endpoints[method]
|
||||||
|
if h != nil && h.handler != nil {
|
||||||
|
rctx.routeParams.Keys = append(rctx.routeParams.Keys, h.paramKeys...)
|
||||||
|
return xn
|
||||||
|
}
|
||||||
|
|
||||||
|
// flag that the routing context found a route, but not a corresponding
|
||||||
|
// supported method
|
||||||
|
rctx.methodNotAllowed = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// recursively find the next node..
|
||||||
|
fin := xn.findRoute(rctx, method, xsearch)
|
||||||
|
if fin != nil {
|
||||||
|
return fin
|
||||||
|
}
|
||||||
|
|
||||||
|
// Did not find final handler, let's remove the param here if it was set
|
||||||
|
if xn.typ > ntStatic {
|
||||||
|
if len(rctx.routeParams.Values) > 0 {
|
||||||
|
rctx.routeParams.Values = rctx.routeParams.Values[:len(rctx.routeParams.Values)-1]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *node) findEdge(ntyp nodeTyp, label byte) *node {
|
||||||
|
nds := n.children[ntyp]
|
||||||
|
num := len(nds)
|
||||||
|
idx := 0
|
||||||
|
|
||||||
|
switch ntyp {
|
||||||
|
case ntStatic, ntParam, ntRegexp:
|
||||||
|
i, j := 0, num-1
|
||||||
|
for i <= j {
|
||||||
|
idx = i + (j-i)/2
|
||||||
|
if label > nds[idx].label {
|
||||||
|
i = idx + 1
|
||||||
|
} else if label < nds[idx].label {
|
||||||
|
j = idx - 1
|
||||||
|
} else {
|
||||||
|
i = num // breaks cond
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if nds[idx].label != label {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return nds[idx]
|
||||||
|
|
||||||
|
default: // catch all
|
||||||
|
return nds[idx]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *node) isEmpty() bool {
|
||||||
|
for _, nds := range n.children {
|
||||||
|
if len(nds) > 0 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *node) isLeaf() bool {
|
||||||
|
return n.endpoints != nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *node) findPattern(pattern string) bool {
|
||||||
|
nn := n
|
||||||
|
for _, nds := range nn.children {
|
||||||
|
if len(nds) == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
n = nn.findEdge(nds[0].typ, pattern[0])
|
||||||
|
if n == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
var idx int
|
||||||
|
var xpattern string
|
||||||
|
|
||||||
|
switch n.typ {
|
||||||
|
case ntStatic:
|
||||||
|
idx = longestPrefix(pattern, n.prefix)
|
||||||
|
if idx < len(n.prefix) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
case ntParam, ntRegexp:
|
||||||
|
idx = strings.IndexByte(pattern, '}') + 1
|
||||||
|
|
||||||
|
case ntCatchAll:
|
||||||
|
idx = longestPrefix(pattern, "*")
|
||||||
|
|
||||||
|
default:
|
||||||
|
panic("chi: unknown node type")
|
||||||
|
}
|
||||||
|
|
||||||
|
xpattern = pattern[idx:]
|
||||||
|
if len(xpattern) == 0 {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return n.findPattern(xpattern)
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *node) routes() []Route {
|
||||||
|
rts := []Route{}
|
||||||
|
|
||||||
|
n.walk(func(eps endpoints, subroutes Routes) bool {
|
||||||
|
if eps[mSTUB] != nil && eps[mSTUB].handler != nil && subroutes == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Group methodHandlers by unique patterns
|
||||||
|
pats := make(map[string]endpoints, 0)
|
||||||
|
|
||||||
|
for mt, h := range eps {
|
||||||
|
if h.pattern == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
p, ok := pats[h.pattern]
|
||||||
|
if !ok {
|
||||||
|
p = endpoints{}
|
||||||
|
pats[h.pattern] = p
|
||||||
|
}
|
||||||
|
p[mt] = h
|
||||||
|
}
|
||||||
|
|
||||||
|
for p, mh := range pats {
|
||||||
|
hs := make(map[string]http.Handler, 0)
|
||||||
|
if mh[mALL] != nil && mh[mALL].handler != nil {
|
||||||
|
hs["*"] = mh[mALL].handler
|
||||||
|
}
|
||||||
|
|
||||||
|
for mt, h := range mh {
|
||||||
|
if h.handler == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
m := methodTypString(mt)
|
||||||
|
if m == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
hs[m] = h.handler
|
||||||
|
}
|
||||||
|
|
||||||
|
rt := Route{p, hs, subroutes}
|
||||||
|
rts = append(rts, rt)
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
})
|
||||||
|
|
||||||
|
return rts
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *node) walk(fn func(eps endpoints, subroutes Routes) bool) bool {
|
||||||
|
// Visit the leaf values if any
|
||||||
|
if (n.endpoints != nil || n.subroutes != nil) && fn(n.endpoints, n.subroutes) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Recurse on the children
|
||||||
|
for _, ns := range n.children {
|
||||||
|
for _, cn := range ns {
|
||||||
|
if cn.walk(fn) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// patNextSegment returns the next segment details from a pattern:
|
||||||
|
// node type, param key, regexp string, param tail byte, param starting index, param ending index
|
||||||
|
func patNextSegment(pattern string) (nodeTyp, string, string, byte, int, int) {
|
||||||
|
ps := strings.Index(pattern, "{")
|
||||||
|
ws := strings.Index(pattern, "*")
|
||||||
|
|
||||||
|
if ps < 0 && ws < 0 {
|
||||||
|
return ntStatic, "", "", 0, 0, len(pattern) // we return the entire thing
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sanity check
|
||||||
|
if ws >= 0 && ws != len(pattern)-1 {
|
||||||
|
panic("chi: wildcard '*' must be the last value in a route. trim trailing text or use a '{param}' instead")
|
||||||
|
}
|
||||||
|
if ps >= 0 && ws >= 0 && ws < ps {
|
||||||
|
panic("chi: wildcard '*' must be the last pattern in a route, otherwise use a '{param}'")
|
||||||
|
}
|
||||||
|
|
||||||
|
var tail byte = '/' // Default endpoint tail to / byte
|
||||||
|
|
||||||
|
if ps >= 0 {
|
||||||
|
// Param/Regexp pattern is next
|
||||||
|
nt := ntParam
|
||||||
|
|
||||||
|
// Read to closing } taking into account opens and closes in curl count (cc)
|
||||||
|
cc := 0
|
||||||
|
pe := ps
|
||||||
|
for i, c := range pattern[ps:] {
|
||||||
|
if c == '{' {
|
||||||
|
cc++
|
||||||
|
} else if c == '}' {
|
||||||
|
cc--
|
||||||
|
if cc == 0 {
|
||||||
|
pe = ps + i
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if pe == ps {
|
||||||
|
panic("chi: route param closing delimiter '}' is missing")
|
||||||
|
}
|
||||||
|
|
||||||
|
key := pattern[ps+1 : pe]
|
||||||
|
pe++ // set end to next position
|
||||||
|
|
||||||
|
if pe < len(pattern) {
|
||||||
|
tail = pattern[pe]
|
||||||
|
}
|
||||||
|
|
||||||
|
var rexpat string
|
||||||
|
if idx := strings.Index(key, ":"); idx >= 0 {
|
||||||
|
nt = ntRegexp
|
||||||
|
rexpat = key[idx+1:]
|
||||||
|
key = key[:idx]
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(rexpat) > 0 {
|
||||||
|
if rexpat[0] != '^' {
|
||||||
|
rexpat = "^" + rexpat
|
||||||
|
}
|
||||||
|
if rexpat[len(rexpat)-1] != '$' {
|
||||||
|
rexpat = rexpat + "$"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nt, key, rexpat, tail, ps, pe
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wildcard pattern as finale
|
||||||
|
// TODO: should we panic if there is stuff after the * ???
|
||||||
|
return ntCatchAll, "*", "", 0, ws, len(pattern)
|
||||||
|
}
|
||||||
|
|
||||||
|
func patParamKeys(pattern string) []string {
|
||||||
|
pat := pattern
|
||||||
|
paramKeys := []string{}
|
||||||
|
for {
|
||||||
|
ptyp, paramKey, _, _, _, e := patNextSegment(pat)
|
||||||
|
if ptyp == ntStatic {
|
||||||
|
return paramKeys
|
||||||
|
}
|
||||||
|
for i := 0; i < len(paramKeys); i++ {
|
||||||
|
if paramKeys[i] == paramKey {
|
||||||
|
panic(fmt.Sprintf("chi: routing pattern '%s' contains duplicate param key, '%s'", pattern, paramKey))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
paramKeys = append(paramKeys, paramKey)
|
||||||
|
pat = pat[e:]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// longestPrefix finds the length of the shared prefix
|
||||||
|
// of two strings
|
||||||
|
func longestPrefix(k1, k2 string) int {
|
||||||
|
max := len(k1)
|
||||||
|
if l := len(k2); l < max {
|
||||||
|
max = l
|
||||||
|
}
|
||||||
|
var i int
|
||||||
|
for i = 0; i < max; i++ {
|
||||||
|
if k1[i] != k2[i] {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return i
|
||||||
|
}
|
||||||
|
|
||||||
|
func methodTypString(method methodTyp) string {
|
||||||
|
for s, t := range methodMap {
|
||||||
|
if method == t {
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
type nodes []*node
|
||||||
|
|
||||||
|
// Sort the list of nodes by label
|
||||||
|
func (ns nodes) Sort() { sort.Sort(ns); ns.tailSort() }
|
||||||
|
func (ns nodes) Len() int { return len(ns) }
|
||||||
|
func (ns nodes) Swap(i, j int) { ns[i], ns[j] = ns[j], ns[i] }
|
||||||
|
func (ns nodes) Less(i, j int) bool { return ns[i].label < ns[j].label }
|
||||||
|
|
||||||
|
// tailSort pushes nodes with '/' as the tail to the end of the list for param nodes.
|
||||||
|
// The list order determines the traversal order.
|
||||||
|
func (ns nodes) tailSort() {
|
||||||
|
for i := len(ns) - 1; i >= 0; i-- {
|
||||||
|
if ns[i].typ > ntStatic && ns[i].tail == '/' {
|
||||||
|
ns.Swap(i, len(ns)-1)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ns nodes) findEdge(label byte) *node {
|
||||||
|
num := len(ns)
|
||||||
|
idx := 0
|
||||||
|
i, j := 0, num-1
|
||||||
|
for i <= j {
|
||||||
|
idx = i + (j-i)/2
|
||||||
|
if label > ns[idx].label {
|
||||||
|
i = idx + 1
|
||||||
|
} else if label < ns[idx].label {
|
||||||
|
j = idx - 1
|
||||||
|
} else {
|
||||||
|
i = num // breaks cond
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ns[idx].label != label {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return ns[idx]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Route describes the details of a routing handler.
|
||||||
|
type Route struct {
|
||||||
|
Pattern string
|
||||||
|
Handlers map[string]http.Handler
|
||||||
|
SubRoutes Routes
|
||||||
|
}
|
||||||
|
|
||||||
|
// WalkFunc is the type of the function called for each method and route visited by Walk.
|
||||||
|
type WalkFunc func(method string, route string, handler http.Handler, middlewares ...func(http.Handler) http.Handler) error
|
||||||
|
|
||||||
|
// Walk walks any router tree that implements Routes interface.
|
||||||
|
func Walk(r Routes, walkFn WalkFunc) error {
|
||||||
|
return walk(r, walkFn, "")
|
||||||
|
}
|
||||||
|
|
||||||
|
func walk(r Routes, walkFn WalkFunc, parentRoute string, parentMw ...func(http.Handler) http.Handler) error {
|
||||||
|
for _, route := range r.Routes() {
|
||||||
|
mws := make([]func(http.Handler) http.Handler, len(parentMw))
|
||||||
|
copy(mws, parentMw)
|
||||||
|
mws = append(mws, r.Middlewares()...)
|
||||||
|
|
||||||
|
if route.SubRoutes != nil {
|
||||||
|
if err := walk(route.SubRoutes, walkFn, parentRoute+route.Pattern, mws...); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
for method, handler := range route.Handlers {
|
||||||
|
if method == "*" {
|
||||||
|
// Ignore a "catchAll" method, since we pass down all the specific methods for each route.
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
fullRoute := parentRoute + route.Pattern
|
||||||
|
|
||||||
|
if chain, ok := handler.(*ChainHandler); ok {
|
||||||
|
if err := walkFn(method, fullRoute, chain.Endpoint, append(mws, chain.Middlewares...)...); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if err := walkFn(method, fullRoute, handler, mws...); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
9
vendor/github.com/google/uuid/.travis.yml
generated
vendored
Normal file
9
vendor/github.com/google/uuid/.travis.yml
generated
vendored
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
language: go
|
||||||
|
|
||||||
|
go:
|
||||||
|
- 1.4.3
|
||||||
|
- 1.5.3
|
||||||
|
- tip
|
||||||
|
|
||||||
|
script:
|
||||||
|
- go test -v ./...
|
10
vendor/github.com/google/uuid/CONTRIBUTING.md
generated
vendored
Normal file
10
vendor/github.com/google/uuid/CONTRIBUTING.md
generated
vendored
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
# How to contribute
|
||||||
|
|
||||||
|
We definitely welcome patches and contribution to this project!
|
||||||
|
|
||||||
|
### Legal requirements
|
||||||
|
|
||||||
|
In order to protect both you and ourselves, you will need to sign the
|
||||||
|
[Contributor License Agreement](https://cla.developers.google.com/clas).
|
||||||
|
|
||||||
|
You may have already signed it for other Google projects.
|
9
vendor/github.com/google/uuid/CONTRIBUTORS
generated
vendored
Normal file
9
vendor/github.com/google/uuid/CONTRIBUTORS
generated
vendored
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
Paul Borman <borman@google.com>
|
||||||
|
bmatsuo
|
||||||
|
shawnps
|
||||||
|
theory
|
||||||
|
jboverfelt
|
||||||
|
dsymonds
|
||||||
|
cd1
|
||||||
|
wallclockbuilder
|
||||||
|
dansouza
|
27
vendor/github.com/google/uuid/LICENSE
generated
vendored
Normal file
27
vendor/github.com/google/uuid/LICENSE
generated
vendored
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
Copyright (c) 2009,2014 Google Inc. All rights reserved.
|
||||||
|
|
||||||
|
Redistribution and use in source and binary forms, with or without
|
||||||
|
modification, are permitted provided that the following conditions are
|
||||||
|
met:
|
||||||
|
|
||||||
|
* Redistributions of source code must retain the above copyright
|
||||||
|
notice, this list of conditions and the following disclaimer.
|
||||||
|
* Redistributions in binary form must reproduce the above
|
||||||
|
copyright notice, this list of conditions and the following disclaimer
|
||||||
|
in the documentation and/or other materials provided with the
|
||||||
|
distribution.
|
||||||
|
* Neither the name of Google Inc. nor the names of its
|
||||||
|
contributors may be used to endorse or promote products derived from
|
||||||
|
this software without specific prior written permission.
|
||||||
|
|
||||||
|
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||||
|
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||||
|
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||||
|
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||||
|
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||||
|
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||||||
|
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||||
|
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||||
|
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||||
|
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||||
|
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
19
vendor/github.com/google/uuid/README.md
generated
vendored
Normal file
19
vendor/github.com/google/uuid/README.md
generated
vendored
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
# uuid 
|
||||||
|
The uuid package generates and inspects UUIDs based on
|
||||||
|
[RFC 4122](http://tools.ietf.org/html/rfc4122)
|
||||||
|
and DCE 1.1: Authentication and Security Services.
|
||||||
|
|
||||||
|
This package is based on the github.com/pborman/uuid package (previously named
|
||||||
|
code.google.com/p/go-uuid). It differs from these earlier packages in that
|
||||||
|
a UUID is a 16 byte array rather than a byte slice. One loss due to this
|
||||||
|
change is the ability to represent an invalid UUID (vs a NIL UUID).
|
||||||
|
|
||||||
|
###### Install
|
||||||
|
`go get github.com/google/uuid`
|
||||||
|
|
||||||
|
###### Documentation
|
||||||
|
[](http://godoc.org/github.com/google/uuid)
|
||||||
|
|
||||||
|
Full `go doc` style documentation for the package can be viewed online without
|
||||||
|
installing this package by using the GoDoc site here:
|
||||||
|
http://godoc.org/github.com/google/uuid
|
80
vendor/github.com/google/uuid/dce.go
generated
vendored
Normal file
80
vendor/github.com/google/uuid/dce.go
generated
vendored
Normal file
@ -0,0 +1,80 @@
|
|||||||
|
// Copyright 2016 Google Inc. All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package uuid
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/binary"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
)
|
||||||
|
|
||||||
|
// A Domain represents a Version 2 domain
|
||||||
|
type Domain byte
|
||||||
|
|
||||||
|
// Domain constants for DCE Security (Version 2) UUIDs.
|
||||||
|
const (
|
||||||
|
Person = Domain(0)
|
||||||
|
Group = Domain(1)
|
||||||
|
Org = Domain(2)
|
||||||
|
)
|
||||||
|
|
||||||
|
// NewDCESecurity returns a DCE Security (Version 2) UUID.
|
||||||
|
//
|
||||||
|
// The domain should be one of Person, Group or Org.
|
||||||
|
// On a POSIX system the id should be the users UID for the Person
|
||||||
|
// domain and the users GID for the Group. The meaning of id for
|
||||||
|
// the domain Org or on non-POSIX systems is site defined.
|
||||||
|
//
|
||||||
|
// For a given domain/id pair the same token may be returned for up to
|
||||||
|
// 7 minutes and 10 seconds.
|
||||||
|
func NewDCESecurity(domain Domain, id uint32) (UUID, error) {
|
||||||
|
uuid, err := NewUUID()
|
||||||
|
if err == nil {
|
||||||
|
uuid[6] = (uuid[6] & 0x0f) | 0x20 // Version 2
|
||||||
|
uuid[9] = byte(domain)
|
||||||
|
binary.BigEndian.PutUint32(uuid[0:], id)
|
||||||
|
}
|
||||||
|
return uuid, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewDCEPerson returns a DCE Security (Version 2) UUID in the person
|
||||||
|
// domain with the id returned by os.Getuid.
|
||||||
|
//
|
||||||
|
// NewDCESecurity(Person, uint32(os.Getuid()))
|
||||||
|
func NewDCEPerson() (UUID, error) {
|
||||||
|
return NewDCESecurity(Person, uint32(os.Getuid()))
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewDCEGroup returns a DCE Security (Version 2) UUID in the group
|
||||||
|
// domain with the id returned by os.Getgid.
|
||||||
|
//
|
||||||
|
// NewDCESecurity(Group, uint32(os.Getgid()))
|
||||||
|
func NewDCEGroup() (UUID, error) {
|
||||||
|
return NewDCESecurity(Group, uint32(os.Getgid()))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Domain returns the domain for a Version 2 UUID. Domains are only defined
|
||||||
|
// for Version 2 UUIDs.
|
||||||
|
func (uuid UUID) Domain() Domain {
|
||||||
|
return Domain(uuid[9])
|
||||||
|
}
|
||||||
|
|
||||||
|
// ID returns the id for a Version 2 UUID. IDs are only defined for Version 2
|
||||||
|
// UUIDs.
|
||||||
|
func (uuid UUID) ID() uint32 {
|
||||||
|
return binary.BigEndian.Uint32(uuid[0:4])
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d Domain) String() string {
|
||||||
|
switch d {
|
||||||
|
case Person:
|
||||||
|
return "Person"
|
||||||
|
case Group:
|
||||||
|
return "Group"
|
||||||
|
case Org:
|
||||||
|
return "Org"
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("Domain%d", int(d))
|
||||||
|
}
|
12
vendor/github.com/google/uuid/doc.go
generated
vendored
Normal file
12
vendor/github.com/google/uuid/doc.go
generated
vendored
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
// Copyright 2016 Google Inc. All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
// Package uuid generates and inspects UUIDs.
|
||||||
|
//
|
||||||
|
// UUIDs are based on RFC 4122 and DCE 1.1: Authentication and Security
|
||||||
|
// Services.
|
||||||
|
//
|
||||||
|
// A UUID is a 16 byte (128 bit) array. UUIDs may be used as keys to
|
||||||
|
// maps or compared directly.
|
||||||
|
package uuid
|
1
vendor/github.com/google/uuid/go.mod
generated
vendored
Normal file
1
vendor/github.com/google/uuid/go.mod
generated
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
module github.com/google/uuid
|
53
vendor/github.com/google/uuid/hash.go
generated
vendored
Normal file
53
vendor/github.com/google/uuid/hash.go
generated
vendored
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
// Copyright 2016 Google Inc. All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package uuid
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/md5"
|
||||||
|
"crypto/sha1"
|
||||||
|
"hash"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Well known namespace IDs and UUIDs
|
||||||
|
var (
|
||||||
|
NameSpaceDNS = Must(Parse("6ba7b810-9dad-11d1-80b4-00c04fd430c8"))
|
||||||
|
NameSpaceURL = Must(Parse("6ba7b811-9dad-11d1-80b4-00c04fd430c8"))
|
||||||
|
NameSpaceOID = Must(Parse("6ba7b812-9dad-11d1-80b4-00c04fd430c8"))
|
||||||
|
NameSpaceX500 = Must(Parse("6ba7b814-9dad-11d1-80b4-00c04fd430c8"))
|
||||||
|
Nil UUID // empty UUID, all zeros
|
||||||
|
)
|
||||||
|
|
||||||
|
// NewHash returns a new UUID derived from the hash of space concatenated with
|
||||||
|
// data generated by h. The hash should be at least 16 byte in length. The
|
||||||
|
// first 16 bytes of the hash are used to form the UUID. The version of the
|
||||||
|
// UUID will be the lower 4 bits of version. NewHash is used to implement
|
||||||
|
// NewMD5 and NewSHA1.
|
||||||
|
func NewHash(h hash.Hash, space UUID, data []byte, version int) UUID {
|
||||||
|
h.Reset()
|
||||||
|
h.Write(space[:])
|
||||||
|
h.Write(data)
|
||||||
|
s := h.Sum(nil)
|
||||||
|
var uuid UUID
|
||||||
|
copy(uuid[:], s)
|
||||||
|
uuid[6] = (uuid[6] & 0x0f) | uint8((version&0xf)<<4)
|
||||||
|
uuid[8] = (uuid[8] & 0x3f) | 0x80 // RFC 4122 variant
|
||||||
|
return uuid
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewMD5 returns a new MD5 (Version 3) UUID based on the
|
||||||
|
// supplied name space and data. It is the same as calling:
|
||||||
|
//
|
||||||
|
// NewHash(md5.New(), space, data, 3)
|
||||||
|
func NewMD5(space UUID, data []byte) UUID {
|
||||||
|
return NewHash(md5.New(), space, data, 3)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewSHA1 returns a new SHA1 (Version 5) UUID based on the
|
||||||
|
// supplied name space and data. It is the same as calling:
|
||||||
|
//
|
||||||
|
// NewHash(sha1.New(), space, data, 5)
|
||||||
|
func NewSHA1(space UUID, data []byte) UUID {
|
||||||
|
return NewHash(sha1.New(), space, data, 5)
|
||||||
|
}
|
37
vendor/github.com/google/uuid/marshal.go
generated
vendored
Normal file
37
vendor/github.com/google/uuid/marshal.go
generated
vendored
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
// Copyright 2016 Google Inc. All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package uuid
|
||||||
|
|
||||||
|
import "fmt"
|
||||||
|
|
||||||
|
// MarshalText implements encoding.TextMarshaler.
|
||||||
|
func (uuid UUID) MarshalText() ([]byte, error) {
|
||||||
|
var js [36]byte
|
||||||
|
encodeHex(js[:], uuid)
|
||||||
|
return js[:], nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnmarshalText implements encoding.TextUnmarshaler.
|
||||||
|
func (uuid *UUID) UnmarshalText(data []byte) error {
|
||||||
|
id, err := ParseBytes(data)
|
||||||
|
if err == nil {
|
||||||
|
*uuid = id
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// MarshalBinary implements encoding.BinaryMarshaler.
|
||||||
|
func (uuid UUID) MarshalBinary() ([]byte, error) {
|
||||||
|
return uuid[:], nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnmarshalBinary implements encoding.BinaryUnmarshaler.
|
||||||
|
func (uuid *UUID) UnmarshalBinary(data []byte) error {
|
||||||
|
if len(data) != 16 {
|
||||||
|
return fmt.Errorf("invalid UUID (got %d bytes)", len(data))
|
||||||
|
}
|
||||||
|
copy(uuid[:], data)
|
||||||
|
return nil
|
||||||
|
}
|
90
vendor/github.com/google/uuid/node.go
generated
vendored
Normal file
90
vendor/github.com/google/uuid/node.go
generated
vendored
Normal file
@ -0,0 +1,90 @@
|
|||||||
|
// Copyright 2016 Google Inc. All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package uuid
|
||||||
|
|
||||||
|
import (
|
||||||
|
"sync"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
nodeMu sync.Mutex
|
||||||
|
ifname string // name of interface being used
|
||||||
|
nodeID [6]byte // hardware for version 1 UUIDs
|
||||||
|
zeroID [6]byte // nodeID with only 0's
|
||||||
|
)
|
||||||
|
|
||||||
|
// NodeInterface returns the name of the interface from which the NodeID was
|
||||||
|
// derived. The interface "user" is returned if the NodeID was set by
|
||||||
|
// SetNodeID.
|
||||||
|
func NodeInterface() string {
|
||||||
|
defer nodeMu.Unlock()
|
||||||
|
nodeMu.Lock()
|
||||||
|
return ifname
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetNodeInterface selects the hardware address to be used for Version 1 UUIDs.
|
||||||
|
// If name is "" then the first usable interface found will be used or a random
|
||||||
|
// Node ID will be generated. If a named interface cannot be found then false
|
||||||
|
// is returned.
|
||||||
|
//
|
||||||
|
// SetNodeInterface never fails when name is "".
|
||||||
|
func SetNodeInterface(name string) bool {
|
||||||
|
defer nodeMu.Unlock()
|
||||||
|
nodeMu.Lock()
|
||||||
|
return setNodeInterface(name)
|
||||||
|
}
|
||||||
|
|
||||||
|
func setNodeInterface(name string) bool {
|
||||||
|
iname, addr := getHardwareInterface(name) // null implementation for js
|
||||||
|
if iname != "" && addr != nil {
|
||||||
|
ifname = iname
|
||||||
|
copy(nodeID[:], addr)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// We found no interfaces with a valid hardware address. If name
|
||||||
|
// does not specify a specific interface generate a random Node ID
|
||||||
|
// (section 4.1.6)
|
||||||
|
if name == "" {
|
||||||
|
ifname = "random"
|
||||||
|
randomBits(nodeID[:])
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// NodeID returns a slice of a copy of the current Node ID, setting the Node ID
|
||||||
|
// if not already set.
|
||||||
|
func NodeID() []byte {
|
||||||
|
defer nodeMu.Unlock()
|
||||||
|
nodeMu.Lock()
|
||||||
|
if nodeID == zeroID {
|
||||||
|
setNodeInterface("")
|
||||||
|
}
|
||||||
|
nid := nodeID
|
||||||
|
return nid[:]
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetNodeID sets the Node ID to be used for Version 1 UUIDs. The first 6 bytes
|
||||||
|
// of id are used. If id is less than 6 bytes then false is returned and the
|
||||||
|
// Node ID is not set.
|
||||||
|
func SetNodeID(id []byte) bool {
|
||||||
|
if len(id) < 6 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
defer nodeMu.Unlock()
|
||||||
|
nodeMu.Lock()
|
||||||
|
copy(nodeID[:], id)
|
||||||
|
ifname = "user"
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// NodeID returns the 6 byte node id encoded in uuid. It returns nil if uuid is
|
||||||
|
// not valid. The NodeID is only well defined for version 1 and 2 UUIDs.
|
||||||
|
func (uuid UUID) NodeID() []byte {
|
||||||
|
var node [6]byte
|
||||||
|
copy(node[:], uuid[10:])
|
||||||
|
return node[:]
|
||||||
|
}
|
12
vendor/github.com/google/uuid/node_js.go
generated
vendored
Normal file
12
vendor/github.com/google/uuid/node_js.go
generated
vendored
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
// Copyright 2017 Google Inc. All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
// +build js
|
||||||
|
|
||||||
|
package uuid
|
||||||
|
|
||||||
|
// getHardwareInterface returns nil values for the JS version of the code.
|
||||||
|
// This remvoves the "net" dependency, because it is not used in the browser.
|
||||||
|
// Using the "net" library inflates the size of the transpiled JS code by 673k bytes.
|
||||||
|
func getHardwareInterface(name string) (string, []byte) { return "", nil }
|
33
vendor/github.com/google/uuid/node_net.go
generated
vendored
Normal file
33
vendor/github.com/google/uuid/node_net.go
generated
vendored
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
// Copyright 2017 Google Inc. All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
// +build !js
|
||||||
|
|
||||||
|
package uuid
|
||||||
|
|
||||||
|
import "net"
|
||||||
|
|
||||||
|
var interfaces []net.Interface // cached list of interfaces
|
||||||
|
|
||||||
|
// getHardwareInterface returns the name and hardware address of interface name.
|
||||||
|
// If name is "" then the name and hardware address of one of the system's
|
||||||
|
// interfaces is returned. If no interfaces are found (name does not exist or
|
||||||
|
// there are no interfaces) then "", nil is returned.
|
||||||
|
//
|
||||||
|
// Only addresses of at least 6 bytes are returned.
|
||||||
|
func getHardwareInterface(name string) (string, []byte) {
|
||||||
|
if interfaces == nil {
|
||||||
|
var err error
|
||||||
|
interfaces, err = net.Interfaces()
|
||||||
|
if err != nil {
|
||||||
|
return "", nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, ifs := range interfaces {
|
||||||
|
if len(ifs.HardwareAddr) >= 6 && (name == "" || name == ifs.Name) {
|
||||||
|
return ifs.Name, ifs.HardwareAddr
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return "", nil
|
||||||
|
}
|
59
vendor/github.com/google/uuid/sql.go
generated
vendored
Normal file
59
vendor/github.com/google/uuid/sql.go
generated
vendored
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
// Copyright 2016 Google Inc. All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package uuid
|
||||||
|
|
||||||
|
import (
|
||||||
|
"database/sql/driver"
|
||||||
|
"fmt"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Scan implements sql.Scanner so UUIDs can be read from databases transparently
|
||||||
|
// Currently, database types that map to string and []byte are supported. Please
|
||||||
|
// consult database-specific driver documentation for matching types.
|
||||||
|
func (uuid *UUID) Scan(src interface{}) error {
|
||||||
|
switch src := src.(type) {
|
||||||
|
case nil:
|
||||||
|
return nil
|
||||||
|
|
||||||
|
case string:
|
||||||
|
// if an empty UUID comes from a table, we return a null UUID
|
||||||
|
if src == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// see Parse for required string format
|
||||||
|
u, err := Parse(src)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("Scan: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
*uuid = u
|
||||||
|
|
||||||
|
case []byte:
|
||||||
|
// if an empty UUID comes from a table, we return a null UUID
|
||||||
|
if len(src) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// assumes a simple slice of bytes if 16 bytes
|
||||||
|
// otherwise attempts to parse
|
||||||
|
if len(src) != 16 {
|
||||||
|
return uuid.Scan(string(src))
|
||||||
|
}
|
||||||
|
copy((*uuid)[:], src)
|
||||||
|
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("Scan: unable to scan type %T into UUID", src)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Value implements sql.Valuer so that UUIDs can be written to databases
|
||||||
|
// transparently. Currently, UUIDs map to strings. Please consult
|
||||||
|
// database-specific driver documentation for matching types.
|
||||||
|
func (uuid UUID) Value() (driver.Value, error) {
|
||||||
|
return uuid.String(), nil
|
||||||
|
}
|
123
vendor/github.com/google/uuid/time.go
generated
vendored
Normal file
123
vendor/github.com/google/uuid/time.go
generated
vendored
Normal file
@ -0,0 +1,123 @@
|
|||||||
|
// Copyright 2016 Google Inc. All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package uuid
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/binary"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// A Time represents a time as the number of 100's of nanoseconds since 15 Oct
|
||||||
|
// 1582.
|
||||||
|
type Time int64
|
||||||
|
|
||||||
|
const (
|
||||||
|
lillian = 2299160 // Julian day of 15 Oct 1582
|
||||||
|
unix = 2440587 // Julian day of 1 Jan 1970
|
||||||
|
epoch = unix - lillian // Days between epochs
|
||||||
|
g1582 = epoch * 86400 // seconds between epochs
|
||||||
|
g1582ns100 = g1582 * 10000000 // 100s of a nanoseconds between epochs
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
timeMu sync.Mutex
|
||||||
|
lasttime uint64 // last time we returned
|
||||||
|
clockSeq uint16 // clock sequence for this run
|
||||||
|
|
||||||
|
timeNow = time.Now // for testing
|
||||||
|
)
|
||||||
|
|
||||||
|
// UnixTime converts t the number of seconds and nanoseconds using the Unix
|
||||||
|
// epoch of 1 Jan 1970.
|
||||||
|
func (t Time) UnixTime() (sec, nsec int64) {
|
||||||
|
sec = int64(t - g1582ns100)
|
||||||
|
nsec = (sec % 10000000) * 100
|
||||||
|
sec /= 10000000
|
||||||
|
return sec, nsec
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetTime returns the current Time (100s of nanoseconds since 15 Oct 1582) and
|
||||||
|
// clock sequence as well as adjusting the clock sequence as needed. An error
|
||||||
|
// is returned if the current time cannot be determined.
|
||||||
|
func GetTime() (Time, uint16, error) {
|
||||||
|
defer timeMu.Unlock()
|
||||||
|
timeMu.Lock()
|
||||||
|
return getTime()
|
||||||
|
}
|
||||||
|
|
||||||
|
func getTime() (Time, uint16, error) {
|
||||||
|
t := timeNow()
|
||||||
|
|
||||||
|
// If we don't have a clock sequence already, set one.
|
||||||
|
if clockSeq == 0 {
|
||||||
|
setClockSequence(-1)
|
||||||
|
}
|
||||||
|
now := uint64(t.UnixNano()/100) + g1582ns100
|
||||||
|
|
||||||
|
// If time has gone backwards with this clock sequence then we
|
||||||
|
// increment the clock sequence
|
||||||
|
if now <= lasttime {
|
||||||
|
clockSeq = ((clockSeq + 1) & 0x3fff) | 0x8000
|
||||||
|
}
|
||||||
|
lasttime = now
|
||||||
|
return Time(now), clockSeq, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ClockSequence returns the current clock sequence, generating one if not
|
||||||
|
// already set. The clock sequence is only used for Version 1 UUIDs.
|
||||||
|
//
|
||||||
|
// The uuid package does not use global static storage for the clock sequence or
|
||||||
|
// the last time a UUID was generated. Unless SetClockSequence is used, a new
|
||||||
|
// random clock sequence is generated the first time a clock sequence is
|
||||||
|
// requested by ClockSequence, GetTime, or NewUUID. (section 4.2.1.1)
|
||||||
|
func ClockSequence() int {
|
||||||
|
defer timeMu.Unlock()
|
||||||
|
timeMu.Lock()
|
||||||
|
return clockSequence()
|
||||||
|
}
|
||||||
|
|
||||||
|
func clockSequence() int {
|
||||||
|
if clockSeq == 0 {
|
||||||
|
setClockSequence(-1)
|
||||||
|
}
|
||||||
|
return int(clockSeq & 0x3fff)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetClockSequence sets the clock sequence to the lower 14 bits of seq. Setting to
|
||||||
|
// -1 causes a new sequence to be generated.
|
||||||
|
func SetClockSequence(seq int) {
|
||||||
|
defer timeMu.Unlock()
|
||||||
|
timeMu.Lock()
|
||||||
|
setClockSequence(seq)
|
||||||
|
}
|
||||||
|
|
||||||
|
func setClockSequence(seq int) {
|
||||||
|
if seq == -1 {
|
||||||
|
var b [2]byte
|
||||||
|
randomBits(b[:]) // clock sequence
|
||||||
|
seq = int(b[0])<<8 | int(b[1])
|
||||||
|
}
|
||||||
|
oldSeq := clockSeq
|
||||||
|
clockSeq = uint16(seq&0x3fff) | 0x8000 // Set our variant
|
||||||
|
if oldSeq != clockSeq {
|
||||||
|
lasttime = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Time returns the time in 100s of nanoseconds since 15 Oct 1582 encoded in
|
||||||
|
// uuid. The time is only defined for version 1 and 2 UUIDs.
|
||||||
|
func (uuid UUID) Time() Time {
|
||||||
|
time := int64(binary.BigEndian.Uint32(uuid[0:4]))
|
||||||
|
time |= int64(binary.BigEndian.Uint16(uuid[4:6])) << 32
|
||||||
|
time |= int64(binary.BigEndian.Uint16(uuid[6:8])&0xfff) << 48
|
||||||
|
return Time(time)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ClockSequence returns the clock sequence encoded in uuid.
|
||||||
|
// The clock sequence is only well defined for version 1 and 2 UUIDs.
|
||||||
|
func (uuid UUID) ClockSequence() int {
|
||||||
|
return int(binary.BigEndian.Uint16(uuid[8:10])) & 0x3fff
|
||||||
|
}
|
43
vendor/github.com/google/uuid/util.go
generated
vendored
Normal file
43
vendor/github.com/google/uuid/util.go
generated
vendored
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
// Copyright 2016 Google Inc. All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package uuid
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io"
|
||||||
|
)
|
||||||
|
|
||||||
|
// randomBits completely fills slice b with random data.
|
||||||
|
func randomBits(b []byte) {
|
||||||
|
if _, err := io.ReadFull(rander, b); err != nil {
|
||||||
|
panic(err.Error()) // rand should never fail
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// xvalues returns the value of a byte as a hexadecimal digit or 255.
|
||||||
|
var xvalues = [256]byte{
|
||||||
|
255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
|
||||||
|
255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
|
||||||
|
255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
|
||||||
|
0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 255, 255, 255, 255, 255, 255,
|
||||||
|
255, 10, 11, 12, 13, 14, 15, 255, 255, 255, 255, 255, 255, 255, 255, 255,
|
||||||
|
255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
|
||||||
|
255, 10, 11, 12, 13, 14, 15, 255, 255, 255, 255, 255, 255, 255, 255, 255,
|
||||||
|
255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
|
||||||
|
255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
|
||||||
|
255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
|
||||||
|
255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
|
||||||
|
255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
|
||||||
|
255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
|
||||||
|
255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
|
||||||
|
255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
|
||||||
|
255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
|
||||||
|
}
|
||||||
|
|
||||||
|
// xtob converts hex characters x1 and x2 into a byte.
|
||||||
|
func xtob(x1, x2 byte) (byte, bool) {
|
||||||
|
b1 := xvalues[x1]
|
||||||
|
b2 := xvalues[x2]
|
||||||
|
return (b1 << 4) | b2, b1 != 255 && b2 != 255
|
||||||
|
}
|
245
vendor/github.com/google/uuid/uuid.go
generated
vendored
Normal file
245
vendor/github.com/google/uuid/uuid.go
generated
vendored
Normal file
@ -0,0 +1,245 @@
|
|||||||
|
// Copyright 2018 Google Inc. All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package uuid
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"crypto/rand"
|
||||||
|
"encoding/hex"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// A UUID is a 128 bit (16 byte) Universal Unique IDentifier as defined in RFC
|
||||||
|
// 4122.
|
||||||
|
type UUID [16]byte
|
||||||
|
|
||||||
|
// A Version represents a UUID's version.
|
||||||
|
type Version byte
|
||||||
|
|
||||||
|
// A Variant represents a UUID's variant.
|
||||||
|
type Variant byte
|
||||||
|
|
||||||
|
// Constants returned by Variant.
|
||||||
|
const (
|
||||||
|
Invalid = Variant(iota) // Invalid UUID
|
||||||
|
RFC4122 // The variant specified in RFC4122
|
||||||
|
Reserved // Reserved, NCS backward compatibility.
|
||||||
|
Microsoft // Reserved, Microsoft Corporation backward compatibility.
|
||||||
|
Future // Reserved for future definition.
|
||||||
|
)
|
||||||
|
|
||||||
|
var rander = rand.Reader // random function
|
||||||
|
|
||||||
|
// Parse decodes s into a UUID or returns an error. Both the standard UUID
|
||||||
|
// forms of xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx and
|
||||||
|
// urn:uuid:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx are decoded as well as the
|
||||||
|
// Microsoft encoding {xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx} and the raw hex
|
||||||
|
// encoding: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx.
|
||||||
|
func Parse(s string) (UUID, error) {
|
||||||
|
var uuid UUID
|
||||||
|
switch len(s) {
|
||||||
|
// xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
|
||||||
|
case 36:
|
||||||
|
|
||||||
|
// urn:uuid:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
|
||||||
|
case 36 + 9:
|
||||||
|
if strings.ToLower(s[:9]) != "urn:uuid:" {
|
||||||
|
return uuid, fmt.Errorf("invalid urn prefix: %q", s[:9])
|
||||||
|
}
|
||||||
|
s = s[9:]
|
||||||
|
|
||||||
|
// {xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx}
|
||||||
|
case 36 + 2:
|
||||||
|
s = s[1:]
|
||||||
|
|
||||||
|
// xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
|
||||||
|
case 32:
|
||||||
|
var ok bool
|
||||||
|
for i := range uuid {
|
||||||
|
uuid[i], ok = xtob(s[i*2], s[i*2+1])
|
||||||
|
if !ok {
|
||||||
|
return uuid, errors.New("invalid UUID format")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return uuid, nil
|
||||||
|
default:
|
||||||
|
return uuid, fmt.Errorf("invalid UUID length: %d", len(s))
|
||||||
|
}
|
||||||
|
// s is now at least 36 bytes long
|
||||||
|
// it must be of the form xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
|
||||||
|
if s[8] != '-' || s[13] != '-' || s[18] != '-' || s[23] != '-' {
|
||||||
|
return uuid, errors.New("invalid UUID format")
|
||||||
|
}
|
||||||
|
for i, x := range [16]int{
|
||||||
|
0, 2, 4, 6,
|
||||||
|
9, 11,
|
||||||
|
14, 16,
|
||||||
|
19, 21,
|
||||||
|
24, 26, 28, 30, 32, 34} {
|
||||||
|
v, ok := xtob(s[x], s[x+1])
|
||||||
|
if !ok {
|
||||||
|
return uuid, errors.New("invalid UUID format")
|
||||||
|
}
|
||||||
|
uuid[i] = v
|
||||||
|
}
|
||||||
|
return uuid, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ParseBytes is like Parse, except it parses a byte slice instead of a string.
|
||||||
|
func ParseBytes(b []byte) (UUID, error) {
|
||||||
|
var uuid UUID
|
||||||
|
switch len(b) {
|
||||||
|
case 36: // xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
|
||||||
|
case 36 + 9: // urn:uuid:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
|
||||||
|
if !bytes.Equal(bytes.ToLower(b[:9]), []byte("urn:uuid:")) {
|
||||||
|
return uuid, fmt.Errorf("invalid urn prefix: %q", b[:9])
|
||||||
|
}
|
||||||
|
b = b[9:]
|
||||||
|
case 36 + 2: // {xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx}
|
||||||
|
b = b[1:]
|
||||||
|
case 32: // xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
|
||||||
|
var ok bool
|
||||||
|
for i := 0; i < 32; i += 2 {
|
||||||
|
uuid[i/2], ok = xtob(b[i], b[i+1])
|
||||||
|
if !ok {
|
||||||
|
return uuid, errors.New("invalid UUID format")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return uuid, nil
|
||||||
|
default:
|
||||||
|
return uuid, fmt.Errorf("invalid UUID length: %d", len(b))
|
||||||
|
}
|
||||||
|
// s is now at least 36 bytes long
|
||||||
|
// it must be of the form xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
|
||||||
|
if b[8] != '-' || b[13] != '-' || b[18] != '-' || b[23] != '-' {
|
||||||
|
return uuid, errors.New("invalid UUID format")
|
||||||
|
}
|
||||||
|
for i, x := range [16]int{
|
||||||
|
0, 2, 4, 6,
|
||||||
|
9, 11,
|
||||||
|
14, 16,
|
||||||
|
19, 21,
|
||||||
|
24, 26, 28, 30, 32, 34} {
|
||||||
|
v, ok := xtob(b[x], b[x+1])
|
||||||
|
if !ok {
|
||||||
|
return uuid, errors.New("invalid UUID format")
|
||||||
|
}
|
||||||
|
uuid[i] = v
|
||||||
|
}
|
||||||
|
return uuid, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// MustParse is like Parse but panics if the string cannot be parsed.
|
||||||
|
// It simplifies safe initialization of global variables holding compiled UUIDs.
|
||||||
|
func MustParse(s string) UUID {
|
||||||
|
uuid, err := Parse(s)
|
||||||
|
if err != nil {
|
||||||
|
panic(`uuid: Parse(` + s + `): ` + err.Error())
|
||||||
|
}
|
||||||
|
return uuid
|
||||||
|
}
|
||||||
|
|
||||||
|
// FromBytes creates a new UUID from a byte slice. Returns an error if the slice
|
||||||
|
// does not have a length of 16. The bytes are copied from the slice.
|
||||||
|
func FromBytes(b []byte) (uuid UUID, err error) {
|
||||||
|
err = uuid.UnmarshalBinary(b)
|
||||||
|
return uuid, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Must returns uuid if err is nil and panics otherwise.
|
||||||
|
func Must(uuid UUID, err error) UUID {
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
return uuid
|
||||||
|
}
|
||||||
|
|
||||||
|
// String returns the string form of uuid, xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
|
||||||
|
// , or "" if uuid is invalid.
|
||||||
|
func (uuid UUID) String() string {
|
||||||
|
var buf [36]byte
|
||||||
|
encodeHex(buf[:], uuid)
|
||||||
|
return string(buf[:])
|
||||||
|
}
|
||||||
|
|
||||||
|
// URN returns the RFC 2141 URN form of uuid,
|
||||||
|
// urn:uuid:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx, or "" if uuid is invalid.
|
||||||
|
func (uuid UUID) URN() string {
|
||||||
|
var buf [36 + 9]byte
|
||||||
|
copy(buf[:], "urn:uuid:")
|
||||||
|
encodeHex(buf[9:], uuid)
|
||||||
|
return string(buf[:])
|
||||||
|
}
|
||||||
|
|
||||||
|
func encodeHex(dst []byte, uuid UUID) {
|
||||||
|
hex.Encode(dst, uuid[:4])
|
||||||
|
dst[8] = '-'
|
||||||
|
hex.Encode(dst[9:13], uuid[4:6])
|
||||||
|
dst[13] = '-'
|
||||||
|
hex.Encode(dst[14:18], uuid[6:8])
|
||||||
|
dst[18] = '-'
|
||||||
|
hex.Encode(dst[19:23], uuid[8:10])
|
||||||
|
dst[23] = '-'
|
||||||
|
hex.Encode(dst[24:], uuid[10:])
|
||||||
|
}
|
||||||
|
|
||||||
|
// Variant returns the variant encoded in uuid.
|
||||||
|
func (uuid UUID) Variant() Variant {
|
||||||
|
switch {
|
||||||
|
case (uuid[8] & 0xc0) == 0x80:
|
||||||
|
return RFC4122
|
||||||
|
case (uuid[8] & 0xe0) == 0xc0:
|
||||||
|
return Microsoft
|
||||||
|
case (uuid[8] & 0xe0) == 0xe0:
|
||||||
|
return Future
|
||||||
|
default:
|
||||||
|
return Reserved
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Version returns the version of uuid.
|
||||||
|
func (uuid UUID) Version() Version {
|
||||||
|
return Version(uuid[6] >> 4)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v Version) String() string {
|
||||||
|
if v > 15 {
|
||||||
|
return fmt.Sprintf("BAD_VERSION_%d", v)
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("VERSION_%d", v)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v Variant) String() string {
|
||||||
|
switch v {
|
||||||
|
case RFC4122:
|
||||||
|
return "RFC4122"
|
||||||
|
case Reserved:
|
||||||
|
return "Reserved"
|
||||||
|
case Microsoft:
|
||||||
|
return "Microsoft"
|
||||||
|
case Future:
|
||||||
|
return "Future"
|
||||||
|
case Invalid:
|
||||||
|
return "Invalid"
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("BadVariant%d", int(v))
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetRand sets the random number generator to r, which implements io.Reader.
|
||||||
|
// If r.Read returns an error when the package requests random data then
|
||||||
|
// a panic will be issued.
|
||||||
|
//
|
||||||
|
// Calling SetRand with nil sets the random number generator to the default
|
||||||
|
// generator.
|
||||||
|
func SetRand(r io.Reader) {
|
||||||
|
if r == nil {
|
||||||
|
rander = rand.Reader
|
||||||
|
return
|
||||||
|
}
|
||||||
|
rander = r
|
||||||
|
}
|
44
vendor/github.com/google/uuid/version1.go
generated
vendored
Normal file
44
vendor/github.com/google/uuid/version1.go
generated
vendored
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
// Copyright 2016 Google Inc. All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package uuid
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/binary"
|
||||||
|
)
|
||||||
|
|
||||||
|
// NewUUID returns a Version 1 UUID based on the current NodeID and clock
|
||||||
|
// sequence, and the current time. If the NodeID has not been set by SetNodeID
|
||||||
|
// or SetNodeInterface then it will be set automatically. If the NodeID cannot
|
||||||
|
// be set NewUUID returns nil. If clock sequence has not been set by
|
||||||
|
// SetClockSequence then it will be set automatically. If GetTime fails to
|
||||||
|
// return the current NewUUID returns nil and an error.
|
||||||
|
//
|
||||||
|
// In most cases, New should be used.
|
||||||
|
func NewUUID() (UUID, error) {
|
||||||
|
nodeMu.Lock()
|
||||||
|
if nodeID == zeroID {
|
||||||
|
setNodeInterface("")
|
||||||
|
}
|
||||||
|
nodeMu.Unlock()
|
||||||
|
|
||||||
|
var uuid UUID
|
||||||
|
now, seq, err := GetTime()
|
||||||
|
if err != nil {
|
||||||
|
return uuid, err
|
||||||
|
}
|
||||||
|
|
||||||
|
timeLow := uint32(now & 0xffffffff)
|
||||||
|
timeMid := uint16((now >> 32) & 0xffff)
|
||||||
|
timeHi := uint16((now >> 48) & 0x0fff)
|
||||||
|
timeHi |= 0x1000 // Version 1
|
||||||
|
|
||||||
|
binary.BigEndian.PutUint32(uuid[0:], timeLow)
|
||||||
|
binary.BigEndian.PutUint16(uuid[4:], timeMid)
|
||||||
|
binary.BigEndian.PutUint16(uuid[6:], timeHi)
|
||||||
|
binary.BigEndian.PutUint16(uuid[8:], seq)
|
||||||
|
copy(uuid[10:], nodeID[:])
|
||||||
|
|
||||||
|
return uuid, nil
|
||||||
|
}
|
38
vendor/github.com/google/uuid/version4.go
generated
vendored
Normal file
38
vendor/github.com/google/uuid/version4.go
generated
vendored
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
// Copyright 2016 Google Inc. All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package uuid
|
||||||
|
|
||||||
|
import "io"
|
||||||
|
|
||||||
|
// New creates a new random UUID or panics. New is equivalent to
|
||||||
|
// the expression
|
||||||
|
//
|
||||||
|
// uuid.Must(uuid.NewRandom())
|
||||||
|
func New() UUID {
|
||||||
|
return Must(NewRandom())
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewRandom returns a Random (Version 4) UUID.
|
||||||
|
//
|
||||||
|
// The strength of the UUIDs is based on the strength of the crypto/rand
|
||||||
|
// package.
|
||||||
|
//
|
||||||
|
// A note about uniqueness derived from the UUID Wikipedia entry:
|
||||||
|
//
|
||||||
|
// Randomly generated UUIDs have 122 random bits. One's annual risk of being
|
||||||
|
// hit by a meteorite is estimated to be one chance in 17 billion, that
|
||||||
|
// means the probability is about 0.00000000006 (6 × 10−11),
|
||||||
|
// equivalent to the odds of creating a few tens of trillions of UUIDs in a
|
||||||
|
// year and having one duplicate.
|
||||||
|
func NewRandom() (UUID, error) {
|
||||||
|
var uuid UUID
|
||||||
|
_, err := io.ReadFull(rander, uuid[:])
|
||||||
|
if err != nil {
|
||||||
|
return Nil, err
|
||||||
|
}
|
||||||
|
uuid[6] = (uuid[6] & 0x0f) | 0x40 // Version 4
|
||||||
|
uuid[8] = (uuid[8] & 0x3f) | 0x80 // Variant is 10
|
||||||
|
return uuid, nil
|
||||||
|
}
|
1
vendor/github.com/joho/godotenv/.gitignore
generated
vendored
Normal file
1
vendor/github.com/joho/godotenv/.gitignore
generated
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
.DS_Store
|
8
vendor/github.com/joho/godotenv/.travis.yml
generated
vendored
Normal file
8
vendor/github.com/joho/godotenv/.travis.yml
generated
vendored
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
language: go
|
||||||
|
|
||||||
|
go:
|
||||||
|
- 1.x
|
||||||
|
|
||||||
|
os:
|
||||||
|
- linux
|
||||||
|
- osx
|
23
vendor/github.com/joho/godotenv/LICENCE
generated
vendored
Normal file
23
vendor/github.com/joho/godotenv/LICENCE
generated
vendored
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
Copyright (c) 2013 John Barton
|
||||||
|
|
||||||
|
MIT License
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining
|
||||||
|
a copy of this software and associated documentation files (the
|
||||||
|
"Software"), to deal in the Software without restriction, including
|
||||||
|
without limitation the rights to use, copy, modify, merge, publish,
|
||||||
|
distribute, sublicense, and/or sell copies of the Software, and to
|
||||||
|
permit persons to whom the Software is furnished to do so, subject to
|
||||||
|
the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be
|
||||||
|
included in all copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||||
|
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||||
|
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||||
|
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
||||||
|
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
||||||
|
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||||
|
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
|
163
vendor/github.com/joho/godotenv/README.md
generated
vendored
Normal file
163
vendor/github.com/joho/godotenv/README.md
generated
vendored
Normal file
@ -0,0 +1,163 @@
|
|||||||
|
# GoDotEnv [](https://travis-ci.org/joho/godotenv) [](https://ci.appveyor.com/project/joho/godotenv) [](https://goreportcard.com/report/github.com/joho/godotenv)
|
||||||
|
|
||||||
|
A Go (golang) port of the Ruby dotenv project (which loads env vars from a .env file)
|
||||||
|
|
||||||
|
From the original Library:
|
||||||
|
|
||||||
|
> Storing configuration in the environment is one of the tenets of a twelve-factor app. Anything that is likely to change between deployment environments–such as resource handles for databases or credentials for external services–should be extracted from the code into environment variables.
|
||||||
|
>
|
||||||
|
> But it is not always practical to set environment variables on development machines or continuous integration servers where multiple projects are run. Dotenv load variables from a .env file into ENV when the environment is bootstrapped.
|
||||||
|
|
||||||
|
It can be used as a library (for loading in env for your own daemons etc) or as a bin command.
|
||||||
|
|
||||||
|
There is test coverage and CI for both linuxish and windows environments, but I make no guarantees about the bin version working on windows.
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
As a library
|
||||||
|
|
||||||
|
```shell
|
||||||
|
go get github.com/joho/godotenv
|
||||||
|
```
|
||||||
|
|
||||||
|
or if you want to use it as a bin command
|
||||||
|
```shell
|
||||||
|
go get github.com/joho/godotenv/cmd/godotenv
|
||||||
|
```
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
Add your application configuration to your `.env` file in the root of your project:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
S3_BUCKET=YOURS3BUCKET
|
||||||
|
SECRET_KEY=YOURSECRETKEYGOESHERE
|
||||||
|
```
|
||||||
|
|
||||||
|
Then in your Go app you can do something like
|
||||||
|
|
||||||
|
```go
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/joho/godotenv"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
err := godotenv.Load()
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal("Error loading .env file")
|
||||||
|
}
|
||||||
|
|
||||||
|
s3Bucket := os.Getenv("S3_BUCKET")
|
||||||
|
secretKey := os.Getenv("SECRET_KEY")
|
||||||
|
|
||||||
|
// now do something with s3 or whatever
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
If you're even lazier than that, you can just take advantage of the autoload package which will read in `.env` on import
|
||||||
|
|
||||||
|
```go
|
||||||
|
import _ "github.com/joho/godotenv/autoload"
|
||||||
|
```
|
||||||
|
|
||||||
|
While `.env` in the project root is the default, you don't have to be constrained, both examples below are 100% legit
|
||||||
|
|
||||||
|
```go
|
||||||
|
_ = godotenv.Load("somerandomfile")
|
||||||
|
_ = godotenv.Load("filenumberone.env", "filenumbertwo.env")
|
||||||
|
```
|
||||||
|
|
||||||
|
If you want to be really fancy with your env file you can do comments and exports (below is a valid env file)
|
||||||
|
|
||||||
|
```shell
|
||||||
|
# I am a comment and that is OK
|
||||||
|
SOME_VAR=someval
|
||||||
|
FOO=BAR # comments at line end are OK too
|
||||||
|
export BAR=BAZ
|
||||||
|
```
|
||||||
|
|
||||||
|
Or finally you can do YAML(ish) style
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
FOO: bar
|
||||||
|
BAR: baz
|
||||||
|
```
|
||||||
|
|
||||||
|
as a final aside, if you don't want godotenv munging your env you can just get a map back instead
|
||||||
|
|
||||||
|
```go
|
||||||
|
var myEnv map[string]string
|
||||||
|
myEnv, err := godotenv.Read()
|
||||||
|
|
||||||
|
s3Bucket := myEnv["S3_BUCKET"]
|
||||||
|
```
|
||||||
|
|
||||||
|
... or from an `io.Reader` instead of a local file
|
||||||
|
|
||||||
|
```go
|
||||||
|
reader := getRemoteFile()
|
||||||
|
myEnv, err := godotenv.Parse(reader)
|
||||||
|
```
|
||||||
|
|
||||||
|
... or from a `string` if you so desire
|
||||||
|
|
||||||
|
```go
|
||||||
|
content := getRemoteFileContent()
|
||||||
|
myEnv, err := godotenv.Unmarshal(content)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Command Mode
|
||||||
|
|
||||||
|
Assuming you've installed the command as above and you've got `$GOPATH/bin` in your `$PATH`
|
||||||
|
|
||||||
|
```
|
||||||
|
godotenv -f /some/path/to/.env some_command with some args
|
||||||
|
```
|
||||||
|
|
||||||
|
If you don't specify `-f` it will fall back on the default of loading `.env` in `PWD`
|
||||||
|
|
||||||
|
### Writing Env Files
|
||||||
|
|
||||||
|
Godotenv can also write a map representing the environment to a correctly-formatted and escaped file
|
||||||
|
|
||||||
|
```go
|
||||||
|
env, err := godotenv.Unmarshal("KEY=value")
|
||||||
|
err := godotenv.Write(env, "./.env")
|
||||||
|
```
|
||||||
|
|
||||||
|
... or to a string
|
||||||
|
|
||||||
|
```go
|
||||||
|
env, err := godotenv.Unmarshal("KEY=value")
|
||||||
|
content, err := godotenv.Marshal(env)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Contributing
|
||||||
|
|
||||||
|
Contributions are most welcome! The parser itself is pretty stupidly naive and I wouldn't be surprised if it breaks with edge cases.
|
||||||
|
|
||||||
|
*code changes without tests will not be accepted*
|
||||||
|
|
||||||
|
1. Fork it
|
||||||
|
2. Create your feature branch (`git checkout -b my-new-feature`)
|
||||||
|
3. Commit your changes (`git commit -am 'Added some feature'`)
|
||||||
|
4. Push to the branch (`git push origin my-new-feature`)
|
||||||
|
5. Create new Pull Request
|
||||||
|
|
||||||
|
## Releases
|
||||||
|
|
||||||
|
Releases should follow [Semver](http://semver.org/) though the first couple of releases are `v1` and `v1.1`.
|
||||||
|
|
||||||
|
Use [annotated tags for all releases](https://github.com/joho/godotenv/issues/30). Example `git tag -a v1.2.1`
|
||||||
|
|
||||||
|
## CI
|
||||||
|
|
||||||
|
Linux: [](https://travis-ci.org/joho/godotenv) Windows: [](https://ci.appveyor.com/project/joho/godotenv)
|
||||||
|
|
||||||
|
## Who?
|
||||||
|
|
||||||
|
The original library [dotenv](https://github.com/bkeepers/dotenv) was written by [Brandon Keepers](http://opensoul.org/), and this port was done by [John Barton](https://johnbarton.co/) based off the tests/fixtures in the original library.
|
15
vendor/github.com/joho/godotenv/autoload/autoload.go
generated
vendored
Normal file
15
vendor/github.com/joho/godotenv/autoload/autoload.go
generated
vendored
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
package autoload
|
||||||
|
|
||||||
|
/*
|
||||||
|
You can just read the .env file on import just by doing
|
||||||
|
|
||||||
|
import _ "github.com/joho/godotenv/autoload"
|
||||||
|
|
||||||
|
And bob's your mother's brother
|
||||||
|
*/
|
||||||
|
|
||||||
|
import "github.com/joho/godotenv"
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
godotenv.Load()
|
||||||
|
}
|
346
vendor/github.com/joho/godotenv/godotenv.go
generated
vendored
Normal file
346
vendor/github.com/joho/godotenv/godotenv.go
generated
vendored
Normal file
@ -0,0 +1,346 @@
|
|||||||
|
// Package godotenv is a go port of the ruby dotenv library (https://github.com/bkeepers/dotenv)
|
||||||
|
//
|
||||||
|
// Examples/readme can be found on the github page at https://github.com/joho/godotenv
|
||||||
|
//
|
||||||
|
// The TL;DR is that you make a .env file that looks something like
|
||||||
|
//
|
||||||
|
// SOME_ENV_VAR=somevalue
|
||||||
|
//
|
||||||
|
// and then in your go code you can call
|
||||||
|
//
|
||||||
|
// godotenv.Load()
|
||||||
|
//
|
||||||
|
// and all the env vars declared in .env will be available through os.Getenv("SOME_ENV_VAR")
|
||||||
|
package godotenv
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"regexp"
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
const doubleQuoteSpecialChars = "\\\n\r\"!$`"
|
||||||
|
|
||||||
|
// Load will read your env file(s) and load them into ENV for this process.
|
||||||
|
//
|
||||||
|
// Call this function as close as possible to the start of your program (ideally in main)
|
||||||
|
//
|
||||||
|
// If you call Load without any args it will default to loading .env in the current path
|
||||||
|
//
|
||||||
|
// You can otherwise tell it which files to load (there can be more than one) like
|
||||||
|
//
|
||||||
|
// godotenv.Load("fileone", "filetwo")
|
||||||
|
//
|
||||||
|
// It's important to note that it WILL NOT OVERRIDE an env variable that already exists - consider the .env file to set dev vars or sensible defaults
|
||||||
|
func Load(filenames ...string) (err error) {
|
||||||
|
filenames = filenamesOrDefault(filenames)
|
||||||
|
|
||||||
|
for _, filename := range filenames {
|
||||||
|
err = loadFile(filename, false)
|
||||||
|
if err != nil {
|
||||||
|
return // return early on a spazout
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Overload will read your env file(s) and load them into ENV for this process.
|
||||||
|
//
|
||||||
|
// Call this function as close as possible to the start of your program (ideally in main)
|
||||||
|
//
|
||||||
|
// If you call Overload without any args it will default to loading .env in the current path
|
||||||
|
//
|
||||||
|
// You can otherwise tell it which files to load (there can be more than one) like
|
||||||
|
//
|
||||||
|
// godotenv.Overload("fileone", "filetwo")
|
||||||
|
//
|
||||||
|
// It's important to note this WILL OVERRIDE an env variable that already exists - consider the .env file to forcefilly set all vars.
|
||||||
|
func Overload(filenames ...string) (err error) {
|
||||||
|
filenames = filenamesOrDefault(filenames)
|
||||||
|
|
||||||
|
for _, filename := range filenames {
|
||||||
|
err = loadFile(filename, true)
|
||||||
|
if err != nil {
|
||||||
|
return // return early on a spazout
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read all env (with same file loading semantics as Load) but return values as
|
||||||
|
// a map rather than automatically writing values into env
|
||||||
|
func Read(filenames ...string) (envMap map[string]string, err error) {
|
||||||
|
filenames = filenamesOrDefault(filenames)
|
||||||
|
envMap = make(map[string]string)
|
||||||
|
|
||||||
|
for _, filename := range filenames {
|
||||||
|
individualEnvMap, individualErr := readFile(filename)
|
||||||
|
|
||||||
|
if individualErr != nil {
|
||||||
|
err = individualErr
|
||||||
|
return // return early on a spazout
|
||||||
|
}
|
||||||
|
|
||||||
|
for key, value := range individualEnvMap {
|
||||||
|
envMap[key] = value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse reads an env file from io.Reader, returning a map of keys and values.
|
||||||
|
func Parse(r io.Reader) (envMap map[string]string, err error) {
|
||||||
|
envMap = make(map[string]string)
|
||||||
|
|
||||||
|
var lines []string
|
||||||
|
scanner := bufio.NewScanner(r)
|
||||||
|
for scanner.Scan() {
|
||||||
|
lines = append(lines, scanner.Text())
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = scanner.Err(); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, fullLine := range lines {
|
||||||
|
if !isIgnoredLine(fullLine) {
|
||||||
|
var key, value string
|
||||||
|
key, value, err = parseLine(fullLine, envMap)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
envMap[key] = value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
//Unmarshal reads an env file from a string, returning a map of keys and values.
|
||||||
|
func Unmarshal(str string) (envMap map[string]string, err error) {
|
||||||
|
return Parse(strings.NewReader(str))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Exec loads env vars from the specified filenames (empty map falls back to default)
|
||||||
|
// then executes the cmd specified.
|
||||||
|
//
|
||||||
|
// Simply hooks up os.Stdin/err/out to the command and calls Run()
|
||||||
|
//
|
||||||
|
// If you want more fine grained control over your command it's recommended
|
||||||
|
// that you use `Load()` or `Read()` and the `os/exec` package yourself.
|
||||||
|
func Exec(filenames []string, cmd string, cmdArgs []string) error {
|
||||||
|
Load(filenames...)
|
||||||
|
|
||||||
|
command := exec.Command(cmd, cmdArgs...)
|
||||||
|
command.Stdin = os.Stdin
|
||||||
|
command.Stdout = os.Stdout
|
||||||
|
command.Stderr = os.Stderr
|
||||||
|
return command.Run()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write serializes the given environment and writes it to a file
|
||||||
|
func Write(envMap map[string]string, filename string) error {
|
||||||
|
content, error := Marshal(envMap)
|
||||||
|
if error != nil {
|
||||||
|
return error
|
||||||
|
}
|
||||||
|
file, error := os.Create(filename)
|
||||||
|
if error != nil {
|
||||||
|
return error
|
||||||
|
}
|
||||||
|
_, err := file.WriteString(content)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Marshal outputs the given environment as a dotenv-formatted environment file.
|
||||||
|
// Each line is in the format: KEY="VALUE" where VALUE is backslash-escaped.
|
||||||
|
func Marshal(envMap map[string]string) (string, error) {
|
||||||
|
lines := make([]string, 0, len(envMap))
|
||||||
|
for k, v := range envMap {
|
||||||
|
lines = append(lines, fmt.Sprintf(`%s="%s"`, k, doubleQuoteEscape(v)))
|
||||||
|
}
|
||||||
|
sort.Strings(lines)
|
||||||
|
return strings.Join(lines, "\n"), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func filenamesOrDefault(filenames []string) []string {
|
||||||
|
if len(filenames) == 0 {
|
||||||
|
return []string{".env"}
|
||||||
|
}
|
||||||
|
return filenames
|
||||||
|
}
|
||||||
|
|
||||||
|
func loadFile(filename string, overload bool) error {
|
||||||
|
envMap, err := readFile(filename)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
currentEnv := map[string]bool{}
|
||||||
|
rawEnv := os.Environ()
|
||||||
|
for _, rawEnvLine := range rawEnv {
|
||||||
|
key := strings.Split(rawEnvLine, "=")[0]
|
||||||
|
currentEnv[key] = true
|
||||||
|
}
|
||||||
|
|
||||||
|
for key, value := range envMap {
|
||||||
|
if !currentEnv[key] || overload {
|
||||||
|
os.Setenv(key, value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func readFile(filename string) (envMap map[string]string, err error) {
|
||||||
|
file, err := os.Open(filename)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer file.Close()
|
||||||
|
|
||||||
|
return Parse(file)
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseLine(line string, envMap map[string]string) (key string, value string, err error) {
|
||||||
|
if len(line) == 0 {
|
||||||
|
err = errors.New("zero length string")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// ditch the comments (but keep quoted hashes)
|
||||||
|
if strings.Contains(line, "#") {
|
||||||
|
segmentsBetweenHashes := strings.Split(line, "#")
|
||||||
|
quotesAreOpen := false
|
||||||
|
var segmentsToKeep []string
|
||||||
|
for _, segment := range segmentsBetweenHashes {
|
||||||
|
if strings.Count(segment, "\"") == 1 || strings.Count(segment, "'") == 1 {
|
||||||
|
if quotesAreOpen {
|
||||||
|
quotesAreOpen = false
|
||||||
|
segmentsToKeep = append(segmentsToKeep, segment)
|
||||||
|
} else {
|
||||||
|
quotesAreOpen = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(segmentsToKeep) == 0 || quotesAreOpen {
|
||||||
|
segmentsToKeep = append(segmentsToKeep, segment)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
line = strings.Join(segmentsToKeep, "#")
|
||||||
|
}
|
||||||
|
|
||||||
|
firstEquals := strings.Index(line, "=")
|
||||||
|
firstColon := strings.Index(line, ":")
|
||||||
|
splitString := strings.SplitN(line, "=", 2)
|
||||||
|
if firstColon != -1 && (firstColon < firstEquals || firstEquals == -1) {
|
||||||
|
//this is a yaml-style line
|
||||||
|
splitString = strings.SplitN(line, ":", 2)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(splitString) != 2 {
|
||||||
|
err = errors.New("Can't separate key from value")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse the key
|
||||||
|
key = splitString[0]
|
||||||
|
if strings.HasPrefix(key, "export") {
|
||||||
|
key = strings.TrimPrefix(key, "export")
|
||||||
|
}
|
||||||
|
key = strings.Trim(key, " ")
|
||||||
|
|
||||||
|
// Parse the value
|
||||||
|
value = parseValue(splitString[1], envMap)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseValue(value string, envMap map[string]string) string {
|
||||||
|
|
||||||
|
// trim
|
||||||
|
value = strings.Trim(value, " ")
|
||||||
|
|
||||||
|
// check if we've got quoted values or possible escapes
|
||||||
|
if len(value) > 1 {
|
||||||
|
rs := regexp.MustCompile(`\A'(.*)'\z`)
|
||||||
|
singleQuotes := rs.FindStringSubmatch(value)
|
||||||
|
|
||||||
|
rd := regexp.MustCompile(`\A"(.*)"\z`)
|
||||||
|
doubleQuotes := rd.FindStringSubmatch(value)
|
||||||
|
|
||||||
|
if singleQuotes != nil || doubleQuotes != nil {
|
||||||
|
// pull the quotes off the edges
|
||||||
|
value = value[1 : len(value)-1]
|
||||||
|
}
|
||||||
|
|
||||||
|
if doubleQuotes != nil {
|
||||||
|
// expand newlines
|
||||||
|
escapeRegex := regexp.MustCompile(`\\.`)
|
||||||
|
value = escapeRegex.ReplaceAllStringFunc(value, func(match string) string {
|
||||||
|
c := strings.TrimPrefix(match, `\`)
|
||||||
|
switch c {
|
||||||
|
case "n":
|
||||||
|
return "\n"
|
||||||
|
case "r":
|
||||||
|
return "\r"
|
||||||
|
default:
|
||||||
|
return match
|
||||||
|
}
|
||||||
|
})
|
||||||
|
// unescape characters
|
||||||
|
e := regexp.MustCompile(`\\([^$])`)
|
||||||
|
value = e.ReplaceAllString(value, "$1")
|
||||||
|
}
|
||||||
|
|
||||||
|
if singleQuotes == nil {
|
||||||
|
value = expandVariables(value, envMap)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
|
||||||
|
func expandVariables(v string, m map[string]string) string {
|
||||||
|
r := regexp.MustCompile(`(\\)?(\$)(\()?\{?([A-Z0-9_]+)?\}?`)
|
||||||
|
|
||||||
|
return r.ReplaceAllStringFunc(v, func(s string) string {
|
||||||
|
submatch := r.FindStringSubmatch(s)
|
||||||
|
|
||||||
|
if submatch == nil {
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
if submatch[1] == "\\" || submatch[2] == "(" {
|
||||||
|
return submatch[0][1:]
|
||||||
|
} else if submatch[4] != "" {
|
||||||
|
return m[submatch[4]]
|
||||||
|
}
|
||||||
|
return s
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func isIgnoredLine(line string) bool {
|
||||||
|
trimmedLine := strings.Trim(line, " \n\t")
|
||||||
|
return len(trimmedLine) == 0 || strings.HasPrefix(trimmedLine, "#")
|
||||||
|
}
|
||||||
|
|
||||||
|
func doubleQuoteEscape(line string) string {
|
||||||
|
for _, c := range doubleQuoteSpecialChars {
|
||||||
|
toReplace := "\\" + string(c)
|
||||||
|
if c == '\n' {
|
||||||
|
toReplace = `\n`
|
||||||
|
}
|
||||||
|
if c == '\r' {
|
||||||
|
toReplace = `\r`
|
||||||
|
}
|
||||||
|
line = strings.Replace(line, string(c), toReplace, -1)
|
||||||
|
}
|
||||||
|
return line
|
||||||
|
}
|
3
vendor/github.com/mailgun/mailgun-go/v3/.gitignore
generated
vendored
Normal file
3
vendor/github.com/mailgun/mailgun-go/v3/.gitignore
generated
vendored
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
.DS_Store
|
||||||
|
.idea/
|
||||||
|
cmd/mailgun/mailgun
|
7
vendor/github.com/mailgun/mailgun-go/v3/.travis.yml
generated
vendored
Normal file
7
vendor/github.com/mailgun/mailgun-go/v3/.travis.yml
generated
vendored
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
language: go
|
||||||
|
|
||||||
|
env:
|
||||||
|
- GO111MODULE=on
|
||||||
|
|
||||||
|
go:
|
||||||
|
- 1.11.x
|
175
vendor/github.com/mailgun/mailgun-go/v3/CHANGELOG
generated
vendored
Normal file
175
vendor/github.com/mailgun/mailgun-go/v3/CHANGELOG
generated
vendored
Normal file
@ -0,0 +1,175 @@
|
|||||||
|
# Changelog
|
||||||
|
All notable changes to this project will be documented in this file.
|
||||||
|
|
||||||
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||||
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||||
|
|
||||||
|
## [3.6.3] - 2019-12-03
|
||||||
|
### Changes
|
||||||
|
* Calls to get stats now use epoch as the time format
|
||||||
|
|
||||||
|
## [3.6.2] - 2019-11-18
|
||||||
|
### Added
|
||||||
|
* Added `AddTemplateVariable()` to make adding variables to templates
|
||||||
|
less confusing and error prone.
|
||||||
|
|
||||||
|
## [3.6.1] - 2019-10-24
|
||||||
|
### Added
|
||||||
|
* Added `VerifyWebhookSignature()` to mailgun interface
|
||||||
|
|
||||||
|
## [3.6.1-rc.3] - 2019-07-16
|
||||||
|
### Added
|
||||||
|
* APIBaseEU and APIBaseUS to help customers change regions
|
||||||
|
* Documented how to change regions in the README
|
||||||
|
|
||||||
|
## [3.6.1-rc.2] - 2019-07-01
|
||||||
|
### Changes
|
||||||
|
* Fix the JSON response for `GetMember()`
|
||||||
|
* Typo in format string in max number of tags error
|
||||||
|
|
||||||
|
## [3.6.0] - 2019-06-26
|
||||||
|
### Added
|
||||||
|
* Added UpdateClickTracking() to modify click tracking for a domain
|
||||||
|
* Added UpdateUnsubscribeTracking() to modify unsubscribe tracking for a domain
|
||||||
|
* Added UpdateOpenTracking() to modify open tracking for a domain
|
||||||
|
|
||||||
|
## [3.5.0] - 2019-05-21
|
||||||
|
### Added
|
||||||
|
* Added notice in README about go dep bug.
|
||||||
|
* Added endpoints for webhooks in mock server
|
||||||
|
### Changes
|
||||||
|
* Change names of some parameters on public methods to make their use clearer.
|
||||||
|
* Changed signature of `GetWebhook()` now returns []string.
|
||||||
|
* Changed signature of `ListWebhooks()` now returns map[string][]string.
|
||||||
|
* Both `GetWebhooks()` and `ListWebhooks()` now handle new and legacy webhooks properly.
|
||||||
|
|
||||||
|
## [3.4.0] - 2019-04-23
|
||||||
|
### Added
|
||||||
|
* Added `Message.SetTemplate()` to allow sending with the body of a template.
|
||||||
|
### Changes
|
||||||
|
* Changed signature of `CreateDomain()` moved password into `CreateDomainOptions`
|
||||||
|
|
||||||
|
## [3.4.0] - 2019-04-23
|
||||||
|
### Added
|
||||||
|
* Added `Message.SetTemplate()` to allow sending with the body of a template.
|
||||||
|
### Changes
|
||||||
|
* Changed signature of `CreateDomain()` moved password into `CreateDomainOptions`
|
||||||
|
|
||||||
|
## [3.3.2] - 2019-03-28
|
||||||
|
### Changes
|
||||||
|
* Uncommented DeliveryStatus.Code and change it to an integer (See #175)
|
||||||
|
* Added UserVariables to all Message events (See #176)
|
||||||
|
|
||||||
|
## [3.3.1] - 2019-03-13
|
||||||
|
### Changes
|
||||||
|
* Updated Template calls to reflect the most recent Template API changes.
|
||||||
|
* GetStoredMessage() now accepts a URL instead of an id
|
||||||
|
* Deprecated GetStoredMessageForURL()
|
||||||
|
* Deprecated GetStoredMessageRawForURL()
|
||||||
|
* Fixed GetUnsubscribed()
|
||||||
|
|
||||||
|
### Added
|
||||||
|
* Added `GetStoredAttachment()`
|
||||||
|
|
||||||
|
### Removed
|
||||||
|
* Method `DeleteStoredMessage()` mailgun API no long allows this call
|
||||||
|
|
||||||
|
## [3.3.0] - 2019-01-28
|
||||||
|
### Changes
|
||||||
|
* Changed signature of CreateDomain() Now returns JSON response
|
||||||
|
* Changed signature of GetDomain() Now returns a single DomainResponse
|
||||||
|
* Clarified installation notes for non golang module users
|
||||||
|
* Changed 'Public Key' to 'Public Validation Key' in readme
|
||||||
|
* Fixed issue with Next() for limit/skip based iterators
|
||||||
|
|
||||||
|
### Added
|
||||||
|
* Added VerifyDomain()
|
||||||
|
|
||||||
|
## [3.2.0] - 2019-01-21
|
||||||
|
### Changes
|
||||||
|
* Deprecated mg.VerifyWebhookRequest()
|
||||||
|
|
||||||
|
### Added
|
||||||
|
* Added mailgun.ParseEvent()
|
||||||
|
* Added mailgun.ParseEvents()
|
||||||
|
* Added mg.VerifyWebhookSignature()
|
||||||
|
|
||||||
|
|
||||||
|
## [3.1.0] - 2019-01-16
|
||||||
|
### Changes
|
||||||
|
* Removed context.Context from ListDomains() signature
|
||||||
|
* ListEventOptions.Begin and End are no longer pointers to time.Time
|
||||||
|
|
||||||
|
### Added
|
||||||
|
* Added mg.ReSend() to public Mailgun interface
|
||||||
|
* Added Message.SetSkipVerification()
|
||||||
|
* Added Message.SetRequireTLS()
|
||||||
|
|
||||||
|
## [3.0.0] - 2019-01-15
|
||||||
|
### Added
|
||||||
|
* Added CHANGELOG
|
||||||
|
* Added `AddDomainIP()`
|
||||||
|
* Added `ListDomainIPS()`
|
||||||
|
* Added `DeleteDomainIP()`
|
||||||
|
* Added `ListIPS()`
|
||||||
|
* Added `GetIP()`
|
||||||
|
* Added `GetDomainTracking()`
|
||||||
|
* Added `GetDomainConnection()`
|
||||||
|
* Added `UpdateDomainConnection()`
|
||||||
|
* Added `CreateExport()`
|
||||||
|
* Added `ListExports()`
|
||||||
|
* Added `GetExports()`
|
||||||
|
* Added `GetExportLink()`
|
||||||
|
* Added `CreateTemplate()`
|
||||||
|
* Added `GetTemplate()`
|
||||||
|
* Added `UpdateTemplate()`
|
||||||
|
* Added `DeleteTemplate()`
|
||||||
|
* Added `ListTemplates()`
|
||||||
|
* Added `AddTemplateVersion()`
|
||||||
|
* Added `GetTemplateVersion()`
|
||||||
|
* Added `UpdateTemplateVersion()`
|
||||||
|
* Added `DeleteTemplateVersion()`
|
||||||
|
* Added `ListTemplateVersions()`
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
* Added a `mailgun.MockServer` which duplicates part of the mailgun API; suitable for testing
|
||||||
|
* `ListMailingLists()` now uses the `/pages` API and returns an iterator
|
||||||
|
* `ListMembers()` now uses the `/pages` API and returns an iterator
|
||||||
|
* Renamed public interface methods to be consistent. IE: `GetThing(), ListThing(), CreateThing()`
|
||||||
|
* Moved event objects into the `mailgun/events` package, so names like
|
||||||
|
`MailingList` returned by API calls and `MailingList` as an event object
|
||||||
|
don't conflict and confuse users.
|
||||||
|
* Now using context.Context for all network operations
|
||||||
|
* Test suite will run without MG_ env vars defined
|
||||||
|
* ListRoutes() now uses the iterator interface
|
||||||
|
* Added SkipNetworkTest()
|
||||||
|
* Renamed GetStatsTotals() to GetStats()
|
||||||
|
* Renamed GetUnsubscribes to ListUnsubscribes()
|
||||||
|
* Renamed Unsubscribe() to CreateUnsubscribe()
|
||||||
|
* Renamed RemoveUnsubscribe() to DeleteUnsubscribe()
|
||||||
|
* GetStats() now takes an `*opt` argument to pass optional parameters
|
||||||
|
* Modified GetUnsubscribe() to follow the API
|
||||||
|
* Now using golang modules
|
||||||
|
* ListCredentials() now returns an iterator
|
||||||
|
* ListUnsubscribes() now returns an paging iterator
|
||||||
|
* CreateDomain now accepts CreateDomainOption{}
|
||||||
|
* CreateDomain() now supports all optional parameters not just spam_action and wildcard.
|
||||||
|
* ListComplaints() now returns a page iterator
|
||||||
|
* Renamed `TagItem` to `Tag`
|
||||||
|
* ListBounces() now returns a page iterator
|
||||||
|
* API responses with CreatedAt fields are now unmarshalled into RFC2822
|
||||||
|
* DomainList() now returns an iterator
|
||||||
|
* Updated godoc documentation
|
||||||
|
* Renamed ApiBase to APIBase
|
||||||
|
* Updated copyright to 2019
|
||||||
|
* `ListEvents()` now returns a list of typed events
|
||||||
|
|
||||||
|
### Removed
|
||||||
|
* Removed more deprecated types
|
||||||
|
* Removed gobuffalo/envy dependency
|
||||||
|
* Remove mention of the CLI in the README
|
||||||
|
* Removed mailgun cli from project
|
||||||
|
* Removed GetCode() from `Bounce` struct. Verified API returns 'string' and not 'int'
|
||||||
|
* Removed deprecated methods NewMessage and NewMIMEMessage
|
||||||
|
* Removed ginkgo and gomega tests
|
||||||
|
* Removed GetStats() As the /stats endpoint is depreciated
|
27
vendor/github.com/mailgun/mailgun-go/v3/LICENSE
generated
vendored
Normal file
27
vendor/github.com/mailgun/mailgun-go/v3/LICENSE
generated
vendored
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
Copyright (c) 2013-2016, Michael Banzon
|
||||||
|
All rights reserved.
|
||||||
|
|
||||||
|
Redistribution and use in source and binary forms, with or without modification,
|
||||||
|
are permitted provided that the following conditions are met:
|
||||||
|
|
||||||
|
* Redistributions of source code must retain the above copyright notice, this
|
||||||
|
list of conditions and the following disclaimer.
|
||||||
|
|
||||||
|
* Redistributions in binary form must reproduce the above copyright notice, this
|
||||||
|
list of conditions and the following disclaimer in the documentation and/or
|
||||||
|
other materials provided with the distribution.
|
||||||
|
|
||||||
|
* Neither the names of Mailgun, Michael Banzon, nor the names of its
|
||||||
|
contributors may be used to endorse or promote products derived from
|
||||||
|
this software without specific prior written permission.
|
||||||
|
|
||||||
|
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
||||||
|
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||||
|
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||||
|
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
|
||||||
|
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
||||||
|
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||||
|
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
|
||||||
|
ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||||
|
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||||
|
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user