Compare commits

..

3 Commits
master ... gapi

Author SHA1 Message Date
AJ ONeal 5db5fcb5e6 drive perms 2020-04-10 19:41:16 +00:00
AJ ONeal 0df4535e17 more scopes and tokens and lists 2020-04-10 19:41:16 +00:00
AJ ONeal 86e605f265 WIP: add basic stuff 2020-04-10 19:41:16 +00:00
174 changed files with 508 additions and 27493 deletions

9
.gitignore vendored
View File

@ -1,15 +1,6 @@
/public-jwks
/go-mockid
# ---> Security
.env
# ---> Vim
.*.sw*
# ---> Node
node_modules
# ---> Go
# Binaries for programs and plugins
*.exe

View File

@ -1 +0,0 @@
vendor

View File

@ -1 +0,0 @@
vendor/

View File

@ -1,81 +1,3 @@
# go-mockid
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() { });`
OAuth2 / JWT / OpenID Connect for mocking auth... which isn't that different from doing it for real, actually.

View File

@ -1,65 +0,0 @@
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
}

View File

@ -1,7 +0,0 @@
{
"kty": "EC",
"crv": "P-256",
"d": "GYAwlBHc2mPsj1lp315HbYOmKNJ7esmO3JAkZVn9nJs",
"x": "ToL2HppsTESXQKvp7ED6NMgV4YnwbMeONexNry3KDNQ",
"y": "Tt6Q3rxU37KAinUV9PLMlwosNy1t3Bf2VDg5q955AGc"
}

View File

@ -1,7 +0,0 @@
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

View File

@ -1 +0,0 @@
go test -mod=vendor -v ./...

11
go.mod
View File

@ -1,12 +1,5 @@
module git.coolaj86.com/coolaj86/go-mockid
go 1.13
go 1.12
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
)
require github.com/joho/godotenv v1.3.0

24
go.sum
View File

@ -1,26 +1,2 @@
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/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=

View File

@ -1,131 +0,0 @@
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
}

View File

@ -1,63 +0,0 @@
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")
}
}

View File

@ -1,19 +1,16 @@
package main
import (
"encoding/json"
"flag"
"fmt"
"io/ioutil"
"log"
"math/rand"
"net/http"
"net/url"
"os"
"strconv"
"time"
"git.coolaj86.com/coolaj86/go-mockid/mockid"
"git.rootprojects.org/root/keypairs"
_ "github.com/joho/godotenv/autoload"
)
@ -23,7 +20,21 @@ func main() {
var port int
var host string
rand.Seed(time.Now().UnixNano())
jwkm := map[string]string{
"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")
urlFlag := flag.String("url", "", "Outward-facing address, such as https://example.com")
@ -41,20 +52,6 @@ func main() {
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 {
host = *urlFlag
} else {
@ -67,15 +64,15 @@ func main() {
} else {
jwksPrefix = "public-jwks"
}
err = os.MkdirAll(jwksPrefix, 0755)
err := os.MkdirAll(jwksPrefix, 0755)
if nil != err {
fmt.Fprintf(os.Stderr, "couldn't write %q: %s", jwksPrefix, err)
os.Exit(1)
}
mux := mockid.Route(jwksPrefix, privkey)
mockid.Route(jwksPrefix, priv, jwk)
fs := http.FileServer(http.Dir("./public"))
fs := http.FileServer(http.Dir("public"))
http.Handle("/", fs)
/*
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
@ -86,16 +83,15 @@ func main() {
fmt.Printf("Serving on port %d\n", port)
go func() {
log.Fatal(http.ListenAndServe(":"+strconv.Itoa(port), mux))
log.Fatal(http.ListenAndServe(":"+strconv.Itoa(port), nil))
done <- true
}()
// TODO privB := keypairs.MarshalJWKPrivateKey(privkey)
privB := keypairs.MarshalJWKPrivateKey(privkey)
fmt.Printf("Private Key:\n\t%s\n", string(privB))
pubB := keypairs.MarshalJWKPublicKey(keypairs.NewPublicKey(privkey.Public()))
fmt.Printf("Public Key:\n\t%s\n", string(pubB))
protected, payload, token := mockid.GenToken(host, privkey, url.Values{})
b, _ := json.Marshal(jwk)
fmt.Printf("Private Key:\n\t%s\n", string(b))
b, _ = json.Marshal(jwk.PublicJWK)
fmt.Printf("Public Key:\n\t%s\n", string(b))
protected, payload, token := mockid.GenToken(host, priv, url.Values{})
fmt.Printf("Protected (Header):\n\t%s\n", protected)
fmt.Printf("Payload (Claims):\n\t%s\n", payload)
fmt.Printf("Access Token:\n\t%s\n", token)

View File

@ -1,71 +0,0 @@
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
}

View File

@ -1,145 +0,0 @@
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
}

View File

@ -1,57 +0,0 @@
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'))
}

View File

@ -1,87 +0,0 @@
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'))
}

View File

@ -1,101 +0,0 @@
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
}

View File

@ -1,46 +0,0 @@
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
}

View File

@ -1,27 +1,29 @@
package mockid
import (
"crypto"
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"crypto/rsa"
"crypto/sha256"
"crypto/sha512"
"encoding/base64"
"encoding/json"
"fmt"
"io"
"io/ioutil"
"log"
"math/big"
"net/http"
"net/url"
"os"
"path/filepath"
"strconv"
"strings"
"time"
"git.rootprojects.org/root/keypairs"
//jwt "github.com/dgrijalva/jwt-go"
)
// TestMain will overwrite this
var rndsrc io.Reader = rand.Reader
type PrivateJWK struct {
PublicJWK
D string `json:"d"`
}
type PublicJWK struct {
Crv string `json:"crv"`
@ -31,79 +33,332 @@ type PublicJWK struct {
Y string `json:"y"`
}
type KVDB interface {
Load(key interface{}) (value interface{}, ok bool, err error)
Store(key interface{}, value interface{}) (err error)
Delete(key interface{}) (err error)
Vacuum() (err error)
var nonces map[string]int64
func init() {
nonces = make(map[string]int64)
}
type InspectableToken struct {
Public keypairs.PublicKey `json:"jwk"`
Protected map[string]interface{} `json:"protected"`
Payload map[string]interface{} `json:"payload"`
Signature string `json:"signature"`
Verified bool `json:"verified"`
Errors []string `json:"errors"`
}
func Route(jwksPrefix string, priv *ecdsa.PrivateKey, jwk *PrivateJWK) {
pub := &priv.PublicKey
thumbprint := thumbprintKey(pub)
func (t *InspectableToken) MarshalJSON() ([]byte, error) {
pub := keypairs.MarshalJWKPublicKey(t.Public)
header, _ := json.Marshal(t.Protected)
payload, _ := json.Marshal(t.Payload)
errs, _ := json.Marshal(t.Errors)
return []byte(fmt.Sprintf(
`{"jwk":%s,"protected":%s,"payload":%s,"signature":%q,"verified":%t,"errors":%s}`,
pub, header, payload, t.Signature, t.Verified, errs,
)), nil
}
http.HandleFunc("/api/new-nonce", func(w http.ResponseWriter, r *http.Request) {
baseURL := getBaseURL(r)
/*
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");
var defaultFrom string
var defaultReplyTo string
w.Header().Set("X-Frame-Options", "DENY")
issueNonce(w, r)
})
var salt []byte
http.HandleFunc("/api/new-account", requireNonce(func(w http.ResponseWriter, r *http.Request) {
http.Error(w, "Not Implemented", http.StatusNotImplemented)
}))
func Init() {
var err error
salt64 := os.Getenv("SALT")
salt, err = base64.RawURLEncoding.DecodeString(salt64)
if len(salt64) < 22 || nil != err {
panic("SALT must be set as 22+ character base64")
}
defaultFrom = os.Getenv("MAILER_FROM")
defaultReplyTo = os.Getenv("MAILER_REPLY_TO")
//nonces = make(map[string]int64)
//nonCh = make(chan string)
/*
go func() {
for {
nonce := <- nonCh
nonces[nonce] = time.Now().Unix()
}
}()
*/
go func() {
for {
time.Sleep(15 * time.Second)
hashcashes.vacuum()
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 GenToken(host string, privkey keypairs.PrivateKey, query url.Values) (string, string, string) {
thumbprint := keypairs.ThumbprintPublicKey(keypairs.NewPublicKey(privkey.Public()))
// TODO keypairs.Alg(key)
alg := "ES256"
switch privkey.(type) {
case *rsa.PrivateKey:
alg = "RS256"
func parseExp(exp string) (int, error) {
if "" == exp {
exp = "15m"
}
protected := fmt.Sprintf(`{"typ":"JWT","alg":%q,"kid":"%s"}`, alg, thumbprint)
mult := 1
switch exp[len(exp)-1] {
case 'w':
mult *= 7
fallthrough
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) {
crv, ok := tok["crv"].(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(
fmt.Sprintf(`{"crv":%q,"kty":"EC","x":%q,"y":%q}`, crv, x, y),
)
alg := crv[2:]
var thumb []byte
switch alg {
case "256":
hash := sha256.Sum256(thumbprintable)
thumb = hash[:]
case "384":
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
}
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(
`{"crv":%q,"kid":%q,"kty":"EC","x":%q,"y":%q}`, crv, kid, x, y,
))
// 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) {
e, ok := tok["e"].(string)
if !ok {
http.Error(w, "Bad Request: missing 'e'", http.StatusBadRequest)
return
}
n, ok := tok["n"].(string)
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))
exp, err := time.ParseDuration(query.Get("exp"))
exp, err := parseExp(query.Get("exp"))
if nil != err {
// cryptic error code
// TODO propagate error
@ -112,61 +367,94 @@ func GenToken(host string, privkey keypairs.PrivateKey, query url.Values) (strin
payload := fmt.Sprintf(
`{"iss":"%s/","sub":"dummy","exp":%s}`,
host, strconv.FormatInt(time.Now().Add(exp*time.Second).Unix(), 10),
host, strconv.FormatInt(time.Now().Add(time.Duration(exp)*time.Second).Unix(), 10),
)
payload64 := base64.RawURLEncoding.EncodeToString([]byte(payload))
hash := sha256.Sum256([]byte(fmt.Sprintf(`%s.%s`, protected64, payload64)))
sig := JOSESign(privkey, hash[:])
sig64 := base64.RawURLEncoding.EncodeToString(sig)
token := fmt.Sprintf("%s.%s.%s\n", protected64, payload64, sig64)
r, s, _ := ecdsa.Sign(rand.Reader, priv, 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...)
}
sig64 := base64.RawURLEncoding.EncodeToString(append(rb, sb...))
token := fmt.Sprintf(`%s.%s.%s`, protected64, payload64, sig64)
return protected, payload, token
}
func JOSESign(privkey keypairs.PrivateKey, hash []byte) []byte {
var sig []byte
switch k := privkey.(type) {
case *rsa.PrivateKey:
panic("TODO: implement rsa sign")
case *ecdsa.PrivateKey:
r, s, _ := ecdsa.Sign(rndsrc, 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...)
func ParseKey(jwk *PrivateJWK) *ecdsa.PrivateKey {
xb, _ := base64.RawURLEncoding.DecodeString(jwk.X)
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,
}
return sig
db, _ := base64.RawURLEncoding.DecodeString(jwk.D)
di := &big.Int{}
di.SetBytes(db)
priv := &ecdsa.PrivateKey{
PublicKey: *pub,
D: di,
}
return priv
}
// TODO: move to keypairs
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 JOSEVerify(pubkey keypairs.PublicKey, hash []byte, sig []byte) bool {
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()
switch pub := pubkey.Key().(type) {
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
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
}
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
delete(nonces, nonce)
issueNonce(w, r)
next(w, r)
}
}
func getBaseURL(r *http.Request) string {
var scheme string
if nil != r.TLS || "https" == r.Header.Get("X-Forwarded-Proto") {
scheme = "https:"
} else {
scheme = "http:"
}
return fmt.Sprintf(
"%s//%s",
scheme,
r.Host,
)
}

View File

@ -1,476 +1,12 @@
package mockid
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"
"git.coolaj86.com/coolaj86/go-mockid/xkeypairs"
"git.rootprojects.org/root/keypairs"
"time"
//keypairs "github.com/big-squid/go-keypairs"
//"github.com/big-squid/go-keypairs/keyfetch/uncached"
)
var srv *httptest.Server
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"))
}
func TestTest(t *testing.T) {
t.Fatal("no test")
}

View File

@ -1,64 +0,0 @@
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
}

File diff suppressed because it is too large Load Diff

View File

@ -1,46 +0,0 @@
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
}
*/

View File

@ -1,70 +0,0 @@
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()),
}
}

View File

@ -1,173 +0,0 @@
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,
))
}

View File

@ -1,173 +0,0 @@
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
}

View File

@ -1,172 +0,0 @@
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
View File

@ -1 +0,0 @@
package-lock.json

View File

@ -1,16 +0,0 @@
{ "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
}

View File

@ -1 +0,0 @@
dist/

View File

@ -1,8 +0,0 @@
{
"bracketSpacing": true,
"printWidth": 80,
"singleQuote": true,
"tabWidth": 4,
"trailingComma": "none",
"useTabs": true
}

60
public/app.js Normal file
View File

@ -0,0 +1,60 @@
'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.');
});
}

View File

@ -1,45 +1,12 @@
<html>
<head>
<meta name="google-signin-scope" content="email" />
<meta
name="google-signin-client_id"
content="291138637698-9hjbgadgkibuv9j26104aj0bg5bia30j.apps.googleusercontent.com"
/>
<meta
name="google-signin-client_id"
content="128764648444-nk2ss16gmals7rhsk2kj0i0ove0v0tnk.apps.googleusercontent.com"
/>
<!-- https://developers.google.com/identity/sign-in/web/sign-in -->
<!-- https://developers.google.com/identity/sign-in/web/quick-migration-guide -->
<!-- Note: You can also specify your app's client ID with the client_id parameter of the gapi.auth2.init() method. -->
<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>
<pre><code>
<h1>Tokens for Testing</h1>
Compatible with
@ -54,7 +21,6 @@ Compatible with
* https://mock.pocketid.app/access_token
* 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/jwks.json
* https://mock.pocketid.app/key.jwk.json
@ -90,13 +56,6 @@ For example:
HEADER=$(curl -fL https://mock.pocketid.app/authorization_header)
# Authorization: Bearer &lt;token&gt;
<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>
The Token will look like this:
@ -154,110 +113,12 @@ You shouldn't use it for automated testing, because it will change, but it looks
}
</code></pre>
</div>
<div class="authn-flow">
<div class="authn-container authn-loading">
<center></center>
</div>
<div class="authn-container authn-email">
<center>
<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>
<script src="https://apis.google.com/js/platform.js" async defer></script>
<div
class="g-signin2"
data-onsuccess="onSignIn"
data-scope="profile email https://www.googleapis.com/auth/spreadsheets.readonly https://www.googleapis.com/auth/drive.readonly"
></div>
<a href="#" onclick="signOut();">Sign out</a>
<script src="app.js"></script>

View File

@ -1,3 +0,0 @@
'use strict';
require('./pocket/consumer.js');

View File

@ -1,34 +0,0 @@
{
"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"
}
}

View File

@ -1,19 +0,0 @@
'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: []
};

View File

@ -1,19 +0,0 @@
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/

View File

@ -1,312 +0,0 @@
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.

View File

@ -1,41 +0,0 @@
# 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"
```

View File

@ -1,3 +0,0 @@
module git.rootprojects.org/root/hashcash
go 1.15

View File

@ -1,284 +0,0 @@
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++
}
}

View File

@ -1,5 +0,0 @@
/keypairs
/dist/
.DS_Store
.*.sw*

View File

@ -1,41 +0,0 @@
# 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:'

View File

@ -1 +0,0 @@
AJ ONeal <aj@therootcompany.com> (https://therootcompany.com)

View File

@ -1,21 +0,0 @@
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.

View File

@ -1,63 +0,0 @@
# [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>.

View File

@ -1,19 +0,0 @@
#!/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

View File

@ -1,40 +0,0 @@
/*
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

View File

@ -1,69 +0,0 @@
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
}

View File

@ -1,3 +0,0 @@
module git.rootprojects.org/root/keypairs
go 1.12

View File

@ -1,69 +0,0 @@
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()),
}
}
*/

View File

@ -1,63 +0,0 @@
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
}

View File

@ -1,516 +0,0 @@
// 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, " ")
}

View File

@ -1,183 +0,0 @@
// 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, "/") + "/"
}

View File

@ -1,645 +0,0 @@
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
}

View File

@ -1,171 +0,0 @@
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,
))
}

View File

@ -1,46 +0,0 @@
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
}

View File

@ -1,165 +0,0 @@
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
}

View File

@ -1,174 +0,0 @@
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
}
}

View File

@ -1,3 +0,0 @@
.idea
*.sw?
.vscode

View File

@ -1,17 +0,0 @@
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

View File

@ -1,139 +0,0 @@
# 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")`

View File

@ -1,31 +0,0 @@
# 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
View File

@ -1,20 +0,0 @@
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.

View File

@ -1,438 +0,0 @@
# <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

View File

@ -1,49 +0,0 @@
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
View File

@ -1,134 +0,0 @@
//
// 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

View File

@ -1,161 +0,0 @@
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
View File

@ -1,460 +0,0 @@
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
View File

@ -1,847 +0,0 @@
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
}

View File

@ -1,9 +0,0 @@
language: go
go:
- 1.4.3
- 1.5.3
- tip
script:
- go test -v ./...

View File

@ -1,10 +0,0 @@
# 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.

View File

@ -1,9 +0,0 @@
Paul Borman <borman@google.com>
bmatsuo
shawnps
theory
jboverfelt
dsymonds
cd1
wallclockbuilder
dansouza

View File

@ -1,27 +0,0 @@
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.

View File

@ -1,19 +0,0 @@
# uuid ![build status](https://travis-ci.org/google/uuid.svg?branch=master)
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
[![GoDoc](https://godoc.org/github.com/google/uuid?status.svg)](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
View File

@ -1,80 +0,0 @@
// 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
View File

@ -1,12 +0,0 @@
// 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

View File

@ -1 +0,0 @@
module github.com/google/uuid

View File

@ -1,53 +0,0 @@
// 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)
}

View File

@ -1,37 +0,0 @@
// 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
}

View File

@ -1,90 +0,0 @@
// 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[:]
}

View File

@ -1,12 +0,0 @@
// 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 }

View File

@ -1,33 +0,0 @@
// 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
View File

@ -1,59 +0,0 @@
// 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
View File

@ -1,123 +0,0 @@
// 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
}

View File

@ -1,43 +0,0 @@
// 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
View File

@ -1,245 +0,0 @@
// 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
}

View File

@ -1,44 +0,0 @@
// 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
}

View File

@ -1,38 +0,0 @@
// 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 × 1011),
// 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
}

View File

@ -1 +0,0 @@
.DS_Store

View File

@ -1,8 +0,0 @@
language: go
go:
- 1.x
os:
- linux
- osx

View File

@ -1,23 +0,0 @@
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.

View File

@ -1,163 +0,0 @@
# GoDotEnv [![Build Status](https://travis-ci.org/joho/godotenv.svg?branch=master)](https://travis-ci.org/joho/godotenv) [![Build status](https://ci.appveyor.com/api/projects/status/9v40vnfvvgde64u4?svg=true)](https://ci.appveyor.com/project/joho/godotenv) [![Go Report Card](https://goreportcard.com/badge/github.com/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 environmentssuch as resource handles for databases or credentials for external servicesshould 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: [![Build Status](https://travis-ci.org/joho/godotenv.svg?branch=master)](https://travis-ci.org/joho/godotenv) Windows: [![Build status](https://ci.appveyor.com/api/projects/status/9v40vnfvvgde64u4)](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.

View File

@ -1,15 +0,0 @@
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()
}

View File

@ -1,346 +0,0 @@
// 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
}

View File

@ -1,3 +0,0 @@
.DS_Store
.idea/
cmd/mailgun/mailgun

View File

@ -1,7 +0,0 @@
language: go
env:
- GO111MODULE=on
go:
- 1.11.x

View File

@ -1,175 +0,0 @@
# 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

View File

@ -1,27 +0,0 @@
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