Compare commits
	
		
			38 Commits
		
	
	
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|  | 3f7513364a | ||
|  | 499949ba52 | ||
| 6a22bfecc4 | |||
|  | c36c0200f3 | ||
|  | 84e1863da2 | ||
|  | 881bf97334 | ||
|  | f13dc593b0 | ||
|  | 557f9085f6 | ||
|  | 9de2f796db | ||
|  | a8f1a99667 | ||
|  | 6981b852d0 | ||
|  | 673671147c | ||
|  | e7c21aa35c | ||
|  | 5ff37be8c5 | ||
|  | 23822cdf09 | ||
|  | 9608a7429b | ||
|  | e6dd414af6 | ||
|  | 05db67c8b7 | ||
|  | ca84b8dbca | ||
|  | e8c50dee76 | ||
|  | a1b4ad1202 | ||
|  | 9b250c8cbb | ||
|  | d80c8226b5 | ||
|  | bc7e9740d8 | ||
|  | da712abbb2 | ||
|  | 42f1089e6c | ||
|  | 075ade3dec | ||
|  | 153851b41d | ||
|  | 2fe128a017 | ||
|  | 87494faffe | ||
|  | 155c006740 | ||
|  | aab56909cb | ||
|  | 563907d477 | ||
|  | d914325e2f | ||
|  | 83a5642829 | ||
|  | 285dc81dd7 | ||
|  | 3ab579ad24 | ||
|  | 66e0639f48 | 
							
								
								
									
										9
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										9
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							| @ -1,6 +1,15 @@ | |||||||
| /public-jwks | /public-jwks | ||||||
| /go-mockid | /go-mockid | ||||||
| 
 | 
 | ||||||
|  | # ---> Security | ||||||
|  | .env | ||||||
|  | 
 | ||||||
|  | # ---> Vim | ||||||
|  | .*.sw* | ||||||
|  | 
 | ||||||
|  | # ---> Node | ||||||
|  | node_modules | ||||||
|  | 
 | ||||||
| # ---> Go | # ---> Go | ||||||
| # Binaries for programs and plugins | # Binaries for programs and plugins | ||||||
| *.exe | *.exe | ||||||
|  | |||||||
							
								
								
									
										1
									
								
								.prettierignore
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								.prettierignore
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1 @@ | |||||||
|  | vendor/ | ||||||
							
								
								
									
										78
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										78
									
								
								README.md
									
									
									
									
									
								
							| @ -1,3 +1,81 @@ | |||||||
| # go-mockid | # go-mockid | ||||||
| 
 | 
 | ||||||
| OAuth2 / JWT / OpenID Connect for mocking auth... which isn't that different from doing it for real, actually. | OAuth2 / JWT / OpenID Connect for mocking auth... which isn't that different from doing it for real, actually. | ||||||
|  | 
 | ||||||
|  | ## Enabling Google OAuth2 (Mid-2020) | ||||||
|  | 
 | ||||||
|  | 1. Create an account at https://console.developers.google.com/apis/dashboard | ||||||
|  | 2. Go back to https://console.developers.google.com/apis/dashboard | ||||||
|  | 3. Create a New Project from the dropdown in the upper left that lists the current project name | ||||||
|  |   4. Give the project a name such as `Example Web App` and accept its generated ID | ||||||
|  |   5. Click "Create" | ||||||
|  | 
 | ||||||
|  | Add your test domain | ||||||
|  | 
 | ||||||
|  | 1. Go back to https://console.developers.google.com/apis/dashboard | ||||||
|  | 1. Select your new project from the upper-left drop-down | ||||||
|  | 2. Select `Domain Verification` from the left hand side of the screen | ||||||
|  | 3. Add your test domain (i.e. `beta.example.com`), but a domain that you actually own | ||||||
|  | 4. Select `Verify Ownership` | ||||||
|  | 5. Follow the specific instructions for adding a txt record to the subdomain you chose | ||||||
|  | 6. Add a collaborator / co-owner if you wish | ||||||
|  | 
 | ||||||
|  | Enable OAuth2 | ||||||
|  | 
 | ||||||
|  | 1. Go back to https://console.developers.google.com/apis/dashboard | ||||||
|  | 1. Select `OAuth consent screen` | ||||||
|  | 2. Select `External` | ||||||
|  | 3. Complete the consent screen form | ||||||
|  | 
 | ||||||
|  | Create Google Credentials | ||||||
|  | 
 | ||||||
|  | 1. Go back to https://console.developers.google.com/apis/dashboard | ||||||
|  | 1. Select `Credentials` from the left sidebar | ||||||
|  | 2. Select `OAuth ID` | ||||||
|  | 3. Select `Web Application` | ||||||
|  | 4. Fill out the same test domain and test app name as before | ||||||
|  | 5. Save the ID and Secret to a place you won't forget (perhaps a .gitignored .env) | ||||||
|  | 
 | ||||||
|  | Update your signin page. | ||||||
|  | 
 | ||||||
|  | 1. You need to put your default scopes (i.e. `profile email`) and client ID in the meta tag of your login page HTML. `profile` is the minimum scope and is always returned. | ||||||
|  |   ```html | ||||||
|  |   <head> | ||||||
|  |     <meta name="google-signin-scope" content="email"> | ||||||
|  |     <meta | ||||||
|  |       name="google-signin-client_id" | ||||||
|  |       content="xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx.apps.googleusercontent.com" | ||||||
|  |     /> | ||||||
|  |   </head> | ||||||
|  |   ``` | ||||||
|  | 2. Although it should be possible to use an thin OAuth client, you'll probably want to start by including the (huge) Google platform.js | ||||||
|  |   ```html | ||||||
|  |   <script src="https://apis.google.com/js/platform.js" async defer></script> | ||||||
|  |   ``` | ||||||
|  | 3. You can start off with the Google's sign in button, but you need your own `data-onsuccess` callback. You can also adjust the `data-scope` per button to include more stuff. Scopes are defined at https://developers.google.com/identity/protocols/oauth2/scopes | ||||||
|  |   ```html | ||||||
|  |   <div | ||||||
|  |     class="g-signin2" | ||||||
|  |     data-onsuccess="ongsignin" | ||||||
|  |     data-scope="profile email https://www.googleapis.com/auth/spreadsheets.readonly https://www.googleapis.com/auth/drive.readonly" | ||||||
|  |   ></div> | ||||||
|  |   <script> | ||||||
|  |     window.ongsignin = function (gauth) { | ||||||
|  |       // Note: this is a special prototype-style instance object with few | ||||||
|  |       // enumerable properties (which don't make sense). Requires API docs. | ||||||
|  |       // See https://developers.google.com/identity/sign-in/web | ||||||
|  |       console.log(goauth) | ||||||
|  |     }; | ||||||
|  |   </script> | ||||||
|  |   ``` | ||||||
|  | 4. Despite the documentation stating that passing a token as a query is deprecated and to use the `Authorization` header, the inspect token URL only supports the query parameter: `GET https://oauth2.googleapis.com/tokeninfo?id_token=<token>` | ||||||
|  |   - You can also validate the token with Google's public key | ||||||
|  |   - https://accounts.google.com/.well-known/openid-configuration | ||||||
|  |   - https://www.googleapis.com/oauth2/v3/certs (note that one of the Key IDs will match that of your kid) | ||||||
|  | 5. While testing you'll probably want to revoke the app's permissions | ||||||
|  |   - Go to https://myaccount.google.com/permissions | ||||||
|  |   - Under "Third-party apps with account access" click "Manage third-party access" and search in the long list and click "Remove access". | ||||||
|  |   - Under "Signing in to other sites" click "Signing in with Google" and search in the list to revoke access | ||||||
|  |   - Active tokens will persist until they expire (1 hour), so you may need to clear cache, cookies, etc, which can be a pain | ||||||
|  | 5. Sign out can be accomplished with a button that calls `gapi.auth2.getAuthInstance().signOut().then(function() { });` | ||||||
|  | 
 | ||||||
|  | |||||||
							
								
								
									
										65
									
								
								cmd/mailer/mailer.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										65
									
								
								cmd/mailer/mailer.go
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,65 @@ | |||||||
|  | package main | ||||||
|  | 
 | ||||||
|  | import ( | ||||||
|  | 	"context" | ||||||
|  | 	"flag" | ||||||
|  | 	"fmt" | ||||||
|  | 	"os" | ||||||
|  | 	"time" | ||||||
|  | 
 | ||||||
|  | 	mailgun "github.com/mailgun/mailgun-go/v3" | ||||||
|  | 
 | ||||||
|  | 	_ "github.com/joho/godotenv/autoload" | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | func main() { | ||||||
|  | 	/* | ||||||
|  | 	  MAILGUN_API_KEY=key-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx | ||||||
|  | 	  MAILGUN_DOMAIN=mail.example.com | ||||||
|  | 	  MAILER_FROM="Rob the Robot <rob.the.robot@mail.example.com>" | ||||||
|  | 	*/ | ||||||
|  | 
 | ||||||
|  | 	to := flag.String("to", "", "message recipient in the format of 'John Doe <john@example.com>'") | ||||||
|  | 	replyTo := flag.String("reply-to", "", "reply-to in the format of 'John Doe <john@example.com>'") | ||||||
|  | 	subject := flag.String("subject", "Test Subject", "the utf8-encoded subject of the email") | ||||||
|  | 	text := flag.String( | ||||||
|  | 		"text", | ||||||
|  | 		"Testing some Mailgun awesomeness!", | ||||||
|  | 		"the body of the email as utf8-encoded plain-text format", | ||||||
|  | 	) | ||||||
|  | 	flag.Parse() | ||||||
|  | 
 | ||||||
|  | 	if 0 == len(*to) { | ||||||
|  | 		flag.Usage() | ||||||
|  | 		os.Exit(1) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	domain := os.Getenv("MAILGUN_DOMAIN") | ||||||
|  | 	apiKey := os.Getenv("MAILGUN_API_KEY") | ||||||
|  | 	from := os.Getenv("MAILER_FROM") | ||||||
|  | 
 | ||||||
|  | 	if 0 == len(*text) { | ||||||
|  | 		*text = "Testing some Mailgun awesomeness!" | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	msgId, err := SendSimpleMessage(domain, apiKey, *to, from, *subject, *text, *replyTo) | ||||||
|  | 	if nil != err { | ||||||
|  | 		panic(err) | ||||||
|  | 	} | ||||||
|  | 	fmt.Printf("Queued with Message ID %q\n", msgId) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func SendSimpleMessage(domain, apiKey, to, from, subject, text, replyTo string) (string, error) { | ||||||
|  | 	mg := mailgun.NewMailgun(domain, apiKey) | ||||||
|  | 	m := mg.NewMessage(from, subject, text, to) | ||||||
|  | 	if 0 != len(replyTo) { | ||||||
|  | 		// mailgun's required "h:" prefix is added by the library | ||||||
|  | 		m.AddHeader("Reply-To", replyTo) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	ctx, cancel := context.WithTimeout(context.Background(), time.Second*30) | ||||||
|  | 	defer cancel() | ||||||
|  | 
 | ||||||
|  | 	_, id, err := mg.Send(ctx, m) | ||||||
|  | 	return id, err | ||||||
|  | } | ||||||
							
								
								
									
										7
									
								
								default.jwk.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								default.jwk.json
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,7 @@ | |||||||
|  | { | ||||||
|  |   "kty": "EC", | ||||||
|  |   "crv": "P-256", | ||||||
|  |   "d": "GYAwlBHc2mPsj1lp315HbYOmKNJ7esmO3JAkZVn9nJs", | ||||||
|  |   "x": "ToL2HppsTESXQKvp7ED6NMgV4YnwbMeONexNry3KDNQ", | ||||||
|  |   "y": "Tt6Q3rxU37KAinUV9PLMlwosNy1t3Bf2VDg5q955AGc" | ||||||
|  | } | ||||||
							
								
								
									
										7
									
								
								examples/example.env
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								examples/example.env
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,7 @@ | |||||||
|  | SALT=XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX | ||||||
|  | 
 | ||||||
|  | MAILGUN_API_KEY=key-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx | ||||||
|  | MAILGUN_DOMAIN=mail.example.com | ||||||
|  | 
 | ||||||
|  | MAILER_FROM="Rob the Robot <rob.the.robot@mail.example.com>" | ||||||
|  | MAILER_REPLY_TO=support@example.com | ||||||
							
								
								
									
										1
									
								
								go-test.sh
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								go-test.sh
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1 @@ | |||||||
|  | go test -mod=vendor -v ./... | ||||||
							
								
								
									
										11
									
								
								go.mod
									
									
									
									
									
								
							
							
						
						
									
										11
									
								
								go.mod
									
									
									
									
									
								
							| @ -1,5 +1,12 @@ | |||||||
| module git.coolaj86.com/coolaj86/go-mockid | module git.coolaj86.com/coolaj86/go-mockid | ||||||
| 
 | 
 | ||||||
| go 1.12 | go 1.13 | ||||||
| 
 | 
 | ||||||
| require github.com/joho/godotenv v1.3.0 | require ( | ||||||
|  | 	git.rootprojects.org/root/hashcash v1.0.1 | ||||||
|  | 	git.rootprojects.org/root/keypairs v0.6.5 | ||||||
|  | 	github.com/google/uuid v1.1.1 | ||||||
|  | 	github.com/joho/godotenv v1.3.0 | ||||||
|  | 	github.com/mailgun/mailgun-go/v3 v3.6.4 | ||||||
|  | 	github.com/mileusna/useragent v1.0.2 | ||||||
|  | ) | ||||||
|  | |||||||
							
								
								
									
										24
									
								
								go.sum
									
									
									
									
									
								
							
							
						
						
									
										24
									
								
								go.sum
									
									
									
									
									
								
							| @ -1,2 +1,26 @@ | |||||||
|  | git.rootprojects.org/root/hashcash v1.0.1 h1:PkzwZu4CR5q/hwAntJdvcmNhmP0ONhetMo7rYhIZhZ0= | ||||||
|  | git.rootprojects.org/root/hashcash v1.0.1/go.mod h1:HdoULUe94o1NVMES5K6aP3p8QGQiIia73F1HNZ1+FkQ= | ||||||
|  | git.rootprojects.org/root/keypairs v0.6.5 h1:sdRAQD/O/JBS8+ZxUewXnY+cjQVDNH3TmcS+KtANZqA= | ||||||
|  | git.rootprojects.org/root/keypairs v0.6.5/go.mod h1:WGI8PadOp+4LjUuI+wNlSwcJwFtY8L9XuNjuO3213HA= | ||||||
|  | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= | ||||||
|  | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= | ||||||
|  | github.com/facebookgo/ensure v0.0.0-20160127193407-b4ab57deab51 h1:0JZ+dUmQeA8IIVUMzysrX4/AKuQwWhV2dYQuPZdvdSQ= | ||||||
|  | github.com/facebookgo/ensure v0.0.0-20160127193407-b4ab57deab51/go.mod h1:Yg+htXGokKKdzcwhuNDwVvN+uBxDGXJ7G/VN1d8fa64= | ||||||
|  | github.com/facebookgo/stack v0.0.0-20160209184415-751773369052 h1:JWuenKqqX8nojtoVVWjGfOF9635RETekkoH6Cc9SX0A= | ||||||
|  | github.com/facebookgo/stack v0.0.0-20160209184415-751773369052/go.mod h1:UbMTZqLaRiH3MsBH8va0n7s1pQYcu3uTb8G4tygF4Zg= | ||||||
|  | github.com/facebookgo/subset v0.0.0-20150612182917-8dac2c3c4870 h1:E2s37DuLxFhQDg5gKsWoLBOB0n+ZW8s599zru8FJ2/Y= | ||||||
|  | github.com/facebookgo/subset v0.0.0-20150612182917-8dac2c3c4870/go.mod h1:5tD+neXqOorC30/tWg0LCSkrqj/AR6gu8yY8/fpw1q0= | ||||||
|  | github.com/go-chi/chi v4.0.0+incompatible h1:SiLLEDyAkqNnw+T/uDTf3aFB9T4FTrwMpuYrgaRcnW4= | ||||||
|  | github.com/go-chi/chi v4.0.0+incompatible/go.mod h1:eB3wogJHnLi3x/kFX2A+IbTBlXxmMeXJVKy9tTv1XzQ= | ||||||
|  | github.com/google/uuid v1.1.1 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY= | ||||||
|  | github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= | ||||||
| github.com/joho/godotenv v1.3.0 h1:Zjp+RcGpHhGlrMbJzXTrZZPrWj+1vfm90La1wgB6Bhc= | github.com/joho/godotenv v1.3.0 h1:Zjp+RcGpHhGlrMbJzXTrZZPrWj+1vfm90La1wgB6Bhc= | ||||||
| github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg= | github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg= | ||||||
|  | github.com/mailgun/mailgun-go/v3 v3.6.4 h1:+cvbZRgLSHivbz/w1iWLmxVl6Bqf4geD2D7QMj4+8PE= | ||||||
|  | github.com/mailgun/mailgun-go/v3 v3.6.4/go.mod h1:ZjVnH8S0dR2BLjvkZc/rxwerdcirzlA12LQDuGAadR0= | ||||||
|  | github.com/mailru/easyjson v0.0.0-20180823135443-60711f1a8329 h1:2gxZ0XQIU/5z3Z3bUBu+FXuk2pFbkN6tcwi/pjyaDic= | ||||||
|  | github.com/mailru/easyjson v0.0.0-20180823135443-60711f1a8329/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= | ||||||
|  | github.com/mileusna/useragent v1.0.2 h1:DgVKtiPnjxlb73z9bCwgdUvU2nQNQ97uhgfO8l9uz/w= | ||||||
|  | github.com/mileusna/useragent v1.0.2/go.mod h1:3d8TOmwL/5I8pJjyVDteHtgDGcefrFUX4ccGOMKNYYc= | ||||||
|  | github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= | ||||||
|  | github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= | ||||||
|  | |||||||
							
								
								
									
										131
									
								
								kvdb/kvdb.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										131
									
								
								kvdb/kvdb.go
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,131 @@ | |||||||
|  | package kvdb | ||||||
|  | 
 | ||||||
|  | import ( | ||||||
|  | 	"encoding/json" | ||||||
|  | 	"errors" | ||||||
|  | 	"fmt" | ||||||
|  | 	"io/ioutil" | ||||||
|  | 	"os" | ||||||
|  | 	"path/filepath" | ||||||
|  | 	"strconv" | ||||||
|  | 	"strings" | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | type KVDB struct { | ||||||
|  | 	Prefix string | ||||||
|  | 	Ext    string | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (kv *KVDB) Load( | ||||||
|  | 	keyif interface{}, | ||||||
|  | 	typ ...interface{}, | ||||||
|  | ) (value interface{}, ok bool, err error) { | ||||||
|  | 	key, _ := keyif.(string) | ||||||
|  | 	if "" == key || strings.Contains(key, "..") || strings.ContainsAny(key, "$#!:| \n") { | ||||||
|  | 		return nil, false, nil | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	userFile := filepath.Join(kv.Prefix, key+"."+kv.Ext) | ||||||
|  | 	fmt.Println("Debug user file:", userFile) | ||||||
|  | 	b, err := ioutil.ReadFile(userFile) | ||||||
|  | 	if nil != err { | ||||||
|  | 		if os.IsNotExist(err) { | ||||||
|  | 			return nil, false, nil | ||||||
|  | 		} | ||||||
|  | 		fmt.Println("kvdb debug read:", err) | ||||||
|  | 		return nil, false, errors.New("database read failed") | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	ok = true | ||||||
|  | 	value = b | ||||||
|  | 	if 1 == len(typ) { | ||||||
|  | 		err := json.Unmarshal(b, typ[0]) | ||||||
|  | 		if nil != err { | ||||||
|  | 			return nil, false, err | ||||||
|  | 		} | ||||||
|  | 		value = typ[0] | ||||||
|  | 	} else if len(b) > 0 && '"' == b[0] { | ||||||
|  | 		var str string | ||||||
|  | 		err := json.Unmarshal(b, &str) | ||||||
|  | 		if nil == err { | ||||||
|  | 			value = str | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	return value, ok, nil | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (kv *KVDB) Store(keyif interface{}, value interface{}) (err error) { | ||||||
|  | 	key, _ := keyif.(string) | ||||||
|  | 	if "" == key || strings.Contains(key, "..") || strings.ContainsAny(key, "$#! \n") { | ||||||
|  | 		return errors.New("invalid key name") | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	keypath := filepath.Join(kv.Prefix, key+"."+kv.Ext) | ||||||
|  | 	f, err := os.Open(keypath) | ||||||
|  | 	if nil == err { | ||||||
|  | 		s, err := f.Stat() | ||||||
|  | 		if nil != err { | ||||||
|  | 			// if we can open, we should be able to stat | ||||||
|  | 			return errors.New("database connection failure") | ||||||
|  | 		} | ||||||
|  | 		ts := strconv.FormatInt(s.ModTime().Unix(), 10) | ||||||
|  | 		bakpath := filepath.Join(kv.Prefix, key+"."+ts+"."+kv.Ext) | ||||||
|  | 		if err := os.Rename(keypath, bakpath); nil != err { | ||||||
|  | 			// keep the old record as a backup | ||||||
|  | 			return errors.New("database write failure") | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	var b []byte | ||||||
|  | 	switch v := value.(type) { | ||||||
|  | 	case []byte: | ||||||
|  | 		b = v | ||||||
|  | 	case string: | ||||||
|  | 		b, _ = json.Marshal(v) | ||||||
|  | 	default: | ||||||
|  | 		fmt.Println("kvdb: not []byte or string:", v) | ||||||
|  | 		jsonb, err := json.Marshal(v) | ||||||
|  | 		if nil != err { | ||||||
|  | 			return err | ||||||
|  | 		} | ||||||
|  | 		b = jsonb | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	if err := ioutil.WriteFile( | ||||||
|  | 		keypath, | ||||||
|  | 		b, | ||||||
|  | 		os.FileMode(0600), | ||||||
|  | 	); nil != err { | ||||||
|  | 		fmt.Println("write failure:", err) | ||||||
|  | 		return errors.New("database write failed") | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	return nil | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (kv *KVDB) Delete(keyif interface{}) (err error) { | ||||||
|  | 	key, _ := keyif.(string) | ||||||
|  | 	if "" == key || strings.Contains(key, "..") || strings.ContainsAny(key, "$#! \n") { | ||||||
|  | 		return errors.New("invalid key name") | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	keypath := filepath.Join(kv.Prefix, key+"."+kv.Ext) | ||||||
|  | 	f, err := os.Open(keypath) | ||||||
|  | 	if nil == err { | ||||||
|  | 		s, err := f.Stat() | ||||||
|  | 		if nil != err { | ||||||
|  | 			return errors.New("database connection failure") | ||||||
|  | 		} | ||||||
|  | 		ts := strconv.FormatInt(s.ModTime().Unix(), 64) | ||||||
|  | 		if err := os.Rename(keypath, filepath.Join(kv.Prefix, key+"."+ts+"."+kv.Ext)); nil != err { | ||||||
|  | 			return errors.New("database connection failure") | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	return nil | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (kv *KVDB) Vacuum() (err error) { | ||||||
|  | 	return nil | ||||||
|  | } | ||||||
							
								
								
									
										63
									
								
								kvdb/kvdb_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										63
									
								
								kvdb/kvdb_test.go
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,63 @@ | |||||||
|  | package kvdb | ||||||
|  | 
 | ||||||
|  | import ( | ||||||
|  | 	"strings" | ||||||
|  | 	"testing" | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | type TestEntry struct { | ||||||
|  | 	Email    string   `json:"email"` | ||||||
|  | 	Subjects []string `json:"subjects"` | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | var email = "john@example.com" | ||||||
|  | var sub = "id123" | ||||||
|  | var dbPrefix = "../testdb" | ||||||
|  | var testKV = &KVDB{ | ||||||
|  | 	Prefix: dbPrefix + "/test-entries", | ||||||
|  | 	Ext:    "eml.json", | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func TestStore(t *testing.T) { | ||||||
|  | 	entry := &TestEntry{ | ||||||
|  | 		Email:    email, | ||||||
|  | 		Subjects: []string{sub}, | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	if err := testKV.Store(email, entry); nil != err { | ||||||
|  | 		t.Fatal(err) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	value, ok, err := testKV.Load(email, &(TestEntry{})) | ||||||
|  | 	if nil != err { | ||||||
|  | 		t.Fatal(err) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 	if !ok { | ||||||
|  | 		t.Fatal("test entry not found") | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	v, ok := value.(*TestEntry) | ||||||
|  | 	if !ok { | ||||||
|  | 		t.Fatal("test entry not of type TestEntry") | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	if email != v.Email || sub != strings.Join(v.Subjects, ",") { | ||||||
|  | 		t.Fatalf("value: %#v", v) | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func TestNoExist(t *testing.T) { | ||||||
|  | 	value, ok, err := testKV.Load("not"+email, &(TestEntry{})) | ||||||
|  | 	if nil != err { | ||||||
|  | 		t.Fatal(err) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 	if ok { | ||||||
|  | 		t.Fatal("found entry that doesn't exist") | ||||||
|  | 	} | ||||||
|  | 	if value != nil { | ||||||
|  | 		t.Fatal("had value for entry that doesn't exist") | ||||||
|  | 	} | ||||||
|  | } | ||||||
							
								
								
									
										54
									
								
								mockid.go
									
									
									
									
									
								
							
							
						
						
									
										54
									
								
								mockid.go
									
									
									
									
									
								
							| @ -1,16 +1,19 @@ | |||||||
| package main | package main | ||||||
| 
 | 
 | ||||||
| import ( | import ( | ||||||
| 	"encoding/json" |  | ||||||
| 	"flag" | 	"flag" | ||||||
| 	"fmt" | 	"fmt" | ||||||
|  | 	"io/ioutil" | ||||||
| 	"log" | 	"log" | ||||||
|  | 	"math/rand" | ||||||
| 	"net/http" | 	"net/http" | ||||||
| 	"net/url" | 	"net/url" | ||||||
| 	"os" | 	"os" | ||||||
| 	"strconv" | 	"strconv" | ||||||
|  | 	"time" | ||||||
| 
 | 
 | ||||||
| 	"git.coolaj86.com/coolaj86/go-mockid/mockid" | 	"git.coolaj86.com/coolaj86/go-mockid/mockid" | ||||||
|  | 	"git.rootprojects.org/root/keypairs" | ||||||
| 
 | 
 | ||||||
| 	_ "github.com/joho/godotenv/autoload" | 	_ "github.com/joho/godotenv/autoload" | ||||||
| ) | ) | ||||||
| @ -20,21 +23,7 @@ func main() { | |||||||
| 	var port int | 	var port int | ||||||
| 	var host string | 	var host string | ||||||
| 
 | 
 | ||||||
| 	jwkm := map[string]string{ | 	rand.Seed(time.Now().UnixNano()) | ||||||
| 		"crv": "P-256", |  | ||||||
| 		"d":   "GYAwlBHc2mPsj1lp315HbYOmKNJ7esmO3JAkZVn9nJs", |  | ||||||
| 		"x":   "ToL2HppsTESXQKvp7ED6NMgV4YnwbMeONexNry3KDNQ", |  | ||||||
| 		"y":   "Tt6Q3rxU37KAinUV9PLMlwosNy1t3Bf2VDg5q955AGc", |  | ||||||
| 	} |  | ||||||
| 	jwk := &mockid.PrivateJWK{ |  | ||||||
| 		PublicJWK: mockid.PublicJWK{ |  | ||||||
| 			Crv: jwkm["crv"], |  | ||||||
| 			X:   jwkm["x"], |  | ||||||
| 			Y:   jwkm["y"], |  | ||||||
| 		}, |  | ||||||
| 		D: jwkm["d"], |  | ||||||
| 	} |  | ||||||
| 	priv := mockid.ParseKey(jwk) |  | ||||||
| 
 | 
 | ||||||
| 	portFlag := flag.Int("port", 0, "Port on which the HTTP server should run") | 	portFlag := flag.Int("port", 0, "Port on which the HTTP server should run") | ||||||
| 	urlFlag := flag.String("url", "", "Outward-facing address, such as https://example.com") | 	urlFlag := flag.String("url", "", "Outward-facing address, such as https://example.com") | ||||||
| @ -52,6 +41,20 @@ func main() { | |||||||
| 		os.Exit(1) | 		os.Exit(1) | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
|  | 	jwkpath := "./default.jwk.json" | ||||||
|  | 	jwkb, err := ioutil.ReadFile(jwkpath) | ||||||
|  | 	if nil != err { | ||||||
|  | 		panic(fmt.Errorf("read default jwk %v: %w", jwkpath, err)) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	privkey, err := keypairs.ParseJWKPrivateKey(jwkb) | ||||||
|  | 	if nil != err { | ||||||
|  | 		// TODO delete the bad file? | ||||||
|  | 		panic(fmt.Errorf("unmarshal jwk %v: %w", string(jwkb), err)) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
| 	if nil != urlFlag && "" != *urlFlag { | 	if nil != urlFlag && "" != *urlFlag { | ||||||
| 		host = *urlFlag | 		host = *urlFlag | ||||||
| 	} else { | 	} else { | ||||||
| @ -64,15 +67,15 @@ func main() { | |||||||
| 	} else { | 	} else { | ||||||
| 		jwksPrefix = "public-jwks" | 		jwksPrefix = "public-jwks" | ||||||
| 	} | 	} | ||||||
| 	err := os.MkdirAll(jwksPrefix, 0755) | 	err = os.MkdirAll(jwksPrefix, 0755) | ||||||
| 	if nil != err { | 	if nil != err { | ||||||
| 		fmt.Fprintf(os.Stderr, "couldn't write %q: %s", jwksPrefix, err) | 		fmt.Fprintf(os.Stderr, "couldn't write %q: %s", jwksPrefix, err) | ||||||
| 		os.Exit(1) | 		os.Exit(1) | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	mockid.Route(jwksPrefix, priv, jwk) | 	mux := mockid.Route(jwksPrefix, privkey) | ||||||
| 
 | 
 | ||||||
| 	fs := http.FileServer(http.Dir("public")) | 	fs := http.FileServer(http.Dir("./public")) | ||||||
| 	http.Handle("/", fs) | 	http.Handle("/", fs) | ||||||
| 	/* | 	/* | ||||||
| 		http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { | 		http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { | ||||||
| @ -83,15 +86,16 @@ func main() { | |||||||
| 
 | 
 | ||||||
| 	fmt.Printf("Serving on port %d\n", port) | 	fmt.Printf("Serving on port %d\n", port) | ||||||
| 	go func() { | 	go func() { | ||||||
| 		log.Fatal(http.ListenAndServe(":"+strconv.Itoa(port), nil)) | 		log.Fatal(http.ListenAndServe(":"+strconv.Itoa(port), mux)) | ||||||
| 		done <- true | 		done <- true | ||||||
| 	}() | 	}() | ||||||
| 
 | 
 | ||||||
| 	b, _ := json.Marshal(jwk) | 	// TODO privB := keypairs.MarshalJWKPrivateKey(privkey) | ||||||
| 	fmt.Printf("Private Key:\n\t%s\n", string(b)) | 	privB := keypairs.MarshalJWKPrivateKey(privkey) | ||||||
| 	b, _ = json.Marshal(jwk.PublicJWK) | 	fmt.Printf("Private Key:\n\t%s\n", string(privB)) | ||||||
| 	fmt.Printf("Public Key:\n\t%s\n", string(b)) | 	pubB := keypairs.MarshalJWKPublicKey(keypairs.NewPublicKey(privkey.Public())) | ||||||
| 	protected, payload, token := mockid.GenToken(host, priv, url.Values{}) | 	fmt.Printf("Public Key:\n\t%s\n", string(pubB)) | ||||||
|  | 	protected, payload, token := mockid.GenToken(host, privkey, url.Values{}) | ||||||
| 	fmt.Printf("Protected (Header):\n\t%s\n", protected) | 	fmt.Printf("Protected (Header):\n\t%s\n", protected) | ||||||
| 	fmt.Printf("Payload (Claims):\n\t%s\n", payload) | 	fmt.Printf("Payload (Claims):\n\t%s\n", payload) | ||||||
| 	fmt.Printf("Access Token:\n\t%s\n", token) | 	fmt.Printf("Access Token:\n\t%s\n", token) | ||||||
|  | |||||||
							
								
								
									
										71
									
								
								mockid/api/common.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										71
									
								
								mockid/api/common.go
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,71 @@ | |||||||
|  | package api | ||||||
|  | 
 | ||||||
|  | import ( | ||||||
|  | 	"bytes" | ||||||
|  | 	"crypto/sha256" | ||||||
|  | 	"encoding/binary" | ||||||
|  | 	"encoding/json" | ||||||
|  | 	"errors" | ||||||
|  | 	"io" | ||||||
|  | 	"log" | ||||||
|  | 	"math/rand" | ||||||
|  | 	mathrand "math/rand" | ||||||
|  | 	"net/http" | ||||||
|  | 
 | ||||||
|  | 	"git.coolaj86.com/coolaj86/go-mockid/xkeypairs" | ||||||
|  | 	"git.rootprojects.org/root/keypairs" | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | /* | ||||||
|  | func getJWS(r *http.Request) (*xkeypairs.KeyOptions, error) { | ||||||
|  | 
 | ||||||
|  | } | ||||||
|  | */ | ||||||
|  | 
 | ||||||
|  | func getOpts(r *http.Request) (*xkeypairs.KeyOptions, error) { | ||||||
|  | 	tok := make(map[string]interface{}) | ||||||
|  | 	decoder := json.NewDecoder(r.Body) | ||||||
|  | 	err := decoder.Decode(&tok) | ||||||
|  | 	if nil != err && io.EOF != err { | ||||||
|  | 		log.Printf("json decode error: %s", err) | ||||||
|  | 		return nil, errors.New("Bad Request: invalid json body") | ||||||
|  | 	} | ||||||
|  | 	defer r.Body.Close() | ||||||
|  | 
 | ||||||
|  | 	var seed int64 | ||||||
|  | 	seedStr, _ := tok["seed"].(string) | ||||||
|  | 	if "" != seedStr { | ||||||
|  | 		if len(seedStr) > 256 { | ||||||
|  | 			return nil, errors.New("Bad Request: base64 seed should be <256 characters (and is truncated to 64-bits anyway)") | ||||||
|  | 		} | ||||||
|  | 		b := sha256.Sum256([]byte(seedStr)) | ||||||
|  | 		seed, _ = binary.ReadVarint(bytes.NewReader(b[0:8])) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	key, _ := tok["key"].(string) | ||||||
|  | 	opts := &xkeypairs.KeyOptions{ | ||||||
|  | 		Seed: seed, | ||||||
|  | 		Key:  key, | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	opts.Claims, _ = tok["claims"].(keypairs.Object) | ||||||
|  | 	opts.Header, _ = tok["header"].(keypairs.Object) | ||||||
|  | 
 | ||||||
|  | 	var n int | ||||||
|  | 	if 0 != seed { | ||||||
|  | 		n = opts.MyFooNextReader().(*mathrand.Rand).Intn(2) | ||||||
|  | 	} else { | ||||||
|  | 		n = rand.Intn(2) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	opts.KeyType, _ = tok["kty"].(string) | ||||||
|  | 	if "" == opts.KeyType { | ||||||
|  | 		if 0 == n { | ||||||
|  | 			opts.KeyType = "RSA" | ||||||
|  | 		} else { | ||||||
|  | 			opts.KeyType = "EC" | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	return opts, nil | ||||||
|  | } | ||||||
							
								
								
									
										145
									
								
								mockid/api/generate.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										145
									
								
								mockid/api/generate.go
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,145 @@ | |||||||
|  | package api | ||||||
|  | 
 | ||||||
|  | import ( | ||||||
|  | 	"net/http" | ||||||
|  | 
 | ||||||
|  | 	"git.coolaj86.com/coolaj86/go-mockid/xkeypairs" | ||||||
|  | 	"git.rootprojects.org/root/keypairs" | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | // GeneratePublicJWK will create a new private key in JWK format | ||||||
|  | func GeneratePublicJWK(w http.ResponseWriter, r *http.Request) { | ||||||
|  | 	if "POST" != r.Method { | ||||||
|  | 		http.Error(w, "Method Not Allowed", http.StatusMethodNotAllowed) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	opts, err := getOpts(r) | ||||||
|  | 	if nil != err { | ||||||
|  | 		http.Error(w, err.Error(), http.StatusBadRequest) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	privkey, err := getPrivKey(opts) | ||||||
|  | 	if nil != err { | ||||||
|  | 		http.Error(w, err.Error(), http.StatusBadRequest) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	jwk := keypairs.MarshalJWKPublicKey(keypairs.NewPublicKey(privkey.Public())) | ||||||
|  | 	w.Write(append(jwk, '\n')) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // GeneratePrivateJWK will create a new private key in JWK format | ||||||
|  | func GeneratePrivateJWK(w http.ResponseWriter, r *http.Request) { | ||||||
|  | 	if "POST" != r.Method { | ||||||
|  | 		http.Error(w, "Method Not Allowed", http.StatusMethodNotAllowed) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	opts, err := getOpts(r) | ||||||
|  | 	if nil != err { | ||||||
|  | 		http.Error(w, err.Error(), http.StatusBadRequest) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	privkey := xkeypairs.GenPrivKey(opts) | ||||||
|  | 
 | ||||||
|  | 	jwk := keypairs.MarshalJWKPrivateKey(privkey) | ||||||
|  | 	w.Write(append(jwk, '\n')) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // GeneratePublicDER will create a new private key in JWK format | ||||||
|  | func GeneratePublicDER(w http.ResponseWriter, r *http.Request) { | ||||||
|  | 	if "POST" != r.Method { | ||||||
|  | 		http.Error(w, "Method Not Allowed", http.StatusMethodNotAllowed) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	opts, err := getOpts(r) | ||||||
|  | 	if nil != err { | ||||||
|  | 		http.Error(w, err.Error(), http.StatusBadRequest) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	privkey, err := getPrivKey(opts) | ||||||
|  | 	if nil != err { | ||||||
|  | 		http.Error(w, err.Error(), http.StatusBadRequest) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	b, _ := keypairs.MarshalDERPublicKey(privkey.Public()) | ||||||
|  | 
 | ||||||
|  | 	w.Write(b) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // GeneratePrivateDER will create a new private key in a valid DER encoding | ||||||
|  | func GeneratePrivateDER(w http.ResponseWriter, r *http.Request) { | ||||||
|  | 	if "POST" != r.Method { | ||||||
|  | 		http.Error(w, "Method Not Allowed", http.StatusMethodNotAllowed) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	opts, err := getOpts(r) | ||||||
|  | 	if nil != err { | ||||||
|  | 		http.Error(w, err.Error(), http.StatusBadRequest) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	privkey := xkeypairs.GenPrivKey(opts) | ||||||
|  | 
 | ||||||
|  | 	der, _ := keypairs.MarshalDERPrivateKey(privkey) | ||||||
|  | 	w.Write(der) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // GeneratePublicPEM will create a new private key in JWK format | ||||||
|  | func GeneratePublicPEM(w http.ResponseWriter, r *http.Request) { | ||||||
|  | 	if "POST" != r.Method { | ||||||
|  | 		http.Error(w, "Method Not Allowed", http.StatusMethodNotAllowed) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	opts, err := getOpts(r) | ||||||
|  | 	if nil != err { | ||||||
|  | 		http.Error(w, err.Error(), http.StatusBadRequest) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	privkey, err := getPrivKey(opts) | ||||||
|  | 	if nil != err { | ||||||
|  | 		http.Error(w, err.Error(), http.StatusBadRequest) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	b, _ := keypairs.MarshalPEMPublicKey(privkey.Public()) | ||||||
|  | 
 | ||||||
|  | 	w.Write(b) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // GeneratePrivatePEM will create a new private key in a valid PEM encoding | ||||||
|  | func GeneratePrivatePEM(w http.ResponseWriter, r *http.Request) { | ||||||
|  | 	if "POST" != r.Method { | ||||||
|  | 		http.Error(w, "Method Not Allowed", http.StatusMethodNotAllowed) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	opts, err := getOpts(r) | ||||||
|  | 	if nil != err { | ||||||
|  | 		http.Error(w, err.Error(), http.StatusBadRequest) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	privkey := xkeypairs.GenPrivKey(opts) | ||||||
|  | 
 | ||||||
|  | 	privpem, _ := keypairs.MarshalPEMPrivateKey(privkey) | ||||||
|  | 	w.Write(privpem) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | const maxRetry = 16 | ||||||
|  | 
 | ||||||
|  | func getPrivKey(opts *xkeypairs.KeyOptions) (keypairs.PrivateKey, error) { | ||||||
|  | 	if "" != opts.Key { | ||||||
|  | 		return keypairs.ParsePrivateKey([]byte(opts.Key)) | ||||||
|  | 	} | ||||||
|  | 	return xkeypairs.GenPrivKey(opts), nil | ||||||
|  | } | ||||||
							
								
								
									
										57
									
								
								mockid/api/sign.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										57
									
								
								mockid/api/sign.go
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,57 @@ | |||||||
|  | package api | ||||||
|  | 
 | ||||||
|  | import ( | ||||||
|  | 	"encoding/json" | ||||||
|  | 	"net/http" | ||||||
|  | 
 | ||||||
|  | 	"git.rootprojects.org/root/keypairs" | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | // SignJWS will create an uncompressed JWT with the given payload | ||||||
|  | func SignJWS(w http.ResponseWriter, r *http.Request) { | ||||||
|  | 	sign(w, r, false) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // SignJWT will create an compressed JWS (JWT) with the given payload | ||||||
|  | func SignJWT(w http.ResponseWriter, r *http.Request) { | ||||||
|  | 	sign(w, r, true) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func sign(w http.ResponseWriter, r *http.Request, jwt bool) { | ||||||
|  | 	if "POST" != r.Method { | ||||||
|  | 		http.Error(w, "Method Not Allowed", http.StatusMethodNotAllowed) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	opts, err := getOpts(r) | ||||||
|  | 	if nil != err { | ||||||
|  | 		http.Error(w, err.Error(), http.StatusBadRequest) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	privkey, err := getPrivKey(opts) | ||||||
|  | 	if nil != err { | ||||||
|  | 		http.Error(w, err.Error(), http.StatusBadRequest) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	header := opts.Header | ||||||
|  | 	if 0 != opts.Seed { | ||||||
|  | 		header["_seed"] = opts.Seed | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	jws, err := keypairs.SignClaims(privkey, header, opts.Claims) | ||||||
|  | 	if nil != err { | ||||||
|  | 		http.Error(w, err.Error(), http.StatusBadRequest) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	var b []byte | ||||||
|  | 	if jwt { | ||||||
|  | 		s := keypairs.JWSToJWT(jws) | ||||||
|  | 		w.Write(append([]byte(s), '\n')) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 	b, _ = json.Marshal(jws) | ||||||
|  | 	w.Write(append(b, '\n')) | ||||||
|  | } | ||||||
							
								
								
									
										87
									
								
								mockid/api/verify.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										87
									
								
								mockid/api/verify.go
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,87 @@ | |||||||
|  | package api | ||||||
|  | 
 | ||||||
|  | import ( | ||||||
|  | 	"encoding/base64" | ||||||
|  | 	"encoding/json" | ||||||
|  | 	"io" | ||||||
|  | 	"log" | ||||||
|  | 	"net/http" | ||||||
|  | 	"strings" | ||||||
|  | 	"time" | ||||||
|  | 
 | ||||||
|  | 	"git.rootprojects.org/root/keypairs" | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | // Verify will verify both JWT and uncompressed JWS | ||||||
|  | func Verify(w http.ResponseWriter, r *http.Request) { | ||||||
|  | 	if "POST" != r.Method { | ||||||
|  | 		http.Error(w, "Method Not Allowed", http.StatusMethodNotAllowed) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	jws := &keypairs.JWS{} | ||||||
|  | 
 | ||||||
|  | 	authzParts := strings.Split(r.Header.Get("Authorization"), " ") | ||||||
|  | 	lenAuthz := len(authzParts) | ||||||
|  | 	if 2 == lenAuthz { | ||||||
|  | 		jwt := authzParts[1] | ||||||
|  | 		jwsParts := strings.Split(jwt, ".") | ||||||
|  | 		if 3 == len(jwsParts) { | ||||||
|  | 			jws.Protected = jwsParts[0] | ||||||
|  | 			jws.Payload = jwsParts[1] | ||||||
|  | 			jws.Signature = jwsParts[2] | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	if nil == jws { | ||||||
|  | 		if 0 != lenAuthz { | ||||||
|  | 			http.Error(w, "Bad Request: malformed Authorization header", http.StatusBadRequest) | ||||||
|  | 			return | ||||||
|  | 		} | ||||||
|  | 		decoder := json.NewDecoder(r.Body) | ||||||
|  | 		err := decoder.Decode(jws) | ||||||
|  | 		if nil != err && io.EOF != err { | ||||||
|  | 			log.Printf("json decode error: %s", err) | ||||||
|  | 			http.Error(w, "Bad Request: invalid JWS body", http.StatusBadRequest) | ||||||
|  | 			return | ||||||
|  | 		} | ||||||
|  | 		defer r.Body.Close() | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	protected, err := base64.RawURLEncoding.DecodeString(jws.Protected) | ||||||
|  | 	if nil != err { | ||||||
|  | 		http.Error(w, "Bad Request: invalid JWS header base64Url encoding", http.StatusBadRequest) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 	if err := json.Unmarshal([]byte(protected), &jws.Header); nil != err { | ||||||
|  | 		log.Printf("json decode header error: %s", err) | ||||||
|  | 		http.Error(w, "Bad Request: invalid JWS header", http.StatusBadRequest) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	payload, err := base64.RawURLEncoding.DecodeString(jws.Payload) | ||||||
|  | 	if nil != err { | ||||||
|  | 		http.Error(w, "Bad Request: invalid JWS payload base64Url encoding", http.StatusBadRequest) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 	if err := json.Unmarshal([]byte(payload), &jws.Claims); nil != err { | ||||||
|  | 		log.Printf("json decode claims error: %s", err) | ||||||
|  | 		http.Error(w, "Bad Request: invalid JWS claims", http.StatusBadRequest) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	if "false" == r.URL.Query().Get("exp") { | ||||||
|  | 		//expf64, _ := jws.Claims["exp"].(float64) | ||||||
|  | 		jws.Claims["exp"] = float64(time.Now().Add(5 * time.Minute).Unix()) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	errs := keypairs.VerifyClaims(nil, jws) | ||||||
|  | 	if 0 == len(errs) { | ||||||
|  | 		log.Printf("jws verify error: %s", errs) | ||||||
|  | 		http.Error(w, "Bad Request: could not verify JWS claims", http.StatusBadRequest) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	b := []byte(`{"success":true}`) | ||||||
|  | 	w.Write(append(b, '\n')) | ||||||
|  | } | ||||||
							
								
								
									
										101
									
								
								mockid/hashcash.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										101
									
								
								mockid/hashcash.go
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,101 @@ | |||||||
|  | package mockid | ||||||
|  | 
 | ||||||
|  | import ( | ||||||
|  | 	"errors" | ||||||
|  | 	"net/http" | ||||||
|  | 	"sync" | ||||||
|  | 	"time" | ||||||
|  | 
 | ||||||
|  | 	"git.rootprojects.org/root/hashcash" | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | var hashcashes = &hashcashDB{ | ||||||
|  | 	db: sync.Map{}, | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func NewHashcash(sub string, exp time.Time) *hashcash.Hashcash { | ||||||
|  | 
 | ||||||
|  | 	h := hashcash.New(hashcash.Hashcash{ | ||||||
|  | 		Subject:   sub, | ||||||
|  | 		ExpiresAt: exp, | ||||||
|  | 	}) | ||||||
|  | 
 | ||||||
|  | 	// ignoring the error because this implementation is backed by an in-memory map | ||||||
|  | 	_ = hashcashes.Store(h.Nonce, h) | ||||||
|  | 
 | ||||||
|  | 	return h | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | var ErrNotFound = errors.New("not found") | ||||||
|  | 
 | ||||||
|  | func UseHashcash(hc, sub string) error { | ||||||
|  | 	phony, err := hashcash.Parse(hc) | ||||||
|  | 	if nil != err { | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	hi, ok, _ := hashcashes.Load(phony.Nonce) | ||||||
|  | 	if !ok { | ||||||
|  | 		return ErrNotFound | ||||||
|  | 	} | ||||||
|  | 	mccoy := hi.(*hashcash.Hashcash) | ||||||
|  | 	mccopy := *mccoy | ||||||
|  | 
 | ||||||
|  | 	mccopy.Solution = phony.Solution | ||||||
|  | 	if err := mccopy.Verify(sub); nil != err { | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	_ = hashcashes.Delete(mccoy.Nonce) | ||||||
|  | 	return nil | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func issueHashcash(w http.ResponseWriter, r *http.Request) *hashcash.Hashcash { | ||||||
|  | 	h := NewHashcash(r.Host, time.Now().Add(5*time.Minute)) | ||||||
|  | 	w.Header().Set("Hashcash-Challenge", h.String()) | ||||||
|  | 	return h | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func requireHashcash(next http.HandlerFunc) http.HandlerFunc { | ||||||
|  | 	return func(w http.ResponseWriter, r *http.Request) { | ||||||
|  | 		hc := r.Header.Get("Hashcash") | ||||||
|  | 		_ = issueHashcash(w, r) | ||||||
|  | 		if err := UseHashcash(hc, r.Host); nil != err { | ||||||
|  | 			http.Error(w, "Bad Request", http.StatusBadRequest) | ||||||
|  | 			return | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		next(w, r) | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | type hashcashDB struct { | ||||||
|  | 	db sync.Map | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (h *hashcashDB) Load(key interface{}) (value interface{}, ok bool, err error) { | ||||||
|  | 	v, ok := h.db.Load(key) | ||||||
|  | 	return v, ok, nil | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (h *hashcashDB) Store(key interface{}, value interface{}) (err error) { | ||||||
|  | 	h.db.Store(key, value) | ||||||
|  | 	return nil | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (h *hashcashDB) Delete(key interface{}) (err error) { | ||||||
|  | 	h.db.Delete(key) | ||||||
|  | 	return nil | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (h *hashcashDB) vacuum() (err error) { | ||||||
|  | 	now := time.Now().UTC() | ||||||
|  | 	h.db.Range(func(key interface{}, val interface{}) bool { | ||||||
|  | 		v := val.(*hashcash.Hashcash) | ||||||
|  | 		if v.ExpiresAt.Sub(now) < 0 { | ||||||
|  | 			h.db.Delete(key) | ||||||
|  | 		} | ||||||
|  | 		return true | ||||||
|  | 	}) | ||||||
|  | 	return nil | ||||||
|  | } | ||||||
							
								
								
									
										46
									
								
								mockid/mailgun.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										46
									
								
								mockid/mailgun.go
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,46 @@ | |||||||
|  | package mockid | ||||||
|  | 
 | ||||||
|  | import ( | ||||||
|  | 	"context" | ||||||
|  | 	"os" | ||||||
|  | 	"time" | ||||||
|  | 
 | ||||||
|  | 	mailgun "github.com/mailgun/mailgun-go/v3" | ||||||
|  | 
 | ||||||
|  | 	_ "github.com/joho/godotenv/autoload" | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | var ( | ||||||
|  | 	mgDomain string | ||||||
|  | 	mgAPIKey string | ||||||
|  | 	mgFrom   string | ||||||
|  | 	mg       *mailgun.MailgunImpl | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | func init() { | ||||||
|  | 	/* | ||||||
|  | 	  MAILGUN_API_KEY=key-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx | ||||||
|  | 	  MAILGUN_DOMAIN=mail.example.com | ||||||
|  | 	  MAILER_FROM="Rob the Robot <rob.the.robot@mail.example.com>" | ||||||
|  | 	*/ | ||||||
|  | 
 | ||||||
|  | 	mgDomain = os.Getenv("MAILGUN_DOMAIN") | ||||||
|  | 	mgAPIKey = os.Getenv("MAILGUN_API_KEY") | ||||||
|  | 	mgFrom = os.Getenv("MAILER_FROM") | ||||||
|  | 
 | ||||||
|  | 	mg = mailgun.NewMailgun(mgDomain, mgAPIKey) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func SendSimpleMessage(to, from, subject, text, replyTo string) (string, error) { | ||||||
|  | 	m := mg.NewMessage(from, subject, text, to) | ||||||
|  | 	if 0 != len(replyTo) { | ||||||
|  | 		// mailgun's required "h:" prefix is added by the library | ||||||
|  | 		m.AddHeader("Reply-To", replyTo) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	ctx, cancel := context.WithTimeout(context.Background(), time.Second*30) | ||||||
|  | 	defer cancel() | ||||||
|  | 
 | ||||||
|  | 	_, id, err := mg.Send(ctx, m) | ||||||
|  | 	return id, err | ||||||
|  | } | ||||||
							
								
								
									
										508
									
								
								mockid/mockid.go
									
									
									
									
									
								
							
							
						
						
									
										508
									
								
								mockid/mockid.go
									
									
									
									
									
								
							| @ -1,29 +1,27 @@ | |||||||
| package mockid | package mockid | ||||||
| 
 | 
 | ||||||
| import ( | import ( | ||||||
|  | 	"crypto" | ||||||
| 	"crypto/ecdsa" | 	"crypto/ecdsa" | ||||||
| 	"crypto/elliptic" |  | ||||||
| 	"crypto/rand" | 	"crypto/rand" | ||||||
|  | 	"crypto/rsa" | ||||||
| 	"crypto/sha256" | 	"crypto/sha256" | ||||||
| 	"crypto/sha512" |  | ||||||
| 	"encoding/base64" | 	"encoding/base64" | ||||||
| 	"encoding/json" | 	"encoding/json" | ||||||
| 	"fmt" | 	"fmt" | ||||||
| 	"io/ioutil" | 	"io" | ||||||
| 	"log" |  | ||||||
| 	"math/big" | 	"math/big" | ||||||
| 	"net/http" |  | ||||||
| 	"net/url" | 	"net/url" | ||||||
| 	"path/filepath" | 	"os" | ||||||
| 	"strconv" | 	"strconv" | ||||||
| 	"strings" |  | ||||||
| 	"time" | 	"time" | ||||||
|  | 
 | ||||||
|  | 	"git.rootprojects.org/root/keypairs" | ||||||
|  | 	//jwt "github.com/dgrijalva/jwt-go" | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| type PrivateJWK struct { | // TestMain will overwrite this | ||||||
| 	PublicJWK | var rndsrc io.Reader = rand.Reader | ||||||
| 	D string `json:"d"` |  | ||||||
| } |  | ||||||
| 
 | 
 | ||||||
| type PublicJWK struct { | type PublicJWK struct { | ||||||
| 	Crv   string `json:"crv"` | 	Crv   string `json:"crv"` | ||||||
| @ -33,332 +31,79 @@ type PublicJWK struct { | |||||||
| 	Y     string `json:"y"` | 	Y     string `json:"y"` | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| var nonces map[string]int64 | type KVDB interface { | ||||||
| 
 | 	Load(key interface{}) (value interface{}, ok bool, err error) | ||||||
| func init() { | 	Store(key interface{}, value interface{}) (err error) | ||||||
| 	nonces = make(map[string]int64) | 	Delete(key interface{}) (err error) | ||||||
|  | 	Vacuum() (err error) | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func Route(jwksPrefix string, priv *ecdsa.PrivateKey, jwk *PrivateJWK) { | type InspectableToken struct { | ||||||
| 	pub := &priv.PublicKey | 	Public    keypairs.PublicKey     `json:"jwk"` | ||||||
| 	thumbprint := thumbprintKey(pub) | 	Protected map[string]interface{} `json:"protected"` | ||||||
|  | 	Payload   map[string]interface{} `json:"payload"` | ||||||
|  | 	Signature string                 `json:"signature"` | ||||||
|  | 	Verified  bool                   `json:"verified"` | ||||||
|  | 	Errors    []string               `json:"errors"` | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | 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 | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | var defaultFrom string | ||||||
|  | var defaultReplyTo string | ||||||
|  | 
 | ||||||
|  | var salt []byte | ||||||
|  | 
 | ||||||
|  | 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) | ||||||
| 
 | 
 | ||||||
| 	http.HandleFunc("/api/new-nonce", func(w http.ResponseWriter, r *http.Request) { |  | ||||||
| 		baseURL := getBaseURL(r) |  | ||||||
| 	/* | 	/* | ||||||
| 			res.statusCode = 200; | 		  go func() { | ||||||
| 			res.setHeader("Cache-Control", "max-age=0, no-cache, no-store"); | 		    for { | ||||||
| 			// TODO | 		      nonce := <- nonCh | ||||||
| 			//res.setHeader("Date", "Sun, 10 Mar 2019 08:04:45 GMT"); | 			    nonces[nonce] = time.Now().Unix() | ||||||
| 			// is this the expiration of the nonce itself? methinks maybe so | 		    } | ||||||
| 			//res.setHeader("Expires", "Sun, 10 Mar 2019 08:04:45 GMT"); | 		  }() | ||||||
| 			// TODO use one of the registered domains |  | ||||||
| 			//var indexUrl = "https://acme-staging-v02.api.letsencrypt.org/index" |  | ||||||
| 	*/ | 	*/ | ||||||
| 		//var port = (state.config.ipc && state.config.ipc.port || state._ipc.port || undefined); |  | ||||||
| 		//var indexUrl = "http://localhost:" + port + "/index"; |  | ||||||
| 		indexUrl := baseURL + "/index" |  | ||||||
| 		w.Header().Set("Link", "<"+indexUrl+">;rel=\"index\"") |  | ||||||
| 		w.Header().Set("Cache-Control", "max-age=0, no-cache, no-store") |  | ||||||
| 		w.Header().Set("Pragma", "no-cache") |  | ||||||
| 		//res.setHeader("Strict-Transport-Security", "max-age=604800"); |  | ||||||
| 
 | 
 | ||||||
| 		w.Header().Set("X-Frame-Options", "DENY") | 	go func() { | ||||||
| 		issueNonce(w, r) | 		for { | ||||||
| 	}) | 			time.Sleep(15 * time.Second) | ||||||
| 
 | 			hashcashes.vacuum() | ||||||
| 	http.HandleFunc("/api/new-account", requireNonce(func(w http.ResponseWriter, r *http.Request) { | 		} | ||||||
| 		http.Error(w, "Not Implemented", http.StatusNotImplemented) | 	}() | ||||||
| 	})) |  | ||||||
| 
 |  | ||||||
| 	http.HandleFunc("/api/jwks", func(w http.ResponseWriter, r *http.Request) { |  | ||||||
| 		log.Printf("%s %s %s", r.Method, r.Host, r.URL.Path) |  | ||||||
| 		if "POST" != r.Method { |  | ||||||
| 			http.Error(w, "Method Not Allowed", http.StatusMethodNotAllowed) |  | ||||||
| 			return |  | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| 		tok := make(map[string]interface{}) | func GenToken(host string, privkey keypairs.PrivateKey, query url.Values) (string, string, string) { | ||||||
| 		decoder := json.NewDecoder(r.Body) | 	thumbprint := keypairs.ThumbprintPublicKey(keypairs.NewPublicKey(privkey.Public())) | ||||||
| 		err := decoder.Decode(&tok) | 	// TODO keypairs.Alg(key) | ||||||
| 		if nil != err { | 	alg := "ES256" | ||||||
| 			http.Error(w, "Bad Request: invalid json", http.StatusBadRequest) | 	switch privkey.(type) { | ||||||
| 			return | 	case *rsa.PrivateKey: | ||||||
|  | 		alg = "RS256" | ||||||
| 	} | 	} | ||||||
| 		defer r.Body.Close() | 	protected := fmt.Sprintf(`{"typ":"JWT","alg":%q,"kid":"%s"}`, alg, thumbprint) | ||||||
| 
 |  | ||||||
| 		// TODO better, JSON error messages |  | ||||||
| 		if _, ok := tok["d"]; ok { |  | ||||||
| 			http.Error(w, "Bad Request: private key", http.StatusBadRequest) |  | ||||||
| 			return |  | ||||||
| 		} |  | ||||||
| 
 |  | ||||||
| 		kty, _ := tok["kty"].(string) |  | ||||||
| 		switch kty { |  | ||||||
| 		case "EC": |  | ||||||
| 			postEC(jwksPrefix, tok, w, r) |  | ||||||
| 		case "RSA": |  | ||||||
| 			postRSA(jwksPrefix, tok, w, r) |  | ||||||
| 		default: |  | ||||||
| 			http.Error(w, "Bad Request: only EC and RSA keys are supported", http.StatusBadRequest) |  | ||||||
| 			return |  | ||||||
| 		} |  | ||||||
| 	}) |  | ||||||
| 
 |  | ||||||
| 	http.HandleFunc("/access_token", func(w http.ResponseWriter, r *http.Request) { |  | ||||||
| 		log.Printf("%s %s\n", r.Method, r.URL.Path) |  | ||||||
| 		_, _, token := GenToken(getBaseURL(r), priv, r.URL.Query()) |  | ||||||
| 		fmt.Fprintf(w, token) |  | ||||||
| 	}) |  | ||||||
| 
 |  | ||||||
| 	http.HandleFunc("/authorization_header", func(w http.ResponseWriter, r *http.Request) { |  | ||||||
| 		log.Printf("%s %s\n", r.Method, r.URL.Path) |  | ||||||
| 
 |  | ||||||
| 		var header string |  | ||||||
| 		headers, _ := r.URL.Query()["header"] |  | ||||||
| 		if 0 == len(headers) { |  | ||||||
| 			header = "Authorization" |  | ||||||
| 		} else { |  | ||||||
| 			header = headers[0] |  | ||||||
| 		} |  | ||||||
| 
 |  | ||||||
| 		var prefix string |  | ||||||
| 		prefixes, _ := r.URL.Query()["prefix"] |  | ||||||
| 		if 0 == len(prefixes) { |  | ||||||
| 			prefix = "Bearer " |  | ||||||
| 		} else { |  | ||||||
| 			prefix = prefixes[0] |  | ||||||
| 		} |  | ||||||
| 
 |  | ||||||
| 		_, _, token := GenToken(getBaseURL(r), priv, r.URL.Query()) |  | ||||||
| 		fmt.Fprintf(w, "%s: %s%s", header, prefix, token) |  | ||||||
| 	}) |  | ||||||
| 
 |  | ||||||
| 	http.HandleFunc("/key.jwk.json", func(w http.ResponseWriter, r *http.Request) { |  | ||||||
| 		log.Printf("%s %s", r.Method, r.URL.Path) |  | ||||||
| 		fmt.Fprintf(w, `{ "kty": "EC" , "crv": %q , "d": %q , "x": %q , "y": %q , "ext": true , "key_ops": ["sign"] }`, jwk.Crv, jwk.D, jwk.X, jwk.Y) |  | ||||||
| 	}) |  | ||||||
| 
 |  | ||||||
| 	http.HandleFunc("/.well-known/openid-configuration", func(w http.ResponseWriter, r *http.Request) { |  | ||||||
| 		baseURL := getBaseURL(r) |  | ||||||
| 		log.Printf("%s %s\n", r.Method, r.URL.Path) |  | ||||||
| 		fmt.Fprintf(w, `{ "issuer": "%s", "jwks_uri": "%s/.well-known/jwks.json" }`, baseURL, baseURL) |  | ||||||
| 	}) |  | ||||||
| 
 |  | ||||||
| 	http.HandleFunc("/.well-known/jwks.json", func(w http.ResponseWriter, r *http.Request) { |  | ||||||
| 		log.Printf("%s %s %s", r.Method, r.Host, r.URL.Path) |  | ||||||
| 		parts := strings.Split(r.Host, ".") |  | ||||||
| 		kid := parts[0] |  | ||||||
| 
 |  | ||||||
| 		b, err := ioutil.ReadFile(filepath.Join(jwksPrefix, strings.ToLower(kid)+".jwk.json")) |  | ||||||
| 		if nil != err { |  | ||||||
| 			//http.Error(w, "Not Found", http.StatusNotFound) |  | ||||||
| 			jwkstr := fmt.Sprintf( |  | ||||||
| 				`{ "keys": [ { "kty": "EC" , "crv": %q , "x": %q , "y": %q , "kid": %q , "ext": true , "key_ops": ["verify"] , "exp": %s } ] }`, |  | ||||||
| 				jwk.Crv, jwk.X, jwk.Y, thumbprint, strconv.FormatInt(time.Now().Add(15*time.Minute).Unix(), 10), |  | ||||||
| 			) |  | ||||||
| 			fmt.Println(jwkstr) |  | ||||||
| 			fmt.Fprintf(w, jwkstr) |  | ||||||
| 			return |  | ||||||
| 		} |  | ||||||
| 
 |  | ||||||
| 		tok := &PublicJWK{} |  | ||||||
| 		err = json.Unmarshal(b, tok) |  | ||||||
| 		if nil != err { |  | ||||||
| 			// TODO delete the bad file? |  | ||||||
| 			http.Error(w, "Internal Server Error", http.StatusInternalServerError) |  | ||||||
| 			return |  | ||||||
| 		} |  | ||||||
| 
 |  | ||||||
| 		jwkstr := fmt.Sprintf( |  | ||||||
| 			`{ "keys": [ { "kty": "EC", "crv": %q, "x": %q, "y": %q, "kid": %q,`+ |  | ||||||
| 				` "ext": true, "key_ops": ["verify"], "exp": %s } ] }`, |  | ||||||
| 			tok.Crv, tok.X, tok.Y, tok.KeyID, strconv.FormatInt(time.Now().Add(15*time.Minute).Unix(), 10), |  | ||||||
| 		) |  | ||||||
| 		fmt.Println(jwkstr) |  | ||||||
| 		fmt.Fprintf(w, jwkstr) |  | ||||||
| 	}) |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func parseExp(exp string) (int, error) { |  | ||||||
| 	if "" == exp { |  | ||||||
| 		exp = "15m" |  | ||||||
| 	} |  | ||||||
| 	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)) | 	protected64 := base64.RawURLEncoding.EncodeToString([]byte(protected)) | ||||||
| 
 | 
 | ||||||
| 	exp, err := parseExp(query.Get("exp")) | 	exp, err := time.ParseDuration(query.Get("exp")) | ||||||
| 	if nil != err { | 	if nil != err { | ||||||
| 		// cryptic error code | 		// cryptic error code | ||||||
| 		// TODO propagate error | 		// TODO propagate error | ||||||
| @ -367,12 +112,25 @@ func GenToken(host string, priv *ecdsa.PrivateKey, query url.Values) (string, st | |||||||
| 
 | 
 | ||||||
| 	payload := fmt.Sprintf( | 	payload := fmt.Sprintf( | ||||||
| 		`{"iss":"%s/","sub":"dummy","exp":%s}`, | 		`{"iss":"%s/","sub":"dummy","exp":%s}`, | ||||||
| 		host, strconv.FormatInt(time.Now().Add(time.Duration(exp)*time.Second).Unix(), 10), | 		host, strconv.FormatInt(time.Now().Add(exp*time.Second).Unix(), 10), | ||||||
| 	) | 	) | ||||||
| 	payload64 := base64.RawURLEncoding.EncodeToString([]byte(payload)) | 	payload64 := base64.RawURLEncoding.EncodeToString([]byte(payload)) | ||||||
| 
 | 
 | ||||||
| 	hash := sha256.Sum256([]byte(fmt.Sprintf(`%s.%s`, protected64, payload64))) | 	hash := sha256.Sum256([]byte(fmt.Sprintf(`%s.%s`, protected64, payload64))) | ||||||
| 	r, s, _ := ecdsa.Sign(rand.Reader, priv, hash[:]) | 	sig := JOSESign(privkey, hash[:]) | ||||||
|  | 	sig64 := base64.RawURLEncoding.EncodeToString(sig) | ||||||
|  | 	token := fmt.Sprintf("%s.%s.%s\n", 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() | 		rb := r.Bytes() | ||||||
| 		for len(rb) < 32 { | 		for len(rb) < 32 { | ||||||
| 			rb = append([]byte{0}, rb...) | 			rb = append([]byte{0}, rb...) | ||||||
| @ -381,80 +139,34 @@ func GenToken(host string, priv *ecdsa.PrivateKey, query url.Values) (string, st | |||||||
| 		for len(rb) < 32 { | 		for len(rb) < 32 { | ||||||
| 			sb = append([]byte{0}, sb...) | 			sb = append([]byte{0}, sb...) | ||||||
| 		} | 		} | ||||||
| 	sig64 := base64.RawURLEncoding.EncodeToString(append(rb, sb...)) | 		sig = append(rb, sb...) | ||||||
| 	token := fmt.Sprintf(`%s.%s.%s`, protected64, payload64, sig64) | 	} | ||||||
| 	return protected, payload, token | 	return sig | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func ParseKey(jwk *PrivateJWK) *ecdsa.PrivateKey { | // TODO: move to keypairs | ||||||
| 	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, |  | ||||||
| 	} |  | ||||||
| 
 | 
 | ||||||
| 	db, _ := base64.RawURLEncoding.DecodeString(jwk.D) | func JOSEVerify(pubkey keypairs.PublicKey, hash []byte, sig []byte) bool { | ||||||
| 	di := &big.Int{} |  | ||||||
| 	di.SetBytes(db) |  | ||||||
| 	priv := &ecdsa.PrivateKey{ |  | ||||||
| 		PublicKey: *pub, |  | ||||||
| 		D:         di, |  | ||||||
| 	} |  | ||||||
| 	return priv |  | ||||||
| } |  | ||||||
| 
 | 
 | ||||||
| func thumbprintKey(pub *ecdsa.PublicKey) string { | 	switch pub := pubkey.Key().(type) { | ||||||
| 	minpub := []byte(fmt.Sprintf(`{"crv":%q,"kty":"EC","x":%q,"y":%q}`, "P-256", pub.X, pub.Y)) | 	case *rsa.PublicKey: | ||||||
| 	sha := sha256.Sum256(minpub) | 		// TODO keypairs.Size(key) to detect key size ? | ||||||
| 	return base64.RawURLEncoding.EncodeToString(sha[:]) | 		//alg := "SHA256" | ||||||
|  | 		// TODO: this hasn't been tested yet | ||||||
|  | 		if err := rsa.VerifyPKCS1v15(pub, crypto.SHA256, hash, sig); nil != err { | ||||||
|  | 			return false | ||||||
| 		} | 		} | ||||||
| 
 | 		return true | ||||||
| func issueNonce(w http.ResponseWriter, r *http.Request) { | 	case *ecdsa.PublicKey: | ||||||
| 	b := make([]byte, 16) | 		r := &big.Int{} | ||||||
| 	_, _ = rand.Read(b) | 		r.SetBytes(sig[0:32]) | ||||||
| 	nonce := base64.RawURLEncoding.EncodeToString(b) | 		s := &big.Int{} | ||||||
| 	nonces[nonce] = time.Now().Unix() | 		s.SetBytes(sig[32:]) | ||||||
| 
 | 		fmt.Println("debug: sig len:", len(sig)) | ||||||
| 	w.Header().Set("Replay-Nonce", nonce) | 		fmt.Println("debug: r, s:", r, s) | ||||||
| } | 		return ecdsa.Verify(pub, hash, r, s) | ||||||
| 
 | 	default: | ||||||
| func requireNonce(next http.HandlerFunc) http.HandlerFunc { | 		panic("impossible condition: non-rsa/non-ecdsa key") | ||||||
| 	return func(w http.ResponseWriter, r *http.Request) { | 		return false | ||||||
| 		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 |  | ||||||
| 		} |  | ||||||
| 
 |  | ||||||
| 		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, |  | ||||||
| 	) |  | ||||||
| } |  | ||||||
|  | |||||||
| @ -1,12 +1,476 @@ | |||||||
| package mockid | package mockid | ||||||
| 
 | 
 | ||||||
| import ( | import ( | ||||||
|  | 	"bytes" | ||||||
|  | 	"crypto/ecdsa" | ||||||
|  | 	"crypto/elliptic" | ||||||
|  | 	"crypto/rsa" | ||||||
|  | 	"encoding/json" | ||||||
|  | 	"errors" | ||||||
|  | 	"fmt" | ||||||
|  | 	"io/ioutil" | ||||||
|  | 	"log" | ||||||
|  | 	mathrand "math/rand" | ||||||
|  | 	"net/http" | ||||||
|  | 	"net/http/httptest" | ||||||
|  | 	"net/url" | ||||||
|  | 	"os" | ||||||
| 	"testing" | 	"testing" | ||||||
| 	"time" | 
 | ||||||
|  | 	"git.coolaj86.com/coolaj86/go-mockid/xkeypairs" | ||||||
|  | 	"git.rootprojects.org/root/keypairs" | ||||||
| 	//keypairs "github.com/big-squid/go-keypairs" | 	//keypairs "github.com/big-squid/go-keypairs" | ||||||
| 	//"github.com/big-squid/go-keypairs/keyfetch/uncached" | 	//"github.com/big-squid/go-keypairs/keyfetch/uncached" | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| func TestTest(t *testing.T) { | var srv *httptest.Server | ||||||
| 	t.Fatal("no test") | 
 | ||||||
|  | type TestReader struct{} | ||||||
|  | 
 | ||||||
|  | func (TestReader) Read(p []byte) (n int, err error) { | ||||||
|  | 	return mathrand.Read(p) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | var testrnd = TestReader{} | ||||||
|  | 
 | ||||||
|  | func init() { | ||||||
|  | 	xkeypairs.RandomReader = testrnd | ||||||
|  | 	rndsrc = testrnd | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func TestMain(m *testing.M) { | ||||||
|  | 	mathrand.Seed(0) // Predictable results | ||||||
|  | 
 | ||||||
|  | 	os.Setenv("SALT", "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX") | ||||||
|  | 	jwksPrefix := "public-jwks" | ||||||
|  | 	err := os.MkdirAll(jwksPrefix, 0755) | ||||||
|  | 	if nil != err { | ||||||
|  | 		fmt.Fprintf(os.Stderr, "couldn't write %q: %s", jwksPrefix, err) | ||||||
|  | 		os.Exit(1) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	privkey, _ := ecdsa.GenerateKey(elliptic.P256(), rndsrc) | ||||||
|  | 	mux := Route(jwksPrefix, privkey) | ||||||
|  | 
 | ||||||
|  | 	srv = httptest.NewServer(mux) | ||||||
|  | 
 | ||||||
|  | 	//fs := http.FileServer(http.Dir("public")) | ||||||
|  | 	//http.Handle("/", fs) | ||||||
|  | 
 | ||||||
|  | 	os.Exit(m.Run()) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | //func TestSelfSignWithoutExp(t *testing.T) | ||||||
|  | //func TestSelfSignWithJTIWithoutExp(t *testing.T) | ||||||
|  | 
 | ||||||
|  | func TestVerifyExpired(t *testing.T) { | ||||||
|  | 	jwt := "eyJfc2VlZCI6LTEzMDY3NDU1MDQxNDQsImFsZyI6IlJTMjU2IiwiandrIjp7ImUiOiJBUUFCIiwia2lkIjoiSEZ4ZTlGV1dVc2N3bjltaVozSXNJeWMwMjMtbEJ1UmtvOEJpVV9IRG9KOCIsImt0eSI6IlJTQSIsIm4iOiJ2NUZkSTdYaC0wekxWVEVQZl94ekdIUVpDcEZ2MWR2N2h3eHhrVjctYmxpYmt6LXIxUG9lZ3lQYzFXMjZlWFBvd0xQQXQ3a3dHQnVOdjdMVjh5MEtvMkxOZklaXzRILW54SkJPaWIybXlHOVVfQ29WRDBiM3NBWTdmcDd2QlV1bTBXYVM4R3hZOGtYU0ZOS0VTY0NDNVBpSmFyblNISk1PcUdIVm51YmpsSjl5c1NyNmNsaGpxc0R4dU9qOHpxamF2MUFxek1STWVpRl9CREJsOUFoUGNZSHpHN0JtaXB5UEo2XzBwdWNLTi0tUDZDRk92d05SVGx2ek41RmlRM3VHcy1fMHcwQzVMZWJ6N21BNmJNTFdXc0tRRFBvb3cxallCWHJKdVF1WkZoSmxLMmdidm9ZcV85dWhfLUM1Z3pPZnR4UHBCNnhtY3RfelVaeUdwUUxnQlEiLCJ1c2UiOiJzaWcifSwidHlwIjoiSldUIn0.eyJleHAiOjE1OTY2MTQ3NTYsInN1YiI6ImJhbmFuYXMifQ.qHpzlglOfZMzE3CTNAUXld_wC62JTAJuoQfMaNeFa-XPtYB2Maj8_w3YmRZg_q5S6y9ToCmZ8nWd1kuMheA5qBKOUQeQH47Jts5zWLd0UBckIHo5lK4mk0bUWuiNgr7c9DY6k1DIdFaavyWCXbhFwG0X83qlMhQlPh02dDpCuU78Nn2hF3mZETQKpBIVESYtfeU1Xy3OU_am0kwcN2klLcdweOcrLx_ONfcvAGY3KiIdFiz0ViySAsQ39BiSSvoDYqOOOi41Hky67bnyZQOdalQC_95McTeXApzmGXRUE74Gj-S8c9e5it5d4QZLPaQ1JHzUKz1s7TPvThIn58NA-g" | ||||||
|  | 	client := srv.Client() | ||||||
|  | 	urlstr, _ := url.Parse(srv.URL + "/debug/verify") | ||||||
|  | 
 | ||||||
|  | 	req := &http.Request{ | ||||||
|  | 		Method: "POST", | ||||||
|  | 		URL:    urlstr, | ||||||
|  | 		Header: http.Header{}, | ||||||
|  | 	} | ||||||
|  | 	req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", jwt)) | ||||||
|  | 	res, err := client.Do(req) | ||||||
|  | 	if nil != err { | ||||||
|  | 		t.Error(err) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 	data, err := ioutil.ReadAll(res.Body) | ||||||
|  | 	if nil != err { | ||||||
|  | 		t.Error(err) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 	if 200 == res.StatusCode { | ||||||
|  | 		log.Printf(string(data)) | ||||||
|  | 		t.Error(fmt.Errorf("did not expect successful status code: %d", res.StatusCode)) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func TestVerifySelfSignedJWT(t *testing.T) { | ||||||
|  | 	jwt := "eyJfc2VlZCI6LTEzMDY3NDU1MDQxNDQsImFsZyI6IlJTMjU2IiwiandrIjp7ImUiOiJBUUFCIiwia2lkIjoiSEZ4ZTlGV1dVc2N3bjltaVozSXNJeWMwMjMtbEJ1UmtvOEJpVV9IRG9KOCIsImt0eSI6IlJTQSIsIm4iOiJ2NUZkSTdYaC0wekxWVEVQZl94ekdIUVpDcEZ2MWR2N2h3eHhrVjctYmxpYmt6LXIxUG9lZ3lQYzFXMjZlWFBvd0xQQXQ3a3dHQnVOdjdMVjh5MEtvMkxOZklaXzRILW54SkJPaWIybXlHOVVfQ29WRDBiM3NBWTdmcDd2QlV1bTBXYVM4R3hZOGtYU0ZOS0VTY0NDNVBpSmFyblNISk1PcUdIVm51YmpsSjl5c1NyNmNsaGpxc0R4dU9qOHpxamF2MUFxek1STWVpRl9CREJsOUFoUGNZSHpHN0JtaXB5UEo2XzBwdWNLTi0tUDZDRk92d05SVGx2ek41RmlRM3VHcy1fMHcwQzVMZWJ6N21BNmJNTFdXc0tRRFBvb3cxallCWHJKdVF1WkZoSmxLMmdidm9ZcV85dWhfLUM1Z3pPZnR4UHBCNnhtY3RfelVaeUdwUUxnQlEiLCJ1c2UiOiJzaWcifSwidHlwIjoiSldUIn0.eyJleHAiOjE1OTY2MTQ3NTYsInN1YiI6ImJhbmFuYXMifQ.qHpzlglOfZMzE3CTNAUXld_wC62JTAJuoQfMaNeFa-XPtYB2Maj8_w3YmRZg_q5S6y9ToCmZ8nWd1kuMheA5qBKOUQeQH47Jts5zWLd0UBckIHo5lK4mk0bUWuiNgr7c9DY6k1DIdFaavyWCXbhFwG0X83qlMhQlPh02dDpCuU78Nn2hF3mZETQKpBIVESYtfeU1Xy3OU_am0kwcN2klLcdweOcrLx_ONfcvAGY3KiIdFiz0ViySAsQ39BiSSvoDYqOOOi41Hky67bnyZQOdalQC_95McTeXApzmGXRUE74Gj-S8c9e5it5d4QZLPaQ1JHzUKz1s7TPvThIn58NA-g" | ||||||
|  | 	client := srv.Client() | ||||||
|  | 	urlstr, _ := url.Parse(srv.URL + "/debug/verify?exp=false") | ||||||
|  | 
 | ||||||
|  | 	req := &http.Request{ | ||||||
|  | 		Method: "POST", | ||||||
|  | 		URL:    urlstr, | ||||||
|  | 		//Body:   ioutil.NopCloser(bytes.NewReader(jws)), | ||||||
|  | 		Header: http.Header{}, | ||||||
|  | 	} | ||||||
|  | 	req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", jwt)) | ||||||
|  | 	res, err := client.Do(req) | ||||||
|  | 	if nil != err { | ||||||
|  | 		t.Error(err) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 	data, err := ioutil.ReadAll(res.Body) | ||||||
|  | 	if nil != err { | ||||||
|  | 		t.Error(err) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 	if 200 != res.StatusCode { | ||||||
|  | 		log.Printf(string(data)) | ||||||
|  | 		t.Error(fmt.Errorf("bad status code: %d", res.StatusCode)) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	log.Printf("TODO: verify, and verify non-self-signed") | ||||||
|  | 	log.Printf(string(data)) | ||||||
|  | 
 | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func TestSelfSign(t *testing.T) { | ||||||
|  | 	client := srv.Client() | ||||||
|  | 	//urlstr, _ := url.Parse(srv.URL + "/debug/jose.jws.json") | ||||||
|  | 	urlstr, _ := url.Parse(srv.URL + "/debug/jose.jws.jwt") | ||||||
|  | 
 | ||||||
|  | 	//fmt.Println("URL:", srv.URL, urlstr) | ||||||
|  | 	tokenRequest := []byte(`{"seed":"test","header":{"_jwk":true},"claims":{"sub":"bananas","exp":"10m"}}`) | ||||||
|  | 	res, err := client.Do(&http.Request{ | ||||||
|  | 		Method: "POST", | ||||||
|  | 		URL:    urlstr, | ||||||
|  | 		Body:   ioutil.NopCloser(bytes.NewReader(tokenRequest)), | ||||||
|  | 	}) | ||||||
|  | 	if nil != err { | ||||||
|  | 		t.Error(err) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 	if 200 != res.StatusCode { | ||||||
|  | 		t.Error(fmt.Errorf("bad status code: %d", res.StatusCode)) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	data, err := ioutil.ReadAll(res.Body) | ||||||
|  | 	if nil != err { | ||||||
|  | 		t.Error(err) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	log.Printf("TODO: verify, and verify non-self-signed") | ||||||
|  | 	log.Printf(string(data)) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func TestGenerateJWK(t *testing.T) { | ||||||
|  | 	client := srv.Client() | ||||||
|  | 	urlstr, _ := url.Parse(srv.URL + "/debug/private.jwk.json") | ||||||
|  | 	//fmt.Println("URL:", srv.URL, urlstr) | ||||||
|  | 	res, err := client.Do(&http.Request{ | ||||||
|  | 		Method: "POST", | ||||||
|  | 		URL:    urlstr, | ||||||
|  | 	}) | ||||||
|  | 	if nil != err { | ||||||
|  | 		//t.Fatal(err) | ||||||
|  | 		t.Error(err) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 	if 200 != res.StatusCode { | ||||||
|  | 		t.Error(fmt.Errorf("bad status code: %d", res.StatusCode)) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	data, err := ioutil.ReadAll(res.Body) | ||||||
|  | 	if nil != err { | ||||||
|  | 		//t.Fatal(err) | ||||||
|  | 		t.Error(err) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	jwk := map[string]string{} | ||||||
|  | 	err = json.Unmarshal(data, &jwk) | ||||||
|  | 	if nil != err { | ||||||
|  | 		//t.Fatal(err) | ||||||
|  | 		t.Error(err) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	if "" == jwk["d"] { | ||||||
|  | 		t.Fatal("Missing key 'd' from supposed private key") | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	key, err := keypairs.ParsePrivateKey(data) | ||||||
|  | 	if nil != err { | ||||||
|  | 		t.Error(err) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	switch key.(type) { | ||||||
|  | 	case *rsa.PrivateKey: | ||||||
|  | 		// no-op | ||||||
|  | 		//log.Println("is RSA") | ||||||
|  | 	case *ecdsa.PrivateKey: | ||||||
|  | 		// no-op | ||||||
|  | 		//log.Println("is EC") | ||||||
|  | 	default: | ||||||
|  | 		t.Fatal(errors.New("impossible key type")) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	//fmt.Printf("%#v\n", jwk) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func TestGenWithSeed(t *testing.T) { | ||||||
|  | 	// Key A | ||||||
|  | 	client := srv.Client() | ||||||
|  | 	urlstr, _ := url.Parse(srv.URL + "/debug/private.jwk.json") | ||||||
|  | 	res, err := client.Do(&http.Request{ | ||||||
|  | 		Method: "POST", | ||||||
|  | 		URL:    urlstr, | ||||||
|  | 		Body:   ioutil.NopCloser(bytes.NewReader([]byte(`{"seed":"test"}`))), | ||||||
|  | 	}) | ||||||
|  | 	if nil != err { | ||||||
|  | 		//t.Fatal(err) | ||||||
|  | 		t.Error(err) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 	if 200 != res.StatusCode { | ||||||
|  | 		t.Error(fmt.Errorf("bad status code: %d", res.StatusCode)) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	dataA, err := ioutil.ReadAll(res.Body) | ||||||
|  | 	if nil != err { | ||||||
|  | 		//t.Fatal(err) | ||||||
|  | 		t.Error(err) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	// See https://github.com/square/go-jose/issues/189 | ||||||
|  | 	for i := 0; i < 8; i++ { | ||||||
|  | 		// Key B | ||||||
|  | 		client = srv.Client() | ||||||
|  | 		urlstr, _ = url.Parse(srv.URL + "/debug/private.jwk.json") | ||||||
|  | 		res, err = client.Do(&http.Request{ | ||||||
|  | 			Method: "POST", | ||||||
|  | 			URL:    urlstr, | ||||||
|  | 			Body:   ioutil.NopCloser(bytes.NewReader([]byte(`{"seed":"test"}`))), | ||||||
|  | 		}) | ||||||
|  | 		if nil != err { | ||||||
|  | 			//t.Fatal(err) | ||||||
|  | 			t.Error(err) | ||||||
|  | 			return | ||||||
|  | 		} | ||||||
|  | 		if 200 != res.StatusCode { | ||||||
|  | 			t.Error(fmt.Errorf("bad status code: %d", res.StatusCode)) | ||||||
|  | 			return | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		dataB, err := ioutil.ReadAll(res.Body) | ||||||
|  | 		if nil != err { | ||||||
|  | 			//t.Fatal(err) | ||||||
|  | 			t.Error(err) | ||||||
|  | 			return | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		if '{' != dataA[0] || len(dataA) < 100 || string(dataA) != string(dataB) { | ||||||
|  | 			log.Println(string(dataA)) | ||||||
|  | 			log.Println(string(dataB)) | ||||||
|  | 			t.Error(errors.New("keys with identical seeds should be identical")) | ||||||
|  | 			return | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func TestGenWithRand(t *testing.T) { | ||||||
|  | 	// Key A | ||||||
|  | 	client := srv.Client() | ||||||
|  | 	urlstr, _ := url.Parse(srv.URL + "/debug/private.jwk.json") | ||||||
|  | 	res, err := client.Do(&http.Request{ | ||||||
|  | 		Method: "POST", | ||||||
|  | 		URL:    urlstr, | ||||||
|  | 		Body:   ioutil.NopCloser(bytes.NewReader([]byte(`{"seed":""}`))), | ||||||
|  | 	}) | ||||||
|  | 	if nil != err { | ||||||
|  | 		//t.Fatal(err) | ||||||
|  | 		t.Error(err) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 	if 200 != res.StatusCode { | ||||||
|  | 		t.Error(fmt.Errorf("bad status code: %d", res.StatusCode)) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	dataA, err := ioutil.ReadAll(res.Body) | ||||||
|  | 	if nil != err { | ||||||
|  | 		//t.Fatal(err) | ||||||
|  | 		t.Error(err) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	// Key B | ||||||
|  | 	client = srv.Client() | ||||||
|  | 	urlstr, _ = url.Parse(srv.URL + "/debug/private.jwk.json") | ||||||
|  | 	res, err = client.Do(&http.Request{ | ||||||
|  | 		Method: "POST", | ||||||
|  | 		URL:    urlstr, | ||||||
|  | 		Body:   ioutil.NopCloser(bytes.NewReader([]byte(`{"seed":""}`))), | ||||||
|  | 	}) | ||||||
|  | 	if nil != err { | ||||||
|  | 		//t.Fatal(err) | ||||||
|  | 		t.Error(err) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 	if 200 != res.StatusCode { | ||||||
|  | 		t.Error(fmt.Errorf("bad status code: %d", res.StatusCode)) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	dataB, err := ioutil.ReadAll(res.Body) | ||||||
|  | 	if nil != err { | ||||||
|  | 		//t.Fatal(err) | ||||||
|  | 		t.Error(err) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	if string(dataA) == string(dataB) { | ||||||
|  | 		t.Error(errors.New("keys with identical seeds should yield identical keys")) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func TestGeneratePEM(t *testing.T) { | ||||||
|  | 	client := srv.Client() | ||||||
|  | 	urlstr, _ := url.Parse(srv.URL + "/debug/priv.pem") | ||||||
|  | 	//fmt.Println("URL:", srv.URL, urlstr) | ||||||
|  | 	res, err := client.Do(&http.Request{ | ||||||
|  | 		Method: "POST", | ||||||
|  | 		URL:    urlstr, | ||||||
|  | 	}) | ||||||
|  | 	if nil != err { | ||||||
|  | 		//t.Fatal(err) | ||||||
|  | 		t.Error(err) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 	if 200 != res.StatusCode { | ||||||
|  | 		t.Error(fmt.Errorf("bad status code: %d", res.StatusCode)) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	data, err := ioutil.ReadAll(res.Body) | ||||||
|  | 	if nil != err { | ||||||
|  | 		//t.Fatal(err) | ||||||
|  | 		t.Error(err) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	key, err := xkeypairs.ParsePEMPrivateKey(data) | ||||||
|  | 	if nil != err { | ||||||
|  | 		t.Error(err) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	switch key.(type) { | ||||||
|  | 	case *rsa.PrivateKey: | ||||||
|  | 		// no-op | ||||||
|  | 		//log.Println("is RSA") | ||||||
|  | 	case *ecdsa.PrivateKey: | ||||||
|  | 		// no-op | ||||||
|  | 		//log.Println("is EC") | ||||||
|  | 	default: | ||||||
|  | 		t.Fatal(errors.New("impossible key type")) | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func TestPublicJWKWithKey(t *testing.T) { | ||||||
|  | 	client := srv.Client() | ||||||
|  | 	urlstr, _ := url.Parse(srv.URL + "/debug/public.jwk.json") | ||||||
|  | 	//fmt.Println("URL:", srv.URL, urlstr) | ||||||
|  | 	res, err := client.Do(&http.Request{ | ||||||
|  | 		Method: "POST", | ||||||
|  | 		URL:    urlstr, | ||||||
|  | 		Body:   ioutil.NopCloser(bytes.NewReader([]byte(`{"key":"{\"crv\":\"P-256\",\"d\":\"s0YhjGUJpp6OvyuNS_4igrc7ddDZy5N2ANxoQm7E5sc\",\"kty\":\"EC\",\"x\":\"hPsE4OMhpd2TvrhjDgr1BhF-L1n4O-gPm1flwTh5kzo\",\"y\":\"BWZ1naEJuNOdnQ4HmbHavqdLKxoj77Fu8mkJPjSuh54\"}"}`))), | ||||||
|  | 	}) | ||||||
|  | 	if nil != err { | ||||||
|  | 		//t.Fatal(err) | ||||||
|  | 		t.Error(err) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 	if 200 != res.StatusCode { | ||||||
|  | 		t.Error(fmt.Errorf("bad status code: %d", res.StatusCode)) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	data, err := ioutil.ReadAll(res.Body) | ||||||
|  | 	if nil != err { | ||||||
|  | 		//t.Fatal(err) | ||||||
|  | 		t.Error(err) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	jwk := map[string]string{} | ||||||
|  | 	err = json.Unmarshal(data, &jwk) | ||||||
|  | 	if nil != err { | ||||||
|  | 		//t.Fatal(err) | ||||||
|  | 		t.Error(err) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	if "" != jwk["d"] { | ||||||
|  | 		t.Fatal("Has private key 'd' from supposed public key") | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	if "hPsE4OMhpd2TvrhjDgr1BhF-L1n4O-gPm1flwTh5kzo" != jwk["x"] { | ||||||
|  | 		t.Fatal("Missing public key 'x' or 'e' from supposed public key") | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	key, err := keypairs.ParsePublicKey(data) | ||||||
|  | 	if nil != err { | ||||||
|  | 		t.Error(err) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	switch key.Key().(type) { | ||||||
|  | 	case *ecdsa.PublicKey: | ||||||
|  | 		// no-op | ||||||
|  | 		//log.Println("is EC") | ||||||
|  | 	default: | ||||||
|  | 		t.Fatal(errors.New("impossible key type")) | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func TestPublicPEMWithSeed(t *testing.T) { | ||||||
|  | 	client := srv.Client() | ||||||
|  | 	urlstr, _ := url.Parse(srv.URL + "/debug/pub.pem") | ||||||
|  | 	//fmt.Println("URL:", srv.URL, urlstr) | ||||||
|  | 	res, err := client.Do(&http.Request{ | ||||||
|  | 		Method: "POST", | ||||||
|  | 		URL:    urlstr, | ||||||
|  | 		Body:   ioutil.NopCloser(bytes.NewReader([]byte(`{"seed":"test"}`))), | ||||||
|  | 	}) | ||||||
|  | 	if nil != err { | ||||||
|  | 		//t.Fatal(err) | ||||||
|  | 		t.Error(err) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 	if 200 != res.StatusCode { | ||||||
|  | 		t.Error(fmt.Errorf("bad status code: %d", res.StatusCode)) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	data, err := ioutil.ReadAll(res.Body) | ||||||
|  | 	if nil != err { | ||||||
|  | 		//t.Fatal(err) | ||||||
|  | 		t.Error(err) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	key, err := keypairs.ParsePublicKey(data) | ||||||
|  | 	if nil != err { | ||||||
|  | 		t.Error(err) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	switch key.Key().(type) { | ||||||
|  | 	case *rsa.PublicKey: | ||||||
|  | 		// no-op | ||||||
|  | 		//log.Println("is RSA") | ||||||
|  | 	case *ecdsa.PublicKey: | ||||||
|  | 		// no-op | ||||||
|  | 		//log.Println("is EC") | ||||||
|  | 	default: | ||||||
|  | 		t.Fatal(errors.New("impossible key type")) | ||||||
|  | 	} | ||||||
| } | } | ||||||
|  | |||||||
							
								
								
									
										64
									
								
								mockid/nonce.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										64
									
								
								mockid/nonce.go
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,64 @@ | |||||||
|  | package mockid | ||||||
|  | 
 | ||||||
|  | import ( | ||||||
|  | 	"crypto/rand" | ||||||
|  | 	"encoding/base64" | ||||||
|  | 	"net/http" | ||||||
|  | 	"sync" | ||||||
|  | 	"time" | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | //var nonces map[string]int64 | ||||||
|  | //var nonCh chan string | ||||||
|  | var nonces sync.Map | ||||||
|  | 
 | ||||||
|  | func issueNonce(w http.ResponseWriter, r *http.Request) { | ||||||
|  | 	b := make([]byte, 16) | ||||||
|  | 	_, _ = rand.Read(b) | ||||||
|  | 	nonce := base64.RawURLEncoding.EncodeToString(b) | ||||||
|  | 	//nonCh <- nonce | ||||||
|  | 	nonces.Store(nonce, time.Now()) | ||||||
|  | 
 | ||||||
|  | 	w.Header().Set("Replay-Nonce", nonce) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func requireNonce(next http.HandlerFunc) http.HandlerFunc { | ||||||
|  | 	return func(w http.ResponseWriter, r *http.Request) { | ||||||
|  | 		nonce := r.Header.Get("Replay-Nonce") | ||||||
|  | 		// TODO expire nonces every so often | ||||||
|  | 		//t := nonces[nonce] | ||||||
|  | 
 | ||||||
|  | 		if !useNonce(nonce) { | ||||||
|  | 			http.Error( | ||||||
|  | 				w, | ||||||
|  | 				`{ "error": "invalid or expired nonce", "error_code": "ENONCE" }`, | ||||||
|  | 				http.StatusBadRequest, | ||||||
|  | 			) | ||||||
|  | 			return | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		issueNonce(w, r) | ||||||
|  | 		next(w, r) | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func checkNonce(nonce string) bool { | ||||||
|  | 	var t time.Time | ||||||
|  | 	tmp, ok := nonces.Load(nonce) | ||||||
|  | 	if ok { | ||||||
|  | 		t = tmp.(time.Time) | ||||||
|  | 	} | ||||||
|  | 	if ok && time.Now().Sub(t) <= 15*time.Minute { | ||||||
|  | 		return true | ||||||
|  | 	} | ||||||
|  | 	return false | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func useNonce(nonce string) bool { | ||||||
|  | 	if checkNonce(nonce) { | ||||||
|  | 		//delete(nonces, nonce) | ||||||
|  | 		nonces.Delete(nonce) | ||||||
|  | 		return true | ||||||
|  | 	} | ||||||
|  | 	return false | ||||||
|  | } | ||||||
							
								
								
									
										1160
									
								
								mockid/route.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1160
									
								
								mockid/route.go
									
									
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										46
									
								
								oldxkeypairs/jose.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										46
									
								
								oldxkeypairs/jose.go
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,46 @@ | |||||||
|  | package xkeypairs | ||||||
|  | 
 | ||||||
|  | import ( | ||||||
|  | 	"encoding/base64" | ||||||
|  | 	"encoding/json" | ||||||
|  | 	"errors" | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | func (jws *JWS) DecodeComponents() error { | ||||||
|  | 	protected, err := base64.RawURLEncoding.DecodeString(jws.Protected) | ||||||
|  | 	if nil != err { | ||||||
|  | 		return errors.New("invalid JWS header base64Url encoding") | ||||||
|  | 	} | ||||||
|  | 	if err := json.Unmarshal([]byte(protected), &jws.Header); nil != err { | ||||||
|  | 		return errors.New("invalid JWS header") | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	payload, err := base64.RawURLEncoding.DecodeString(jws.Payload) | ||||||
|  | 	if nil != err { | ||||||
|  | 		return errors.New("invalid JWS payload base64Url encoding") | ||||||
|  | 	} | ||||||
|  | 	if err := json.Unmarshal([]byte(payload), &jws.Claims); nil != err { | ||||||
|  | 		return errors.New("invalid JWS claims") | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	return nil | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | /* | ||||||
|  | func Decode(msg string) (*JWS, error) { | ||||||
|  | 	jws := &JWS{} | ||||||
|  | 
 | ||||||
|  | 	decoder := json.NewDecoder(r.Body) | ||||||
|  | 	err := decoder.Decode(jws) | ||||||
|  | 	return jws, err | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func Unmarshal(msg string) (*JWS, error) { | ||||||
|  | 	jws := &JWS{} | ||||||
|  | 
 | ||||||
|  | 	if err := json.Unmarshal([]byte(msg), jws); nil != err { | ||||||
|  | 		return nil, err | ||||||
|  | 	} | ||||||
|  | 	return jws, nil | ||||||
|  | } | ||||||
|  | */ | ||||||
							
								
								
									
										70
									
								
								oldxkeypairs/jwk.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										70
									
								
								oldxkeypairs/jwk.go
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,70 @@ | |||||||
|  | package xkeypairs | ||||||
|  | 
 | ||||||
|  | import ( | ||||||
|  | 	"crypto/ecdsa" | ||||||
|  | 	"crypto/rsa" | ||||||
|  | 	"encoding/base64" | ||||||
|  | 	"errors" | ||||||
|  | 	"fmt" | ||||||
|  | 	"math/big" | ||||||
|  | 
 | ||||||
|  | 	"git.rootprojects.org/root/keypairs" | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | type JWK interface { | ||||||
|  | 	marshalJWK() ([]byte, error) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | type ECJWK struct { | ||||||
|  | 	KeyID string   `json:"kid,omitempty"` | ||||||
|  | 	Curve string   `json:"crv"` | ||||||
|  | 	X     string   `json:"x"` | ||||||
|  | 	Y     string   `json:"y"` | ||||||
|  | 	Use   []string `json:"use,omitempty"` | ||||||
|  | 	Seed  string   `json:"_seed,omitempty"` | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (k *ECJWK) marshalJWK() ([]byte, error) { | ||||||
|  | 	return []byte(fmt.Sprintf(`{"crv":%q,"kty":"EC","x":%q,"y":%q}`, k.Curve, k.X, k.Y)), nil | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | type RSAJWK struct { | ||||||
|  | 	KeyID string   `json:"kid,omitempty"` | ||||||
|  | 	Exp   string   `json:"e"` | ||||||
|  | 	N     string   `json"n"` | ||||||
|  | 	Use   []string `json:"use,omitempty"` | ||||||
|  | 	Seed  string   `json:"_seed,omitempty"` | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (k *RSAJWK) marshalJWK() ([]byte, error) { | ||||||
|  | 	return []byte(fmt.Sprintf(`{"e":%q,"kty":"RSA","n":%q}`, k.Exp, k.N)), nil | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func ToPublicJWK(pubkey keypairs.PublicKey) JWK { | ||||||
|  | 	switch k := pubkey.Key().(type) { | ||||||
|  | 	case *ecdsa.PublicKey: | ||||||
|  | 		return ECToPublicJWK(k) | ||||||
|  | 	case *rsa.PublicKey: | ||||||
|  | 		return RSAToPublicJWK(k) | ||||||
|  | 	default: | ||||||
|  | 		panic(errors.New("impossible key type")) | ||||||
|  | 		return nil | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // ECToPublicJWK will output the most minimal version of an EC JWK (no key id, no "use" flag, nada) | ||||||
|  | func ECToPublicJWK(k *ecdsa.PublicKey) *ECJWK { | ||||||
|  | 	return &ECJWK{ | ||||||
|  | 		Curve: k.Curve.Params().Name, | ||||||
|  | 		X:     base64.RawURLEncoding.EncodeToString(k.X.Bytes()), | ||||||
|  | 		Y:     base64.RawURLEncoding.EncodeToString(k.Y.Bytes()), | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // RSAToPublicJWK will output the most minimal version of an RSA JWK (no key id, no "use" flag, nada) | ||||||
|  | func RSAToPublicJWK(p *rsa.PublicKey) *RSAJWK { | ||||||
|  | 	return &RSAJWK{ | ||||||
|  | 		Exp: base64.RawURLEncoding.EncodeToString(big.NewInt(int64(p.E)).Bytes()), | ||||||
|  | 		N:   base64.RawURLEncoding.EncodeToString(p.N.Bytes()), | ||||||
|  | 	} | ||||||
|  | } | ||||||
							
								
								
									
										173
									
								
								oldxkeypairs/marshal.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										173
									
								
								oldxkeypairs/marshal.go
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,173 @@ | |||||||
|  | package xkeypairs | ||||||
|  | 
 | ||||||
|  | import ( | ||||||
|  | 	"crypto" | ||||||
|  | 	"crypto/ecdsa" | ||||||
|  | 	"crypto/rsa" | ||||||
|  | 	"crypto/x509" | ||||||
|  | 	"encoding/base64" | ||||||
|  | 	"encoding/pem" | ||||||
|  | 	"fmt" | ||||||
|  | 	"log" | ||||||
|  | 	"math/big" | ||||||
|  | 	mathrand "math/rand" | ||||||
|  | 
 | ||||||
|  | 	"git.rootprojects.org/root/keypairs" | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | // MarshalPEMPublicKey outputs the given public key as JWK | ||||||
|  | func MarshalPEMPublicKey(pubkey crypto.PublicKey) ([]byte, error) { | ||||||
|  | 	block, err := marshalDERPublicKey(pubkey) | ||||||
|  | 	if nil != err { | ||||||
|  | 		return nil, err | ||||||
|  | 	} | ||||||
|  | 	return pem.EncodeToMemory(block), nil | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // MarshalDERPublicKey outputs the given public key as JWK | ||||||
|  | func MarshalDERPublicKey(pubkey crypto.PublicKey) ([]byte, error) { | ||||||
|  | 	block, err := marshalDERPublicKey(pubkey) | ||||||
|  | 	if nil != err { | ||||||
|  | 		return nil, err | ||||||
|  | 	} | ||||||
|  | 	return block.Bytes, nil | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // marshalDERPublicKey outputs the given public key as JWK | ||||||
|  | func marshalDERPublicKey(pubkey crypto.PublicKey) (*pem.Block, error) { | ||||||
|  | 
 | ||||||
|  | 	var der []byte | ||||||
|  | 	var typ string | ||||||
|  | 	var err error | ||||||
|  | 	switch k := pubkey.(type) { | ||||||
|  | 	case *rsa.PublicKey: | ||||||
|  | 		der = x509.MarshalPKCS1PublicKey(k) | ||||||
|  | 		typ = "RSA PUBLIC KEY" | ||||||
|  | 	case *ecdsa.PublicKey: | ||||||
|  | 		typ = "PUBLIC KEY" | ||||||
|  | 		der, err = x509.MarshalPKIXPublicKey(k) | ||||||
|  | 		if nil != err { | ||||||
|  | 			return nil, err | ||||||
|  | 		} | ||||||
|  | 	default: | ||||||
|  | 		panic("Developer Error: impossible key type") | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	return &pem.Block{ | ||||||
|  | 		Bytes: der, | ||||||
|  | 		Type:  typ, | ||||||
|  | 	}, nil | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // MarshalJWKPrivateKey outputs the given private key as JWK | ||||||
|  | func MarshalJWKPrivateKey(privkey keypairs.PrivateKey) []byte { | ||||||
|  | 	// thumbprint keys are alphabetically sorted and only include the necessary public parts | ||||||
|  | 	switch k := privkey.(type) { | ||||||
|  | 	case *rsa.PrivateKey: | ||||||
|  | 		return MarshalRSAPrivateKey(k) | ||||||
|  | 	case *ecdsa.PrivateKey: | ||||||
|  | 		return MarshalECPrivateKey(k) | ||||||
|  | 	default: | ||||||
|  | 		// this is unreachable because we know the types that we pass in | ||||||
|  | 		log.Printf("keytype: %t, %+v\n", privkey, privkey) | ||||||
|  | 		panic(keypairs.ErrInvalidPublicKey) | ||||||
|  | 		return nil | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // MarshalDERPrivateKey outputs the given private key as ASN.1 DER | ||||||
|  | func MarshalDERPrivateKey(privkey keypairs.PrivateKey) ([]byte, error) { | ||||||
|  | 	// thumbprint keys are alphabetically sorted and only include the necessary public parts | ||||||
|  | 	switch k := privkey.(type) { | ||||||
|  | 	case *rsa.PrivateKey: | ||||||
|  | 		return x509.MarshalPKCS1PrivateKey(k), nil | ||||||
|  | 	case *ecdsa.PrivateKey: | ||||||
|  | 		return x509.MarshalECPrivateKey(k) | ||||||
|  | 	default: | ||||||
|  | 		// this is unreachable because we know the types that we pass in | ||||||
|  | 		log.Printf("keytype: %t, %+v\n", privkey, privkey) | ||||||
|  | 		panic(keypairs.ErrInvalidPublicKey) | ||||||
|  | 		return nil, nil | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func marshalDERPrivateKey(privkey keypairs.PrivateKey) (*pem.Block, error) { | ||||||
|  | 	var typ string | ||||||
|  | 	var bytes []byte | ||||||
|  | 	var err error | ||||||
|  | 
 | ||||||
|  | 	switch k := privkey.(type) { | ||||||
|  | 	case *rsa.PrivateKey: | ||||||
|  | 		if 0 == mathrand.Intn(2) { | ||||||
|  | 			typ = "PRIVATE KEY" | ||||||
|  | 			bytes, err = x509.MarshalPKCS8PrivateKey(k) | ||||||
|  | 			if nil != err { | ||||||
|  | 				return nil, err | ||||||
|  | 			} | ||||||
|  | 		} else { | ||||||
|  | 			typ = "RSA PRIVATE KEY" | ||||||
|  | 			bytes = x509.MarshalPKCS1PrivateKey(k) | ||||||
|  | 		} | ||||||
|  | 		return &pem.Block{ | ||||||
|  | 			Type:  typ, | ||||||
|  | 			Bytes: bytes, | ||||||
|  | 		}, nil | ||||||
|  | 	case *ecdsa.PrivateKey: | ||||||
|  | 		if 0 == mathrand.Intn(2) { | ||||||
|  | 			typ = "PRIVATE KEY" | ||||||
|  | 			bytes, err = x509.MarshalPKCS8PrivateKey(k) | ||||||
|  | 		} else { | ||||||
|  | 			typ = "EC PRIVATE KEY" | ||||||
|  | 			bytes, err = x509.MarshalECPrivateKey(k) | ||||||
|  | 		} | ||||||
|  | 		if nil != err { | ||||||
|  | 			return nil, err | ||||||
|  | 		} | ||||||
|  | 		return &pem.Block{ | ||||||
|  | 			Type:  typ, | ||||||
|  | 			Bytes: bytes, | ||||||
|  | 		}, nil | ||||||
|  | 	default: | ||||||
|  | 		// this is unreachable because we know the types that we pass in | ||||||
|  | 		log.Printf("keytype: %t, %+v\n", privkey, privkey) | ||||||
|  | 		panic(keypairs.ErrInvalidPublicKey) | ||||||
|  | 		return nil, nil | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // MarshalPEMPrivateKey outputs the given private key as ASN.1 PEM | ||||||
|  | func MarshalPEMPrivateKey(privkey keypairs.PrivateKey) ([]byte, error) { | ||||||
|  | 	block, err := marshalDERPrivateKey(privkey) | ||||||
|  | 	if nil != err { | ||||||
|  | 		return nil, err | ||||||
|  | 	} | ||||||
|  | 	return pem.EncodeToMemory(block), nil | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // MarshalECPrivateKey will output the given private key as JWK | ||||||
|  | func MarshalECPrivateKey(k *ecdsa.PrivateKey) []byte { | ||||||
|  | 	crv := k.Curve.Params().Name | ||||||
|  | 	d := base64.RawURLEncoding.EncodeToString(k.D.Bytes()) | ||||||
|  | 	x := base64.RawURLEncoding.EncodeToString(k.X.Bytes()) | ||||||
|  | 	y := base64.RawURLEncoding.EncodeToString(k.Y.Bytes()) | ||||||
|  | 	return []byte(fmt.Sprintf( | ||||||
|  | 		`{"crv":%q,"d":%q,"kty":"EC","x":%q,"y":%q}`, | ||||||
|  | 		crv, d, x, y, | ||||||
|  | 	)) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // MarshalRSAPrivateKey will output the given private key as JWK | ||||||
|  | func MarshalRSAPrivateKey(pk *rsa.PrivateKey) []byte { | ||||||
|  | 	e := base64.RawURLEncoding.EncodeToString(big.NewInt(int64(pk.E)).Bytes()) | ||||||
|  | 	n := base64.RawURLEncoding.EncodeToString(pk.N.Bytes()) | ||||||
|  | 	d := base64.RawURLEncoding.EncodeToString(pk.D.Bytes()) | ||||||
|  | 	p := base64.RawURLEncoding.EncodeToString(pk.Primes[0].Bytes()) | ||||||
|  | 	q := base64.RawURLEncoding.EncodeToString(pk.Primes[1].Bytes()) | ||||||
|  | 	dp := base64.RawURLEncoding.EncodeToString(pk.Precomputed.Dp.Bytes()) | ||||||
|  | 	dq := base64.RawURLEncoding.EncodeToString(pk.Precomputed.Dq.Bytes()) | ||||||
|  | 	qi := base64.RawURLEncoding.EncodeToString(pk.Precomputed.Qinv.Bytes()) | ||||||
|  | 	return []byte(fmt.Sprintf( | ||||||
|  | 		`{"d":%q,"dp":%q,"dq":%q,"e":%q,"kty":"RSA","n":%q,"p":%q,"q":%q,"qi":%q}`, | ||||||
|  | 		d, dp, dq, e, n, p, q, qi, | ||||||
|  | 	)) | ||||||
|  | } | ||||||
							
								
								
									
										173
									
								
								oldxkeypairs/sign.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										173
									
								
								oldxkeypairs/sign.go
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,173 @@ | |||||||
|  | package xkeypairs | ||||||
|  | 
 | ||||||
|  | import ( | ||||||
|  | 	"crypto" | ||||||
|  | 	"crypto/ecdsa" | ||||||
|  | 	"crypto/rand" | ||||||
|  | 	"crypto/rsa" | ||||||
|  | 	"crypto/sha256" | ||||||
|  | 	"encoding/base64" | ||||||
|  | 	"encoding/json" | ||||||
|  | 	"errors" | ||||||
|  | 	"fmt" | ||||||
|  | 	"io" | ||||||
|  | 	mathrand "math/rand" | ||||||
|  | 	"time" | ||||||
|  | 
 | ||||||
|  | 	"git.rootprojects.org/root/keypairs" | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | // RandomReader may be overwritten for testing | ||||||
|  | var RandomReader io.Reader = rand.Reader | ||||||
|  | 
 | ||||||
|  | //var RandomReader = rand.Reader | ||||||
|  | 
 | ||||||
|  | type JWS struct { | ||||||
|  | 	Header    Object `json:"header"`    // JSON | ||||||
|  | 	Claims    Object `json:"claims"`    // JSON | ||||||
|  | 	Protected string `json:"protected"` // base64 | ||||||
|  | 	Payload   string `json:"payload"`   // base64 | ||||||
|  | 	Signature string `json:"signature"` // base64 | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | type Object = map[string]interface{} | ||||||
|  | 
 | ||||||
|  | // SignClaims adds `typ`, `kid` (or `jwk`), and `alg` in the header and expects claims for `jti`, `exp`, `iss`, and `iat` | ||||||
|  | func SignClaims(privkey keypairs.PrivateKey, header Object, claims Object) (*JWS, error) { | ||||||
|  | 	var randsrc io.Reader = RandomReader | ||||||
|  | 	seed, _ := header["_seed"].(int64) | ||||||
|  | 	if 0 != seed { | ||||||
|  | 		randsrc = mathrand.New(mathrand.NewSource(seed)) | ||||||
|  | 		//delete(header, "_seed") | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	protected, err := headerToProtected(keypairs.NewPublicKey(privkey.Public()), header) | ||||||
|  | 	if nil != err { | ||||||
|  | 		return nil, err | ||||||
|  | 	} | ||||||
|  | 	protected64 := base64.RawURLEncoding.EncodeToString(protected) | ||||||
|  | 
 | ||||||
|  | 	payload, err := claimsToPayload(claims) | ||||||
|  | 	if nil != err { | ||||||
|  | 		return nil, err | ||||||
|  | 	} | ||||||
|  | 	payload64 := base64.RawURLEncoding.EncodeToString(payload) | ||||||
|  | 
 | ||||||
|  | 	signable := fmt.Sprintf(`%s.%s`, protected64, payload64) | ||||||
|  | 	hash := sha256.Sum256([]byte(signable)) | ||||||
|  | 
 | ||||||
|  | 	sig := Sign(randsrc, privkey, hash[:]) | ||||||
|  | 	sig64 := base64.RawURLEncoding.EncodeToString(sig) | ||||||
|  | 	//log.Printf("\n(Sign)\nSignable: %s", signable) | ||||||
|  | 	//log.Printf("Hash: %s", hash) | ||||||
|  | 	//log.Printf("Sig: %s", sig64) | ||||||
|  | 
 | ||||||
|  | 	return &JWS{ | ||||||
|  | 		Header:    header, | ||||||
|  | 		Claims:    claims, | ||||||
|  | 		Protected: protected64, | ||||||
|  | 		Payload:   payload64, | ||||||
|  | 		Signature: sig64, | ||||||
|  | 	}, nil | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func headerToProtected(pub keypairs.PublicKey, header Object) ([]byte, error) { | ||||||
|  | 	if nil == header { | ||||||
|  | 		header = Object{} | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	// Only supporting 2048-bit and P256 keys right now | ||||||
|  | 	// because that's all that's practical and well-supported. | ||||||
|  | 	// No security theatre here. | ||||||
|  | 	alg := "ES256" | ||||||
|  | 	switch pub.Key().(type) { | ||||||
|  | 	case *rsa.PublicKey: | ||||||
|  | 		alg = "RS256" | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	if selfSign, _ := header["_jwk"].(bool); selfSign { | ||||||
|  | 		delete(header, "_jwk") | ||||||
|  | 		any := Object{} | ||||||
|  | 		_ = json.Unmarshal(keypairs.MarshalJWKPublicKey(pub), &any) | ||||||
|  | 		header["jwk"] = any | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	// TODO what are the acceptable values? JWT. JWS? others? | ||||||
|  | 	header["typ"] = "JWT" | ||||||
|  | 	if _, ok := header["jwk"]; !ok { | ||||||
|  | 		thumbprint := keypairs.ThumbprintPublicKey(pub) | ||||||
|  | 		kid, _ := header["kid"].(string) | ||||||
|  | 		if "" != kid && thumbprint != kid { | ||||||
|  | 			return nil, errors.New("'kid' should be the key's thumbprint") | ||||||
|  | 		} | ||||||
|  | 		header["kid"] = thumbprint | ||||||
|  | 	} | ||||||
|  | 	header["alg"] = alg | ||||||
|  | 
 | ||||||
|  | 	protected, err := json.Marshal(header) | ||||||
|  | 	if nil != err { | ||||||
|  | 		return nil, err | ||||||
|  | 	} | ||||||
|  | 	return protected, nil | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func claimsToPayload(claims Object) ([]byte, error) { | ||||||
|  | 	if nil == claims { | ||||||
|  | 		claims = Object{} | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	jti, _ := claims["jti"].(string) | ||||||
|  | 	exp, _ := claims["exp"].(int64) | ||||||
|  | 	dur, _ := claims["exp"].(string) | ||||||
|  | 	insecure, _ := claims["insecure"].(bool) | ||||||
|  | 
 | ||||||
|  | 	// parse if exp is actually a duration, such as "15m" | ||||||
|  | 	if 0 == exp && "" != dur { | ||||||
|  | 		s, err := time.ParseDuration(dur) | ||||||
|  | 		// TODO s, err := time.ParseDuration(dur) | ||||||
|  | 		if nil != err { | ||||||
|  | 			return nil, err | ||||||
|  | 		} | ||||||
|  | 		exp = time.Now().Add(s * time.Second).Unix() | ||||||
|  | 		claims["exp"] = exp | ||||||
|  | 	} | ||||||
|  | 	if "" == jti && 0 == exp && !insecure { | ||||||
|  | 		return nil, errors.New("token must have jti or exp as to be expirable / cancellable") | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	return json.Marshal(claims) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func JWSToJWT(jwt *JWS) string { | ||||||
|  | 	return fmt.Sprintf( | ||||||
|  | 		"%s.%s.%s", | ||||||
|  | 		jwt.Protected, | ||||||
|  | 		jwt.Payload, | ||||||
|  | 		jwt.Signature, | ||||||
|  | 	) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func Sign(rand io.Reader, privkey keypairs.PrivateKey, hash []byte) []byte { | ||||||
|  | 	var sig []byte | ||||||
|  | 
 | ||||||
|  | 	if len(hash) != 32 { | ||||||
|  | 		panic("only 256-bit hashes for 2048-bit and 256-bit keys are supported") | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	switch k := privkey.(type) { | ||||||
|  | 	case *rsa.PrivateKey: | ||||||
|  | 		sig, _ = rsa.SignPKCS1v15(rand, k, crypto.SHA256, hash) | ||||||
|  | 	case *ecdsa.PrivateKey: | ||||||
|  | 		r, s, _ := ecdsa.Sign(rand, k, hash[:]) | ||||||
|  | 		rb := r.Bytes() | ||||||
|  | 		for len(rb) < 32 { | ||||||
|  | 			rb = append([]byte{0}, rb...) | ||||||
|  | 		} | ||||||
|  | 		sb := s.Bytes() | ||||||
|  | 		for len(rb) < 32 { | ||||||
|  | 			sb = append([]byte{0}, sb...) | ||||||
|  | 		} | ||||||
|  | 		sig = append(rb, sb...) | ||||||
|  | 	} | ||||||
|  | 	return sig | ||||||
|  | } | ||||||
							
								
								
									
										172
									
								
								oldxkeypairs/verify.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										172
									
								
								oldxkeypairs/verify.go
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,172 @@ | |||||||
|  | package xkeypairs | ||||||
|  | 
 | ||||||
|  | import ( | ||||||
|  | 	"crypto" | ||||||
|  | 	"crypto/ecdsa" | ||||||
|  | 	"crypto/elliptic" | ||||||
|  | 	"crypto/rsa" | ||||||
|  | 	"crypto/sha256" | ||||||
|  | 	"encoding/base64" | ||||||
|  | 	"errors" | ||||||
|  | 	"fmt" | ||||||
|  | 	"io" | ||||||
|  | 	"log" | ||||||
|  | 	"math/big" | ||||||
|  | 	mathrand "math/rand" | ||||||
|  | 	"time" | ||||||
|  | 
 | ||||||
|  | 	"git.rootprojects.org/root/keypairs" | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | func VerifyClaims(pubkey keypairs.PublicKey, jws *JWS) (bool, error) { | ||||||
|  | 	seed, _ := jws.Header["_seed"].(int64) | ||||||
|  | 	seedf64, _ := jws.Header["_seed"].(float64) | ||||||
|  | 	kty, _ := jws.Header["_kty"].(string) | ||||||
|  | 	kid, _ := jws.Header["kid"].(string) | ||||||
|  | 	jwkmap, hasJWK := jws.Header["jwk"].(Object) | ||||||
|  | 	//var jwk JWK = nil | ||||||
|  | 
 | ||||||
|  | 	if 0 == seed { | ||||||
|  | 		seed = int64(seedf64) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	var pub keypairs.PublicKey = nil | ||||||
|  | 	if hasJWK { | ||||||
|  | 		log.Println("Security TODO: did not check jws.Claims[\"sub\"] against 'jwk' thumbprint") | ||||||
|  | 		log.Println("Security TODO: did not check jws.Claims[\"iss\"]") | ||||||
|  | 		kty := jwkmap["kty"] | ||||||
|  | 		var err error | ||||||
|  | 		if "RSA" == kty { | ||||||
|  | 			e, _ := jwkmap["e"].(string) | ||||||
|  | 			n, _ := jwkmap["n"].(string) | ||||||
|  | 			k, _ := (&RSAJWK{ | ||||||
|  | 				Exp: e, | ||||||
|  | 				N:   n, | ||||||
|  | 			}).marshalJWK() | ||||||
|  | 			pub, err = keypairs.ParseJWKPublicKey(k) | ||||||
|  | 			if nil != err { | ||||||
|  | 				return false, err | ||||||
|  | 			} | ||||||
|  | 		} else { | ||||||
|  | 			crv, _ := jwkmap["crv"].(string) | ||||||
|  | 			x, _ := jwkmap["x"].(string) | ||||||
|  | 			y, _ := jwkmap["y"].(string) | ||||||
|  | 			k, _ := (&ECJWK{ | ||||||
|  | 				Curve: crv, | ||||||
|  | 				X:     x, | ||||||
|  | 				Y:     y, | ||||||
|  | 			}).marshalJWK() | ||||||
|  | 			pub, err = keypairs.ParseJWKPublicKey(k) | ||||||
|  | 			if nil != err { | ||||||
|  | 				return false, err | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 	} else { | ||||||
|  | 		if "" == kid { | ||||||
|  | 			return false, errors.New("token should have 'kid' or 'jwk' in header") | ||||||
|  | 		} | ||||||
|  | 		if nil == pubkey { | ||||||
|  | 			if 0 == seed { | ||||||
|  | 				return false, errors.New("the debug API requires '_seed' to accompany 'kid'") | ||||||
|  | 			} | ||||||
|  | 			if "" == kty { | ||||||
|  | 				return false, errors.New("the debug API requires '_kty' to accompany '_seed'") | ||||||
|  | 			} | ||||||
|  | 			privkey := genPrivKey(seed, kty) | ||||||
|  | 			pub = keypairs.NewPublicKey(privkey.Public()) | ||||||
|  | 		} else { | ||||||
|  | 			pub = pubkey | ||||||
|  | 		} | ||||||
|  | 		log.Println("Security TODO: did not check jws.Claims[\"kid\"] against thumbprint") | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	jti, _ := jws.Claims["jti"].(string) | ||||||
|  | 	expf64, _ := jws.Claims["exp"].(float64) | ||||||
|  | 	exp := int64(expf64) | ||||||
|  | 	if 0 == exp { | ||||||
|  | 		if "" == jti { | ||||||
|  | 			return false, errors.New("one of 'jti' or 'exp' must exist for token expiry") | ||||||
|  | 		} | ||||||
|  | 	} else { | ||||||
|  | 		if time.Now().Unix() > exp { | ||||||
|  | 			return false, fmt.Errorf("token expired at %d (%s)", exp, time.Unix(exp, 0)) | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	signable := fmt.Sprintf("%s.%s", jws.Protected, jws.Payload) | ||||||
|  | 	hash := sha256.Sum256([]byte(signable)) | ||||||
|  | 	sig, err := base64.RawURLEncoding.DecodeString(jws.Signature) | ||||||
|  | 	if nil != err { | ||||||
|  | 		return false, err | ||||||
|  | 	} | ||||||
|  | 	//log.Printf("\n(Verify)\nSignable: %s", signable) | ||||||
|  | 	//log.Printf("Hash: %s", hash) | ||||||
|  | 	//log.Printf("Sig: %s", jws.Signature) | ||||||
|  | 
 | ||||||
|  | 	return Verify(pub, hash[:], sig), nil | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func Verify(pubkey keypairs.PublicKey, hash []byte, sig []byte) bool { | ||||||
|  | 
 | ||||||
|  | 	switch pub := pubkey.Key().(type) { | ||||||
|  | 	case *rsa.PublicKey: | ||||||
|  | 		//log.Printf("RSA VERIFY") | ||||||
|  | 		// TODO keypairs.Size(key) to detect key size ? | ||||||
|  | 		//alg := "SHA256" | ||||||
|  | 		// TODO: this hasn't been tested yet | ||||||
|  | 		if err := rsa.VerifyPKCS1v15(pub, crypto.SHA256, hash, sig); nil != err { | ||||||
|  | 			return false | ||||||
|  | 		} | ||||||
|  | 		return true | ||||||
|  | 	case *ecdsa.PublicKey: | ||||||
|  | 		r := &big.Int{} | ||||||
|  | 		r.SetBytes(sig[0:32]) | ||||||
|  | 		s := &big.Int{} | ||||||
|  | 		s.SetBytes(sig[32:]) | ||||||
|  | 		return ecdsa.Verify(pub, hash, r, s) | ||||||
|  | 	default: | ||||||
|  | 		panic("impossible condition: non-rsa/non-ecdsa key") | ||||||
|  | 		return false | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | const maxRetry = 16 | ||||||
|  | 
 | ||||||
|  | func genPrivKey(seed int64, kty string) keypairs.PrivateKey { | ||||||
|  | 	var privkey keypairs.PrivateKey | ||||||
|  | 
 | ||||||
|  | 	if "RSA" == kty { | ||||||
|  | 		keylen := 2048 | ||||||
|  | 		privkey, _ = rsa.GenerateKey(nextReader(seed), keylen) | ||||||
|  | 		if 0 != seed { | ||||||
|  | 			for i := 0; i < maxRetry; i++ { | ||||||
|  | 				otherkey, _ := rsa.GenerateKey(nextReader(seed), keylen) | ||||||
|  | 				otherCmp := otherkey.D.Cmp(privkey.(*rsa.PrivateKey).D) | ||||||
|  | 				if 0 != otherCmp { | ||||||
|  | 					// There are two possible keys, choose the lesser D value | ||||||
|  | 					// See https://github.com/square/go-jose/issues/189 | ||||||
|  | 					if otherCmp < 0 { | ||||||
|  | 						privkey = otherkey | ||||||
|  | 					} | ||||||
|  | 					break | ||||||
|  | 				} | ||||||
|  | 				if maxRetry == i-1 { | ||||||
|  | 					log.Printf("error: coinflip landed on heads %d times", maxRetry) | ||||||
|  | 					// TODO return random / retry error | ||||||
|  | 				} | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 	} else { | ||||||
|  | 		// TODO: EC keys may also suffer the same random problems in the future | ||||||
|  | 		privkey, _ = ecdsa.GenerateKey(elliptic.P256(), nextReader(seed)) | ||||||
|  | 	} | ||||||
|  | 	return privkey | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // this shananigans is only for testing and debug API stuff | ||||||
|  | func nextReader(seed int64) io.Reader { | ||||||
|  | 	if 0 == seed { | ||||||
|  | 		return RandomReader | ||||||
|  | 	} | ||||||
|  | 	return mathrand.New(mathrand.NewSource(seed)) | ||||||
|  | } | ||||||
							
								
								
									
										1
									
								
								public/.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								public/.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @ -0,0 +1 @@ | |||||||
|  | package-lock.json | ||||||
							
								
								
									
										16
									
								
								public/.jshintrc
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								public/.jshintrc
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,16 @@ | |||||||
|  | { "node": true | ||||||
|  | , "browser": true | ||||||
|  | , "globals": { "Promise": true } | ||||||
|  | , "esversion": 8 | ||||||
|  | 
 | ||||||
|  | , "indent": 2 | ||||||
|  | , "onevar": true | ||||||
|  | , "laxbreak": true | ||||||
|  | , "curly": true | ||||||
|  | , "nonbsp": true | ||||||
|  | 
 | ||||||
|  | , "eqeqeq": true | ||||||
|  | , "immed": true | ||||||
|  | , "undef": true | ||||||
|  | , "unused": true | ||||||
|  | } | ||||||
							
								
								
									
										1
									
								
								public/.prettierignore
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								public/.prettierignore
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1 @@ | |||||||
|  | dist/ | ||||||
							
								
								
									
										8
									
								
								public/.prettierrc
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										8
									
								
								public/.prettierrc
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,8 @@ | |||||||
|  | { | ||||||
|  |   "bracketSpacing": true, | ||||||
|  |   "printWidth": 80, | ||||||
|  |   "singleQuote": true, | ||||||
|  |   "tabWidth": 4, | ||||||
|  |   "trailingComma": "none", | ||||||
|  |   "useTabs": true | ||||||
|  | } | ||||||
| @ -1,3 +1,44 @@ | |||||||
|  | <html> | ||||||
|  | 	<head> | ||||||
|  | 		<meta name="google-signin-scope" content="email" /> | ||||||
|  | 		<meta | ||||||
|  | 			name="google-signin-client_id" | ||||||
|  | 			content="291138637698-9hjbgadgkibuv9j26104aj0bg5bia30j.apps.googleusercontent.com" | ||||||
|  | 		/> | ||||||
|  | 
 | ||||||
|  | 		<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> | <h1>Tokens for Testing</h1> | ||||||
| Compatible with | Compatible with | ||||||
| @ -13,6 +54,7 @@ Compatible with | |||||||
| 
 | 
 | ||||||
|   * https://mock.pocketid.app/access_token |   * https://mock.pocketid.app/access_token | ||||||
|   * https://mock.pocketid.app/authorization_header |   * https://mock.pocketid.app/authorization_header | ||||||
|  |   * https://mock.pocketid.app/inspect_token | ||||||
|   * https://xxx.mock.pocketid.app/.well-known/openid-configuration |   * https://xxx.mock.pocketid.app/.well-known/openid-configuration | ||||||
|   * https://xxx.mock.pocketid.app/.well-known/jwks.json |   * https://xxx.mock.pocketid.app/.well-known/jwks.json | ||||||
|   * https://mock.pocketid.app/key.jwk.json |   * https://mock.pocketid.app/key.jwk.json | ||||||
| @ -48,6 +90,13 @@ For example: | |||||||
|   HEADER=$(curl -fL https://mock.pocketid.app/authorization_header) |   HEADER=$(curl -fL https://mock.pocketid.app/authorization_header) | ||||||
|   # Authorization: Bearer <token> |   # Authorization: Bearer <token> | ||||||
| 
 | 
 | ||||||
|  | <h3>Inspecting the Token</h3> | ||||||
|  | 
 | ||||||
|  | You can see its decoded form at the `inspect_token` endpoint: | ||||||
|  | 
 | ||||||
|  |   curl -fL https://mock.pocketid.app/inspect_token \ | ||||||
|  |     -H "$(curl -fL https://mock.pocketid.app/authorization_header)" | ||||||
|  | 
 | ||||||
| <h3>The Token, Decoded</h3> | <h3>The Token, Decoded</h3> | ||||||
| 
 | 
 | ||||||
| The Token will look like this: | The Token will look like this: | ||||||
| @ -105,3 +154,110 @@ You shouldn't use it for automated testing, because it will change, but it looks | |||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
| </code></pre> | </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> | ||||||
|  | |||||||
							
								
								
									
										3
									
								
								public/main.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								public/main.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,3 @@ | |||||||
|  | 'use strict'; | ||||||
|  | 
 | ||||||
|  | require('./pocket/consumer.js'); | ||||||
							
								
								
									
										34
									
								
								public/package.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										34
									
								
								public/package.json
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,34 @@ | |||||||
|  | { | ||||||
|  |   "name": "pocketid", | ||||||
|  |   "version": "0.1.0", | ||||||
|  |   "description": "ID tokens made easy", | ||||||
|  |   "main": "pocketid.js", | ||||||
|  |   "scripts": { | ||||||
|  |     "prettier": "prettier --write '**/*.{css,js,md}'", | ||||||
|  |     "test": "node pocketid_test.js" | ||||||
|  |   }, | ||||||
|  |   "repository": { | ||||||
|  |     "type": "git", | ||||||
|  |     "url": "https://example.com/pocketid.git" | ||||||
|  |   }, | ||||||
|  |   "keywords": [ | ||||||
|  |     "oauth1", | ||||||
|  |     "oauth2", | ||||||
|  |     "oauth3", | ||||||
|  |     "oidc", | ||||||
|  |     "acme", | ||||||
|  |     "jwt", | ||||||
|  |     "jose", | ||||||
|  |     "jws", | ||||||
|  |     "jwk" | ||||||
|  |   ], | ||||||
|  |   "author": "AJ ONeal <coolaj86@gmail.com>", | ||||||
|  |   "license": "MPL-2.0", | ||||||
|  |   "dependencies": { | ||||||
|  |     "@root/keypairs": "^0.10.1" | ||||||
|  |   }, | ||||||
|  |   "devDependencies": { | ||||||
|  |     "webpack": "^5.0.0-beta.28", | ||||||
|  |     "webpack-cli": "^3.3.12" | ||||||
|  |   } | ||||||
|  | } | ||||||
							
								
								
									
										19
									
								
								public/webpack.config.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								public/webpack.config.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,19 @@ | |||||||
|  | 'use strict'; | ||||||
|  | 
 | ||||||
|  | var path = require('path'); | ||||||
|  | 
 | ||||||
|  | module.exports = { | ||||||
|  | 	entry: './main.js', | ||||||
|  | 	mode: 'development', | ||||||
|  | 	devServer: { | ||||||
|  | 		contentBase: path.join(__dirname, 'dist'), | ||||||
|  | 		port: 3001 | ||||||
|  | 	}, | ||||||
|  | 	output: { | ||||||
|  | 		publicPath: 'http://localhost:3001/' | ||||||
|  | 	}, | ||||||
|  | 	module: { | ||||||
|  | 		rules: [{}] | ||||||
|  | 	}, | ||||||
|  | 	plugins: [] | ||||||
|  | }; | ||||||
							
								
								
									
										19
									
								
								vendor/git.rootprojects.org/root/hashcash/.gitignore
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								vendor/git.rootprojects.org/root/hashcash/.gitignore
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							| @ -0,0 +1,19 @@ | |||||||
|  | cmd/hashcash/hashcash | ||||||
|  | 
 | ||||||
|  | # ---> Go | ||||||
|  | # Binaries for programs and plugins | ||||||
|  | *.exe | ||||||
|  | *.exe~ | ||||||
|  | *.dll | ||||||
|  | *.so | ||||||
|  | *.dylib | ||||||
|  | 
 | ||||||
|  | # Test binary, built with `go test -c` | ||||||
|  | *.test | ||||||
|  | 
 | ||||||
|  | # Output of the go coverage tool, specifically when used with LiteIDE | ||||||
|  | *.out | ||||||
|  | 
 | ||||||
|  | # Dependency directories (remove the comment below to include it) | ||||||
|  | # vendor/ | ||||||
|  | 
 | ||||||
							
								
								
									
										312
									
								
								vendor/git.rootprojects.org/root/hashcash/LICENSE
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										312
									
								
								vendor/git.rootprojects.org/root/hashcash/LICENSE
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							| @ -0,0 +1,312 @@ | |||||||
|  | Mozilla Public License Version 2.0 | ||||||
|  | 
 | ||||||
|  |    1. Definitions | ||||||
|  | 
 | ||||||
|  | 1.1. "Contributor" means each individual or legal entity that creates, contributes | ||||||
|  | to the creation of, or owns Covered Software. | ||||||
|  | 
 | ||||||
|  | 1.2. "Contributor Version" means the combination of the Contributions of others | ||||||
|  | (if any) used by a Contributor and that particular Contributor's Contribution. | ||||||
|  | 
 | ||||||
|  |       1.3. "Contribution" means Covered Software of a particular Contributor. | ||||||
|  | 
 | ||||||
|  | 1.4. "Covered Software" means Source Code Form to which the initial Contributor | ||||||
|  | has attached the notice in Exhibit A, the Executable Form of such Source Code | ||||||
|  | Form, and Modifications of such Source Code Form, in each case including portions | ||||||
|  | thereof. | ||||||
|  | 
 | ||||||
|  |       1.5. "Incompatible With Secondary Licenses" means | ||||||
|  | 
 | ||||||
|  | (a) that the initial Contributor has attached the notice described in Exhibit | ||||||
|  | B to the Covered Software; or | ||||||
|  | 
 | ||||||
|  | (b) that the Covered Software was made available under the terms of version | ||||||
|  | 1.1 or earlier of the License, but not also under the terms of a Secondary | ||||||
|  | License. | ||||||
|  | 
 | ||||||
|  | 1.6. "Executable Form" means any form of the work other than Source Code Form. | ||||||
|  | 
 | ||||||
|  | 1.7. "Larger Work" means a work that combines Covered Software with other | ||||||
|  | material, in a separate file or files, that is not Covered Software. | ||||||
|  | 
 | ||||||
|  |       1.8. "License" means this document. | ||||||
|  | 
 | ||||||
|  | 1.9. "Licensable" means having the right to grant, to the maximum extent possible, | ||||||
|  | whether at the time of the initial grant or subsequently, any and all of the | ||||||
|  | rights conveyed by this License. | ||||||
|  | 
 | ||||||
|  |       1.10. "Modifications" means any of the following: | ||||||
|  | 
 | ||||||
|  | (a) any file in Source Code Form that results from an addition to, deletion | ||||||
|  | from, or modification of the contents of Covered Software; or | ||||||
|  | 
 | ||||||
|  | (b) any new file in Source Code Form that contains any Covered Software. | ||||||
|  | 
 | ||||||
|  | 1.11. "Patent Claims" of a Contributor means any patent claim(s), including | ||||||
|  | without limitation, method, process, and apparatus claims, in any patent Licensable | ||||||
|  | by such Contributor that would be infringed, but for the grant of the License, | ||||||
|  | by the making, using, selling, offering for sale, having made, import, or | ||||||
|  | transfer of either its Contributions or its Contributor Version. | ||||||
|  | 
 | ||||||
|  | 1.12. "Secondary License" means either the GNU General Public License, Version | ||||||
|  | 2.0, the GNU Lesser General Public License, Version 2.1, the GNU Affero General | ||||||
|  | Public License, Version 3.0, or any later versions of those licenses. | ||||||
|  | 
 | ||||||
|  | 1.13. "Source Code Form" means the form of the work preferred for making modifications. | ||||||
|  | 
 | ||||||
|  | 1.14. "You" (or "Your") means an individual or a legal entity exercising rights | ||||||
|  | under this License. For legal entities, "You" includes any entity that controls, | ||||||
|  | is controlled by, or is under common control with You. For purposes of this | ||||||
|  | definition, "control" means (a) the power, direct or indirect, to cause the | ||||||
|  | direction or management of such entity, whether by contract or otherwise, | ||||||
|  | or (b) ownership of more than fifty percent (50%) of the outstanding shares | ||||||
|  | or beneficial ownership of such entity. | ||||||
|  | 
 | ||||||
|  |    2. License Grants and Conditions | ||||||
|  | 
 | ||||||
|  |       2.1. Grants | ||||||
|  | 
 | ||||||
|  | Each Contributor hereby grants You a world-wide, royalty-free, non-exclusive | ||||||
|  | license: | ||||||
|  | 
 | ||||||
|  | (a) under intellectual property rights (other than patent or trademark) Licensable | ||||||
|  | by such Contributor to use, reproduce, make available, modify, display, perform, | ||||||
|  | distribute, and otherwise exploit its Contributions, either on an unmodified | ||||||
|  | basis, with Modifications, or as part of a Larger Work; and | ||||||
|  | 
 | ||||||
|  | (b) under Patent Claims of such Contributor to make, use, sell, offer for | ||||||
|  | sale, have made, import, and otherwise transfer either its Contributions or | ||||||
|  | its Contributor Version. | ||||||
|  | 
 | ||||||
|  |       2.2. Effective Date | ||||||
|  | 
 | ||||||
|  | The licenses granted in Section 2.1 with respect to any Contribution become | ||||||
|  | effective for each Contribution on the date the Contributor first distributes | ||||||
|  | such Contribution. | ||||||
|  | 
 | ||||||
|  |       2.3. Limitations on Grant Scope | ||||||
|  | 
 | ||||||
|  | The licenses granted in this Section 2 are the only rights granted under this | ||||||
|  | License. No additional rights or licenses will be implied from the distribution | ||||||
|  | or licensing of Covered Software under this License. Notwithstanding Section | ||||||
|  | 2.1(b) above, no patent license is granted by a Contributor: | ||||||
|  | 
 | ||||||
|  | (a) for any code that a Contributor has removed from Covered Software; or | ||||||
|  | 
 | ||||||
|  | (b) for infringements caused by: (i) Your and any other third party's modifications | ||||||
|  | of Covered Software, or (ii) the combination of its Contributions with other | ||||||
|  | software (except as part of its Contributor Version); or | ||||||
|  | 
 | ||||||
|  | (c) under Patent Claims infringed by Covered Software in the absence of its | ||||||
|  | Contributions. | ||||||
|  | 
 | ||||||
|  | This License does not grant any rights in the trademarks, service marks, or | ||||||
|  | logos of any Contributor (except as may be necessary to comply with the notice | ||||||
|  | requirements in Section 3.4). | ||||||
|  | 
 | ||||||
|  |       2.4. Subsequent Licenses | ||||||
|  | 
 | ||||||
|  | No Contributor makes additional grants as a result of Your choice to distribute | ||||||
|  | the Covered Software under a subsequent version of this License (see Section | ||||||
|  | 10.2) or under the terms of a Secondary License (if permitted under the terms | ||||||
|  | of Section 3.3). | ||||||
|  | 
 | ||||||
|  |       2.5. Representation | ||||||
|  | 
 | ||||||
|  | Each Contributor represents that the Contributor believes its Contributions | ||||||
|  | are its original creation(s) or it has sufficient rights to grant the rights | ||||||
|  | to its Contributions conveyed by this License. | ||||||
|  | 
 | ||||||
|  |       2.6. Fair Use | ||||||
|  | 
 | ||||||
|  | This License is not intended to limit any rights You have under applicable | ||||||
|  | copyright doctrines of fair use, fair dealing, or other equivalents. | ||||||
|  | 
 | ||||||
|  |       2.7. Conditions | ||||||
|  | 
 | ||||||
|  | Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted in | ||||||
|  | Section 2.1. | ||||||
|  | 
 | ||||||
|  |    3. Responsibilities | ||||||
|  | 
 | ||||||
|  |       3.1. Distribution of Source Form | ||||||
|  | 
 | ||||||
|  | All distribution of Covered Software in Source Code Form, including any Modifications | ||||||
|  | that You create or to which You contribute, must be under the terms of this | ||||||
|  | License. You must inform recipients that the Source Code Form of the Covered | ||||||
|  | Software is governed by the terms of this License, and how they can obtain | ||||||
|  | a copy of this License. You may not attempt to alter or restrict the recipients' | ||||||
|  | rights in the Source Code Form. | ||||||
|  | 
 | ||||||
|  |       3.2. Distribution of Executable Form | ||||||
|  | 
 | ||||||
|  |       If You distribute Covered Software in Executable Form then: | ||||||
|  | 
 | ||||||
|  | (a) such Covered Software must also be made available in Source Code Form, | ||||||
|  | as described in Section 3.1, and You must inform recipients of the Executable | ||||||
|  | Form how they can obtain a copy of such Source Code Form by reasonable means | ||||||
|  | in a timely manner, at a charge no more than the cost of distribution to the | ||||||
|  | recipient; and | ||||||
|  | 
 | ||||||
|  | (b) You may distribute such Executable Form under the terms of this License, | ||||||
|  | or sublicense it under different terms, provided that the license for the | ||||||
|  | Executable Form does not attempt to limit or alter the recipients' rights | ||||||
|  | in the Source Code Form under this License. | ||||||
|  | 
 | ||||||
|  |       3.3. Distribution of a Larger Work | ||||||
|  | 
 | ||||||
|  | You may create and distribute a Larger Work under terms of Your choice, provided | ||||||
|  | that You also comply with the requirements of this License for the Covered | ||||||
|  | Software. If the Larger Work is a combination of Covered Software with a work | ||||||
|  | governed by one or more Secondary Licenses, and the Covered Software is not | ||||||
|  | Incompatible With Secondary Licenses, this License permits You to additionally | ||||||
|  | distribute such Covered Software under the terms of such Secondary License(s), | ||||||
|  | so that the recipient of the Larger Work may, at their option, further distribute | ||||||
|  | the Covered Software under the terms of either this License or such Secondary | ||||||
|  | License(s). | ||||||
|  | 
 | ||||||
|  |       3.4. Notices | ||||||
|  | 
 | ||||||
|  | You may not remove or alter the substance of any license notices (including | ||||||
|  | copyright notices, patent notices, disclaimers of warranty, or limitations | ||||||
|  | of liability) contained within the Source Code Form of the Covered Software, | ||||||
|  | except that You may alter any license notices to the extent required to remedy | ||||||
|  | known factual inaccuracies. | ||||||
|  | 
 | ||||||
|  |       3.5. Application of Additional Terms | ||||||
|  | 
 | ||||||
|  | You may choose to offer, and to charge a fee for, warranty, support, indemnity | ||||||
|  | or liability obligations to one or more recipients of Covered Software. However, | ||||||
|  | You may do so only on Your own behalf, and not on behalf of any Contributor. | ||||||
|  | You must make it absolutely clear that any such warranty, support, indemnity, | ||||||
|  | or liability obligation is offered by You alone, and You hereby agree to indemnify | ||||||
|  | every Contributor for any liability incurred by such Contributor as a result | ||||||
|  | of warranty, support, indemnity or liability terms You offer. You may include | ||||||
|  | additional disclaimers of warranty and limitations of liability specific to | ||||||
|  | any jurisdiction. | ||||||
|  | 
 | ||||||
|  |    4. Inability to Comply Due to Statute or Regulation | ||||||
|  | 
 | ||||||
|  | If it is impossible for You to comply with any of the terms of this License | ||||||
|  | with respect to some or all of the Covered Software due to statute, judicial | ||||||
|  | order, or regulation then You must: (a) comply with the terms of this License | ||||||
|  | to the maximum extent possible; and (b) describe the limitations and the code | ||||||
|  | they affect. Such description must be placed in a text file included with | ||||||
|  | all distributions of the Covered Software under this License. Except to the | ||||||
|  | extent prohibited by statute or regulation, such description must be sufficiently | ||||||
|  | detailed for a recipient of ordinary skill to be able to understand it. | ||||||
|  | 
 | ||||||
|  |    5. Termination | ||||||
|  | 
 | ||||||
|  | 5.1. The rights granted under this License will terminate automatically if | ||||||
|  | You fail to comply with any of its terms. However, if You become compliant, | ||||||
|  | then the rights granted under this License from a particular Contributor are | ||||||
|  | reinstated (a) provisionally, unless and until such Contributor explicitly | ||||||
|  | and finally terminates Your grants, and (b) on an ongoing basis, if such Contributor | ||||||
|  | fails to notify You of the non-compliance by some reasonable means prior to | ||||||
|  | 60 days after You have come back into compliance. Moreover, Your grants from | ||||||
|  | a particular Contributor are reinstated on an ongoing basis if such Contributor | ||||||
|  | notifies You of the non-compliance by some reasonable means, this is the first | ||||||
|  | time You have received notice of non-compliance with this License from such | ||||||
|  | Contributor, and You become compliant prior to 30 days after Your receipt | ||||||
|  | of the notice. | ||||||
|  | 
 | ||||||
|  | 5.2. If You initiate litigation against any entity by asserting a patent infringement | ||||||
|  | claim (excluding declaratory judgment actions, counter-claims, and cross-claims) | ||||||
|  | alleging that a Contributor Version directly or indirectly infringes any patent, | ||||||
|  | then the rights granted to You by any and all Contributors for the Covered | ||||||
|  | Software under Section 2.1 of this License shall terminate. | ||||||
|  | 
 | ||||||
|  | 5.3. In the event of termination under Sections 5.1 or 5.2 above, all end | ||||||
|  | user license agreements (excluding distributors and resellers) which have | ||||||
|  | been validly granted by You or Your distributors under this License prior | ||||||
|  | to termination shall survive termination. | ||||||
|  | 
 | ||||||
|  |    6. Disclaimer of Warranty | ||||||
|  | 
 | ||||||
|  | Covered Software is provided under this License on an "as is" basis, without | ||||||
|  | warranty of any kind, either expressed, implied, or statutory, including, | ||||||
|  | without limitation, warranties that the Covered Software is free of defects, | ||||||
|  | merchantable, fit for a particular purpose or non-infringing. The entire risk | ||||||
|  | as to the quality and performance of the Covered Software is with You. Should | ||||||
|  | any Covered Software prove defective in any respect, You (not any Contributor) | ||||||
|  | assume the cost of any necessary servicing, repair, or correction. This disclaimer | ||||||
|  | of warranty constitutes an essential part of this License. No use of any Covered | ||||||
|  | Software is authorized under this License except under this disclaimer. | ||||||
|  | 
 | ||||||
|  |    7. Limitation of Liability | ||||||
|  | 
 | ||||||
|  | Under no circumstances and under no legal theory, whether tort (including | ||||||
|  | negligence), contract, or otherwise, shall any Contributor, or anyone who | ||||||
|  | distributes Covered Software as permitted above, be liable to You for any | ||||||
|  | direct, indirect, special, incidental, or consequential damages of any character | ||||||
|  | including, without limitation, damages for lost profits, loss of goodwill, | ||||||
|  | work stoppage, computer failure or malfunction, or any and all other commercial | ||||||
|  | damages or losses, even if such party shall have been informed of the possibility | ||||||
|  | of such damages. This limitation of liability shall not apply to liability | ||||||
|  | for death or personal injury resulting from such party's negligence to the | ||||||
|  | extent applicable law prohibits such limitation. Some jurisdictions do not | ||||||
|  | allow the exclusion or limitation of incidental or consequential damages, | ||||||
|  | so this exclusion and limitation may not apply to You. | ||||||
|  | 
 | ||||||
|  |    8. Litigation | ||||||
|  | 
 | ||||||
|  | Any litigation relating to this License may be brought only in the courts | ||||||
|  | of a jurisdiction where the defendant maintains its principal place of business | ||||||
|  | and such litigation shall be governed by laws of that jurisdiction, without | ||||||
|  | reference to its conflict-of-law provisions. Nothing in this Section shall | ||||||
|  | prevent a party's ability to bring cross-claims or counter-claims. | ||||||
|  | 
 | ||||||
|  |    9. Miscellaneous | ||||||
|  | 
 | ||||||
|  | This License represents the complete agreement concerning the subject matter | ||||||
|  | hereof. If any provision of this License is held to be unenforceable, such | ||||||
|  | provision shall be reformed only to the extent necessary to make it enforceable. | ||||||
|  | Any law or regulation which provides that the language of a contract shall | ||||||
|  | be construed against the drafter shall not be used to construe this License | ||||||
|  | against a Contributor. | ||||||
|  | 
 | ||||||
|  |    10. Versions of the License | ||||||
|  | 
 | ||||||
|  |       10.1. New Versions | ||||||
|  | 
 | ||||||
|  | Mozilla Foundation is the license steward. Except as provided in Section 10.3, | ||||||
|  | no one other than the license steward has the right to modify or publish new | ||||||
|  | versions of this License. Each version will be given a distinguishing version | ||||||
|  | number. | ||||||
|  | 
 | ||||||
|  |       10.2. Effect of New Versions | ||||||
|  | 
 | ||||||
|  | You may distribute the Covered Software under the terms of the version of | ||||||
|  | the License under which You originally received the Covered Software, or under | ||||||
|  | the terms of any subsequent version published by the license steward. | ||||||
|  | 
 | ||||||
|  |       10.3. Modified Versions | ||||||
|  | 
 | ||||||
|  | If you create software not governed by this License, and you want to create | ||||||
|  | a new license for such software, you may create and use a modified version | ||||||
|  | of this License if you rename the license and remove any references to the | ||||||
|  | name of the license steward (except to note that such modified license differs | ||||||
|  | from this License). | ||||||
|  | 
 | ||||||
|  | 10.4. Distributing Source Code Form that is Incompatible With Secondary Licenses | ||||||
|  | 
 | ||||||
|  | If You choose to distribute Source Code Form that is Incompatible With Secondary | ||||||
|  | Licenses under the terms of this version of the License, the notice described | ||||||
|  | in Exhibit B of this License must be attached. Exhibit A - Source Code Form | ||||||
|  | License Notice | ||||||
|  | 
 | ||||||
|  | This Source Code Form is subject to the terms of the Mozilla Public License, | ||||||
|  | v. 2.0. If a copy of the MPL was not distributed with this file, You can obtain | ||||||
|  | one at http://mozilla.org/MPL/2.0/. | ||||||
|  | 
 | ||||||
|  | If it is not possible or desirable to put the notice in a particular file, | ||||||
|  | then You may include the notice in a location (such as a LICENSE file in a | ||||||
|  | relevant directory) where a recipient would be likely to look for such a notice. | ||||||
|  | 
 | ||||||
|  | You may add additional accurate notices of copyright ownership. | ||||||
|  | 
 | ||||||
|  | Exhibit B - "Incompatible With Secondary Licenses" Notice | ||||||
|  | 
 | ||||||
|  | This Source Code Form is "Incompatible With Secondary Licenses", as defined | ||||||
|  | by the Mozilla Public License, v. 2.0. | ||||||
							
								
								
									
										41
									
								
								vendor/git.rootprojects.org/root/hashcash/README.md
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										41
									
								
								vendor/git.rootprojects.org/root/hashcash/README.md
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							| @ -0,0 +1,41 @@ | |||||||
|  | # hashcash | ||||||
|  | 
 | ||||||
|  | HTTP Hashcash implemented in Go. | ||||||
|  | 
 | ||||||
|  | Explanation at https://therootcompany.com/blog/http-hashcash/ | ||||||
|  | 
 | ||||||
|  | Go docs at https://godoc.org/git.rootprojects.org/root/hashcash | ||||||
|  | 
 | ||||||
|  | # CLI Usage | ||||||
|  | 
 | ||||||
|  | Install: | ||||||
|  | 
 | ||||||
|  | ```bash | ||||||
|  | go get git.rootprojects.org/root/hashcash/cmd/hashcash | ||||||
|  | ``` | ||||||
|  | 
 | ||||||
|  | Usage: | ||||||
|  | 
 | ||||||
|  | ```txt | ||||||
|  | Usage: | ||||||
|  | 	hashcash new [subject *] [expires in 5m] [difficulty 10] | ||||||
|  | 	hashcash parse <hashcash> | ||||||
|  | 	hashcash solve <hashcash> | ||||||
|  | 	hashcash verify <hashcash> [subject *] | ||||||
|  | ``` | ||||||
|  | 
 | ||||||
|  | Example: | ||||||
|  | 
 | ||||||
|  | ```bash | ||||||
|  | my_hc=$(hashcash new) | ||||||
|  | echo New: $my_hc | ||||||
|  | hashcash parse "$my_hc" | ||||||
|  | echo "" | ||||||
|  | 
 | ||||||
|  | my_hc=$(hashcash solve "$my_hc") | ||||||
|  | echo Solved: $my_hc | ||||||
|  | hashcash parse "$my_hc" | ||||||
|  | echo "" | ||||||
|  | 
 | ||||||
|  | hashcash verify "$my_hc" | ||||||
|  | ``` | ||||||
							
								
								
									
										3
									
								
								vendor/git.rootprojects.org/root/hashcash/go.mod
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								vendor/git.rootprojects.org/root/hashcash/go.mod
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							| @ -0,0 +1,3 @@ | |||||||
|  | module git.rootprojects.org/root/hashcash | ||||||
|  | 
 | ||||||
|  | go 1.15 | ||||||
							
								
								
									
										284
									
								
								vendor/git.rootprojects.org/root/hashcash/hashcash.go
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										284
									
								
								vendor/git.rootprojects.org/root/hashcash/hashcash.go
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							| @ -0,0 +1,284 @@ | |||||||
|  | package hashcash | ||||||
|  | 
 | ||||||
|  | import ( | ||||||
|  | 	"crypto/rand" | ||||||
|  | 	"crypto/sha256" | ||||||
|  | 	"encoding/base64" | ||||||
|  | 	"encoding/binary" | ||||||
|  | 	"errors" | ||||||
|  | 	"strconv" | ||||||
|  | 	"strings" | ||||||
|  | 	"time" | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | // ErrParse is returned when fewer than 6 or more than 7 segments are split | ||||||
|  | var ErrParse = errors.New("could not split the hashcash parts") | ||||||
|  | 
 | ||||||
|  | // ErrInvalidTag is returned when the Hashcash version is unsupported | ||||||
|  | var ErrInvalidTag = errors.New("expected tag to be 'H'") | ||||||
|  | 
 | ||||||
|  | // ErrInvalidDifficulty is returned when the difficulty is outside of the acceptable range | ||||||
|  | var ErrInvalidDifficulty = errors.New("the number of bits of difficulty is too low or too high") | ||||||
|  | 
 | ||||||
|  | // ErrInvalidDate is returned when the date cannot be parsed as a positive int64 | ||||||
|  | var ErrInvalidDate = errors.New("invalid date") | ||||||
|  | 
 | ||||||
|  | // ErrExpired is returned when the current time is past that of ExpiresAt | ||||||
|  | var ErrExpired = errors.New("expired hashcash") | ||||||
|  | 
 | ||||||
|  | // ErrInvalidSubject is returned when the subject is invalid or does not match that passed to Verify() | ||||||
|  | var ErrInvalidSubject = errors.New("the subject is invalid or rejected") | ||||||
|  | 
 | ||||||
|  | // ErrInvalidNonce is returned when the nonce | ||||||
|  | //var ErrInvalidNonce = errors.New("the nonce has been used or is invalid") | ||||||
|  | 
 | ||||||
|  | // ErrUnsupportedAlgorithm is returned when the given algorithm is not supported | ||||||
|  | var ErrUnsupportedAlgorithm = errors.New("the given algorithm is invalid or not supported") | ||||||
|  | 
 | ||||||
|  | // ErrInvalidSolution is returned when the given hashcash is not properly solved | ||||||
|  | var ErrInvalidSolution = errors.New("the given solution is not valid") | ||||||
|  | 
 | ||||||
|  | // MaxDifficulty is the upper bound for all Solve() operations | ||||||
|  | var MaxDifficulty = 26 | ||||||
|  | 
 | ||||||
|  | // Sep is the separator character to use | ||||||
|  | var Sep = ":" | ||||||
|  | 
 | ||||||
|  | // no milliseconds | ||||||
|  | //var isoTS = "2006-01-02T15:04:05Z" | ||||||
|  | 
 | ||||||
|  | // Hashcash represents a parsed Hashcash string | ||||||
|  | type Hashcash struct { | ||||||
|  | 	Tag        string    `json:"tag"`        // Always "H" for "HTTP" | ||||||
|  | 	Difficulty int       `json:"difficulty"` // Number of "partial pre-image" (zero) bits in the hashed code | ||||||
|  | 	ExpiresAt  time.Time `json:"exp"`        // The timestamp that the hashcash expires, as seconds since the Unix epoch | ||||||
|  | 	Subject    string    `json:"sub"`        // Resource data string being transmitted, e.g., a domain or URL | ||||||
|  | 	Nonce      string    `json:"nonce"`      // Unique string of random characters, encoded as url-safe base-64 | ||||||
|  | 	Alg        string    `json:"alg"`        // always SHA-256 for now | ||||||
|  | 	Solution   string    `json:"solution"`   // Binary counter, encoded as url-safe base-64 | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // New returns a Hashcash with reasonable defaults | ||||||
|  | func New(h Hashcash) *Hashcash { | ||||||
|  | 	h.Tag = "H" | ||||||
|  | 
 | ||||||
|  | 	if 0 == h.Difficulty { | ||||||
|  | 		// safe for WebCrypto | ||||||
|  | 		h.Difficulty = 10 | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	if h.ExpiresAt.IsZero() { | ||||||
|  | 		h.ExpiresAt = time.Now().Add(5 * time.Minute) | ||||||
|  | 	} | ||||||
|  | 	h.ExpiresAt = h.ExpiresAt.UTC().Truncate(time.Second) | ||||||
|  | 
 | ||||||
|  | 	if "" == h.Subject { | ||||||
|  | 		h.Subject = "*" | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	if "" == h.Nonce { | ||||||
|  | 		nonce := make([]byte, 16) | ||||||
|  | 		if _, err := rand.Read(nonce); nil != err { | ||||||
|  | 			panic(err) | ||||||
|  | 			return nil | ||||||
|  | 		} | ||||||
|  | 		h.Nonce = base64.RawURLEncoding.EncodeToString(nonce) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	if "" == h.Alg { | ||||||
|  | 		h.Alg = "SHA-256" | ||||||
|  | 	} | ||||||
|  | 	/* | ||||||
|  | 		if "SHA-256" != h.Alg { | ||||||
|  | 			// TODO error | ||||||
|  | 		} | ||||||
|  | 	*/ | ||||||
|  | 
 | ||||||
|  | 	return &h | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // Parse will (obviously) parse the hashcash string, without verifying any | ||||||
|  | // of the parameters. | ||||||
|  | func Parse(hc string) (*Hashcash, error) { | ||||||
|  | 	parts := strings.Split(hc, Sep) | ||||||
|  | 	n := len(parts) | ||||||
|  | 	if n < 6 || n > 7 { | ||||||
|  | 		return nil, ErrParse | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	tag := parts[0] | ||||||
|  | 	if "H" != tag { | ||||||
|  | 		return nil, ErrInvalidTag | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	bits, err := strconv.Atoi(parts[1]) | ||||||
|  | 	if nil != err || bits < 0 { | ||||||
|  | 		return nil, ErrInvalidDifficulty | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	// Allow empty ExpiresAt | ||||||
|  | 	var exp time.Time | ||||||
|  | 	if "" != parts[2] { | ||||||
|  | 		expAt, err := strconv.ParseInt(parts[2], 10, 64) | ||||||
|  | 		if nil != err || expAt < 0 { | ||||||
|  | 			return nil, ErrInvalidDate | ||||||
|  | 		} | ||||||
|  | 		exp = time.Unix(int64(expAt), 0).UTC() | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	/* | ||||||
|  | 		exp, err := time.ParseInLocation(isoTS, parts[2], time.UTC) | ||||||
|  | 		if nil != err { | ||||||
|  | 			return nil, ErrInvalidDate | ||||||
|  | 		} | ||||||
|  | 	*/ | ||||||
|  | 
 | ||||||
|  | 	sub := parts[3] | ||||||
|  | 
 | ||||||
|  | 	nonce := parts[4] | ||||||
|  | 
 | ||||||
|  | 	alg := parts[5] | ||||||
|  | 
 | ||||||
|  | 	var solution string | ||||||
|  | 	if n > 6 { | ||||||
|  | 		solution = parts[6] | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	h := &Hashcash{ | ||||||
|  | 		Tag:        tag, | ||||||
|  | 		Difficulty: bits, | ||||||
|  | 		ExpiresAt:  exp.UTC().Truncate(time.Second), | ||||||
|  | 		Subject:    sub, | ||||||
|  | 		Nonce:      nonce, | ||||||
|  | 		Alg:        alg, | ||||||
|  | 		Solution:   solution, | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	return h, nil | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // String will return the formatted Hashcash, omitting the solution if it has not be solved. | ||||||
|  | func (h *Hashcash) String() string { | ||||||
|  | 	var solution string | ||||||
|  | 	if "" != h.Solution { | ||||||
|  | 		solution = Sep + h.Solution | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	var expAt string | ||||||
|  | 	if !h.ExpiresAt.IsZero() { | ||||||
|  | 		expAt = strconv.FormatInt(h.ExpiresAt.UTC().Truncate(time.Second).Unix(), 10) | ||||||
|  | 	} | ||||||
|  | 	return strings.Join( | ||||||
|  | 		[]string{ | ||||||
|  | 			"H", | ||||||
|  | 			strconv.Itoa(h.Difficulty), | ||||||
|  | 			//h.ExpiresAt.UTC().Format(isoTS), | ||||||
|  | 			expAt, | ||||||
|  | 			h.Subject, | ||||||
|  | 			h.Nonce, | ||||||
|  | 			h.Alg, | ||||||
|  | 		}, | ||||||
|  | 		Sep, | ||||||
|  | 	) + solution | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // Verify the Hashcash based on Difficulty, Algorithm, ExpiresAt, Subject and, | ||||||
|  | // of course, the Solution and hash. | ||||||
|  | func (h *Hashcash) Verify(subject string) error { | ||||||
|  | 	if h.Difficulty < 0 { | ||||||
|  | 		return ErrInvalidDifficulty | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	if "SHA-256" != h.Alg { | ||||||
|  | 		return ErrUnsupportedAlgorithm | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	if !h.ExpiresAt.IsZero() && h.ExpiresAt.Sub(time.Now()) < 0 { | ||||||
|  | 		return ErrExpired | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	if subject != h.Subject { | ||||||
|  | 		return ErrInvalidSubject | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	bits := h.Difficulty | ||||||
|  | 	hash := sha256.Sum256([]byte(h.String())) | ||||||
|  | 	n := bits / 8 // 10 / 8 = 1 | ||||||
|  | 	m := bits % 8 // 10 % 8 = 2 | ||||||
|  | 	if m > 0 { | ||||||
|  | 		n++ // 10 bits = 2 bytes | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	if !verifyBits(hash[:n], bits, n) { | ||||||
|  | 		return ErrInvalidSolution | ||||||
|  | 	} | ||||||
|  | 	return nil | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func verifyBits(hash []byte, bits, n int) bool { | ||||||
|  | 	for i := 0; i < n; i++ { | ||||||
|  | 		if bits > 8 { | ||||||
|  | 			bits -= 8 | ||||||
|  | 			if 0 != hash[i] { | ||||||
|  | 				return false | ||||||
|  | 			} | ||||||
|  | 			continue | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		// (bits % 8) == bits | ||||||
|  | 		pad := 8 - bits | ||||||
|  | 		if 0 != hash[i]>>pad { | ||||||
|  | 			return false | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		return true | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	// 0 == bits | ||||||
|  | 	return true | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // Solve will search for a solution, returning an error if the difficulty is | ||||||
|  | // above the local or global MaxDifficulty, the Algorithm is unsupported. | ||||||
|  | func (h *Hashcash) Solve(maxDifficulty int) error { | ||||||
|  | 	if "SHA-256" != h.Alg { | ||||||
|  | 		return ErrUnsupportedAlgorithm | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	if h.Difficulty < 0 { | ||||||
|  | 		return ErrInvalidDifficulty | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	if h.Difficulty > maxDifficulty || h.Difficulty > MaxDifficulty { | ||||||
|  | 		return ErrInvalidDifficulty | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	if "" != h.Solution { | ||||||
|  | 		if nil == h.Verify(h.Subject) { | ||||||
|  | 			return nil | ||||||
|  | 		} | ||||||
|  | 		h.Solution = "" | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	hashcash := h.String() | ||||||
|  | 	bits := h.Difficulty | ||||||
|  | 	n := bits / 8 // 10 / 8 = 1 | ||||||
|  | 	m := bits % 8 // 10 % 8 = 2 | ||||||
|  | 	if m > 0 { | ||||||
|  | 		n++ // 10 bits = 2 bytes | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	var solution uint32 = 0 | ||||||
|  | 	sb := make([]byte, 4) | ||||||
|  | 	for { | ||||||
|  | 		// Note: it's not actually important what method of change or encoding is used | ||||||
|  | 		// but incrementing by 1 on an int32 is good enough, and makes for a small base64 encoding | ||||||
|  | 		binary.LittleEndian.PutUint32(sb, solution) | ||||||
|  | 		h.Solution = base64.RawURLEncoding.EncodeToString(sb) | ||||||
|  | 		hash := sha256.Sum256([]byte(hashcash + Sep + h.Solution)) | ||||||
|  | 		if verifyBits(hash[:n], bits, n) { | ||||||
|  | 			return nil | ||||||
|  | 		} | ||||||
|  | 		solution++ | ||||||
|  | 	} | ||||||
|  | } | ||||||
							
								
								
									
										5
									
								
								vendor/git.rootprojects.org/root/keypairs/.gitignore
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								vendor/git.rootprojects.org/root/keypairs/.gitignore
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							| @ -0,0 +1,5 @@ | |||||||
|  | /keypairs | ||||||
|  | /dist/ | ||||||
|  | 
 | ||||||
|  | .DS_Store | ||||||
|  | .*.sw* | ||||||
							
								
								
									
										41
									
								
								vendor/git.rootprojects.org/root/keypairs/.goreleaser.yml
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										41
									
								
								vendor/git.rootprojects.org/root/keypairs/.goreleaser.yml
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							| @ -0,0 +1,41 @@ | |||||||
|  | # This is an example goreleaser.yaml file with some sane defaults. | ||||||
|  | # Make sure to check the documentation at http://goreleaser.com | ||||||
|  | before: | ||||||
|  |   hooks: | ||||||
|  |     - go generate ./... | ||||||
|  | builds: | ||||||
|  |   - id: keypairs | ||||||
|  |     main: ./cmd/keypairs/keypairs.go | ||||||
|  |     env: | ||||||
|  |       - CGO_ENABLED=0 | ||||||
|  |     flags: | ||||||
|  |       - -mod=vendor | ||||||
|  |     goos: | ||||||
|  |       - linux | ||||||
|  |       - windows | ||||||
|  |       - darwin | ||||||
|  |       - freebsd | ||||||
|  |     goarch: | ||||||
|  |       - amd64 | ||||||
|  |       - arm | ||||||
|  |       - arm64 | ||||||
|  | archives: | ||||||
|  |   - replacements: | ||||||
|  |       386: i386 | ||||||
|  |       amd64: x86-64 | ||||||
|  |       arm64: aarch64 | ||||||
|  |     format_overrides: | ||||||
|  |       - goos: windows | ||||||
|  |         format: zip | ||||||
|  | env_files: | ||||||
|  |   github_token: ~/.config/goreleaser/github_token.txt | ||||||
|  | checksum: | ||||||
|  |   name_template: 'checksums.txt' | ||||||
|  | snapshot: | ||||||
|  |   name_template: "{{ .Tag }}-next" | ||||||
|  | changelog: | ||||||
|  |   sort: asc | ||||||
|  |   filters: | ||||||
|  |     exclude: | ||||||
|  |       - '^docs:' | ||||||
|  |       - '^test:' | ||||||
							
								
								
									
										1
									
								
								vendor/git.rootprojects.org/root/keypairs/AUTHORS
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								vendor/git.rootprojects.org/root/keypairs/AUTHORS
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							| @ -0,0 +1 @@ | |||||||
|  | AJ ONeal <aj@therootcompany.com> (https://therootcompany.com) | ||||||
							
								
								
									
										21
									
								
								vendor/git.rootprojects.org/root/keypairs/LICENSE
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								vendor/git.rootprojects.org/root/keypairs/LICENSE
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							| @ -0,0 +1,21 @@ | |||||||
|  | The MIT License | ||||||
|  | 
 | ||||||
|  | Copyright (c) 2018-2019 Big Squid, Inc | ||||||
|  | 
 | ||||||
|  | Permission is hereby granted, free of charge, to any person obtaining a copy | ||||||
|  | of this software and associated documentation files (the "Software"), to deal | ||||||
|  | in the Software without restriction, including without limitation the rights | ||||||
|  | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | ||||||
|  | copies of the Software, and to permit persons to whom the Software is | ||||||
|  | furnished to do so, subject to the following conditions: | ||||||
|  | 
 | ||||||
|  | The above copyright notice and this permission notice shall be included in all | ||||||
|  | copies or substantial portions of the Software. | ||||||
|  | 
 | ||||||
|  | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | ||||||
|  | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | ||||||
|  | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | ||||||
|  | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | ||||||
|  | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | ||||||
|  | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | ||||||
|  | SOFTWARE. | ||||||
							
								
								
									
										63
									
								
								vendor/git.rootprojects.org/root/keypairs/README.md
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										63
									
								
								vendor/git.rootprojects.org/root/keypairs/README.md
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							| @ -0,0 +1,63 @@ | |||||||
|  | # [keypairs](https://git.rootprojects.org/root/keypairs) | ||||||
|  | 
 | ||||||
|  | JSON Web Key (JWK) support and type safety lightly placed over top of Go's `crypto/ecdsa` and `crypto/rsa` | ||||||
|  | 
 | ||||||
|  | Useful for JWT, JOSE, etc. | ||||||
|  | 
 | ||||||
|  | ```go | ||||||
|  | key, err := keypairs.ParsePrivateKey(bytesForJWKOrPEMOrDER) | ||||||
|  | 
 | ||||||
|  | pub, err := keypairs.ParsePublicKey(bytesForJWKOrPEMOrDER) | ||||||
|  | 
 | ||||||
|  | jwk, err := keypairs.MarshalJWKPublicKey(pub, time.Now().Add(2 * time.Day)) | ||||||
|  | 
 | ||||||
|  | kid, err := keypairs.ThumbprintPublicKey(pub) | ||||||
|  | ``` | ||||||
|  | 
 | ||||||
|  | # GoDoc API Documentation | ||||||
|  | 
 | ||||||
|  | See <https://pkg.go.dev/git.rootprojects.org/root/keypairs> | ||||||
|  | 
 | ||||||
|  | # Philosophy | ||||||
|  | 
 | ||||||
|  | Go's standard library is great. | ||||||
|  | 
 | ||||||
|  | Go has _excellent_ crytography support and provides wonderful | ||||||
|  | primitives for dealing with them. | ||||||
|  | 
 | ||||||
|  | I prefer to stay as close to Go's `crypto` package as possible, | ||||||
|  | just adding a light touch for JWT support and type safety. | ||||||
|  | 
 | ||||||
|  | # Type Safety | ||||||
|  | 
 | ||||||
|  | `crypto.PublicKey` is a "marker interface", meaning that it is **not typesafe**! | ||||||
|  | 
 | ||||||
|  | `go-keypairs` defines `type keypairs.PrivateKey interface { Public() crypto.PublicKey }`, | ||||||
|  | which is implemented by `crypto/rsa` and `crypto/ecdsa` | ||||||
|  | (but not `crypto/dsa`, which we really don't care that much about). | ||||||
|  | 
 | ||||||
|  | Go1.15 will add `[PublicKey.Equal(crypto.PublicKey)](https://github.com/golang/go/issues/21704)`, | ||||||
|  | which will make it possible to remove the additional wrapper over `PublicKey` | ||||||
|  | and use an interface instead. | ||||||
|  | 
 | ||||||
|  | Since there are no common methods between `rsa.PublicKey` and `ecdsa.PublicKey`, | ||||||
|  | go-keypairs lightly wraps each to implement `Thumbprint() string` (part of the JOSE/JWK spec). | ||||||
|  | 
 | ||||||
|  | ## JSON Web Key (JWK) as a "codec" | ||||||
|  | 
 | ||||||
|  | Although there are many, many ways that JWKs could be interpreted | ||||||
|  | (possibly why they haven't made it into the standard library), `go-keypairs` | ||||||
|  | follows the basic pattern of `encoding/x509` to `Parse` and `Marshal` | ||||||
|  | only the most basic and most meaningful parts of a key. | ||||||
|  | 
 | ||||||
|  | I highly recommend that you use `Thumbprint()` for `KeyID` you also | ||||||
|  | get the benefit of not losing information when encoding and decoding | ||||||
|  | between the ASN.1, x509, PEM, and JWK formats. | ||||||
|  | 
 | ||||||
|  | # LICENSE | ||||||
|  | 
 | ||||||
|  | Copyright (c) 2020-present AJ ONeal \ | ||||||
|  | Copyright (c) 2018-2019 Big Squid, Inc. | ||||||
|  | 
 | ||||||
|  | This work is licensed under the terms of the MIT license. \ | ||||||
|  | For a copy, see <https://opensource.org/licenses/MIT>. | ||||||
							
								
								
									
										19
									
								
								vendor/git.rootprojects.org/root/keypairs/cli_test.sh
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								vendor/git.rootprojects.org/root/keypairs/cli_test.sh
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							| @ -0,0 +1,19 @@ | |||||||
|  | #!/bin/bash | ||||||
|  | set -u | ||||||
|  | 
 | ||||||
|  | go build -mod=vendor cmd/keypairs/*.go | ||||||
|  | ./keypairs gen > testkey.jwk.json 2> testpub.jwk.json | ||||||
|  | 
 | ||||||
|  | ./keypairs sign --exp 1h ./testkey.jwk.json '{"foo":"bar"}' > testjwt.txt 2> testjws.json | ||||||
|  | 
 | ||||||
|  | echo "" | ||||||
|  | echo "Should pass:" | ||||||
|  | ./keypairs verify ./testpub.jwk.json testjwt.txt > /dev/null | ||||||
|  | ./keypairs verify ./testpub.jwk.json "$(cat testjwt.txt)" > /dev/null | ||||||
|  | ./keypairs verify ./testpub.jwk.json testjws.json > /dev/null | ||||||
|  | ./keypairs verify ./testpub.jwk.json "$(cat testjws.json)" > /dev/null | ||||||
|  | 
 | ||||||
|  | echo "" | ||||||
|  | echo "Should fail:" | ||||||
|  | ./keypairs sign --exp -1m ./testkey.jwk.json '{"bar":"foo"}' > errjwt.txt 2> errjws.json | ||||||
|  | ./keypairs verify ./testpub.jwk.json errjwt.txt > /dev/null | ||||||
							
								
								
									
										40
									
								
								vendor/git.rootprojects.org/root/keypairs/doc.go
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										40
									
								
								vendor/git.rootprojects.org/root/keypairs/doc.go
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							| @ -0,0 +1,40 @@ | |||||||
|  | /* | ||||||
|  | Package keypairs complements Go's standard keypair-related packages | ||||||
|  | (encoding/pem, crypto/x509, crypto/rsa, crypto/ecdsa, crypto/elliptic) | ||||||
|  | with JWK encoding support and typesafe PrivateKey and PublicKey interfaces. | ||||||
|  | 
 | ||||||
|  | Basics | ||||||
|  | 
 | ||||||
|  | 	key, err := keypairs.ParsePrivateKey(bytesForJWKOrPEMOrDER) | ||||||
|  | 
 | ||||||
|  | 	pub, err := keypairs.ParsePublicKey(bytesForJWKOrPEMOrDER) | ||||||
|  | 
 | ||||||
|  | 	jwk, err := keypairs.MarshalJWKPublicKey(pub, time.Now().Add(2 * time.Day)) | ||||||
|  | 
 | ||||||
|  | 	kid, err := keypairs.ThumbprintPublicKey(pub) | ||||||
|  | 
 | ||||||
|  | Convenience functions are available which will fetch keys | ||||||
|  | (or retrieve them from cache) via OIDC, .well-known/jwks.json, and direct urls. | ||||||
|  | All keys are cached by Thumbprint, as well as kid(@issuer), if available. | ||||||
|  | 
 | ||||||
|  | 	import "git.rootprojects.org/root/keypairs/keyfetch" | ||||||
|  | 
 | ||||||
|  | 	pubs, err := keyfetch.OIDCJWKs("https://example.com/") | ||||||
|  | 	pubs, err := keyfetch.OIDCJWK(ThumbOrKeyID, "https://example.com/") | ||||||
|  | 
 | ||||||
|  | 	pubs, err := keyfetch.WellKnownJWKs("https://example.com/") | ||||||
|  | 	pubs, err := keyfetch.WellKnownJWK(ThumbOrKeyID, "https://example.com/") | ||||||
|  | 
 | ||||||
|  | 	pubs, err := keyfetch.JWKs("https://example.com/path/to/jwks/") | ||||||
|  | 	pubs, err := keyfetch.JWK(ThumbOrKeyID, "https://example.com/path/to/jwks/") | ||||||
|  | 
 | ||||||
|  | 	// From URL | ||||||
|  | 	pub, err := keyfetch.Fetch("https://example.com/jwk.json") | ||||||
|  | 
 | ||||||
|  | 	// From Cache only | ||||||
|  | 	pub := keyfetch.Get(thumbprint, "https://example.com/jwk.json") | ||||||
|  | 
 | ||||||
|  | A non-caching version with the same capabilities is also available. | ||||||
|  | 
 | ||||||
|  | */ | ||||||
|  | package keypairs | ||||||
							
								
								
									
										69
									
								
								vendor/git.rootprojects.org/root/keypairs/generate.go
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										69
									
								
								vendor/git.rootprojects.org/root/keypairs/generate.go
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							| @ -0,0 +1,69 @@ | |||||||
|  | package keypairs | ||||||
|  | 
 | ||||||
|  | import ( | ||||||
|  | 	"crypto/ecdsa" | ||||||
|  | 	"crypto/elliptic" | ||||||
|  | 	"crypto/rand" | ||||||
|  | 	"crypto/rsa" | ||||||
|  | 	"io" | ||||||
|  | 	mathrand "math/rand" | ||||||
|  | 	"time" | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | var randReader io.Reader = rand.Reader | ||||||
|  | var allowMocking = false | ||||||
|  | 
 | ||||||
|  | // KeyOptions are the things that we may need to know about a request to fulfill it properly | ||||||
|  | type keyOptions struct { | ||||||
|  | 	//Key     string `json:"key"` | ||||||
|  | 	KeyType  string `json:"kty"` | ||||||
|  | 	mockSeed int64  //`json:"-"` | ||||||
|  | 	//SeedStr string `json:"seed"` | ||||||
|  | 	//Claims  Object `json:"claims"` | ||||||
|  | 	//Header  Object `json:"header"` | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (o *keyOptions) nextReader() io.Reader { | ||||||
|  | 	if allowMocking { | ||||||
|  | 		return o.maybeMockReader() | ||||||
|  | 	} | ||||||
|  | 	return randReader | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // NewDefaultPrivateKey generates a key with reasonable strength. | ||||||
|  | // Today that means a 256-bit equivalent - either RSA 2048 or EC P-256. | ||||||
|  | func NewDefaultPrivateKey() PrivateKey { | ||||||
|  | 	// insecure random is okay here, | ||||||
|  | 	// it's just used for a coin toss | ||||||
|  | 	mathrand.Seed(time.Now().UnixNano()) | ||||||
|  | 	coin := mathrand.Int() | ||||||
|  | 
 | ||||||
|  | 	// the idea here is that we want to make | ||||||
|  | 	// it dead simple to support RSA and EC | ||||||
|  | 	// so it shouldn't matter which is used | ||||||
|  | 	if 0 == coin%2 { | ||||||
|  | 		return newPrivateKey(&keyOptions{ | ||||||
|  | 			KeyType: "RSA", | ||||||
|  | 		}) | ||||||
|  | 	} | ||||||
|  | 	return newPrivateKey(&keyOptions{ | ||||||
|  | 		KeyType: "EC", | ||||||
|  | 	}) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // newPrivateKey generates a 256-bit entropy RSA or ECDSA private key | ||||||
|  | func newPrivateKey(opts *keyOptions) PrivateKey { | ||||||
|  | 	var privkey PrivateKey | ||||||
|  | 
 | ||||||
|  | 	if "RSA" == opts.KeyType { | ||||||
|  | 		keylen := 2048 | ||||||
|  | 		privkey, _ = rsa.GenerateKey(opts.nextReader(), keylen) | ||||||
|  | 		if allowMocking { | ||||||
|  | 			privkey = maybeDerandomizeMockKey(privkey, keylen, opts) | ||||||
|  | 		} | ||||||
|  | 	} else { | ||||||
|  | 		// TODO: EC keys may also suffer the same random problems in the future | ||||||
|  | 		privkey, _ = ecdsa.GenerateKey(elliptic.P256(), opts.nextReader()) | ||||||
|  | 	} | ||||||
|  | 	return privkey | ||||||
|  | } | ||||||
							
								
								
									
										3
									
								
								vendor/git.rootprojects.org/root/keypairs/go.mod
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								vendor/git.rootprojects.org/root/keypairs/go.mod
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							| @ -0,0 +1,3 @@ | |||||||
|  | module git.rootprojects.org/root/keypairs | ||||||
|  | 
 | ||||||
|  | go 1.12 | ||||||
							
								
								
									
										69
									
								
								vendor/git.rootprojects.org/root/keypairs/jwk.go
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										69
									
								
								vendor/git.rootprojects.org/root/keypairs/jwk.go
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							| @ -0,0 +1,69 @@ | |||||||
|  | package keypairs | ||||||
|  | 
 | ||||||
|  | import ( | ||||||
|  | 	"fmt" | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | // JWK abstracts EC and RSA keys | ||||||
|  | type JWK interface { | ||||||
|  | 	marshalJWK() ([]byte, error) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // ECJWK is the EC variant | ||||||
|  | type ECJWK struct { | ||||||
|  | 	KeyID string   `json:"kid,omitempty"` | ||||||
|  | 	Curve string   `json:"crv"` | ||||||
|  | 	X     string   `json:"x"` | ||||||
|  | 	Y     string   `json:"y"` | ||||||
|  | 	Use   []string `json:"use,omitempty"` | ||||||
|  | 	Seed  string   `json:"_seed,omitempty"` | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (k *ECJWK) marshalJWK() ([]byte, error) { | ||||||
|  | 	return []byte(fmt.Sprintf(`{"crv":%q,"kty":"EC","x":%q,"y":%q}`, k.Curve, k.X, k.Y)), nil | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // RSAJWK is the RSA variant | ||||||
|  | type RSAJWK struct { | ||||||
|  | 	KeyID string   `json:"kid,omitempty"` | ||||||
|  | 	Exp   string   `json:"e"` | ||||||
|  | 	N     string   `json:"n"` | ||||||
|  | 	Use   []string `json:"use,omitempty"` | ||||||
|  | 	Seed  string   `json:"_seed,omitempty"` | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (k *RSAJWK) marshalJWK() ([]byte, error) { | ||||||
|  | 	return []byte(fmt.Sprintf(`{"e":%q,"kty":"RSA","n":%q}`, k.Exp, k.N)), nil | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | /* | ||||||
|  | // ToPublicJWK exposes only the public parts | ||||||
|  | func ToPublicJWK(pubkey PublicKey) JWK { | ||||||
|  | 	switch k := pubkey.Key().(type) { | ||||||
|  | 	case *ecdsa.PublicKey: | ||||||
|  | 		return ECToPublicJWK(k) | ||||||
|  | 	case *rsa.PublicKey: | ||||||
|  | 		return RSAToPublicJWK(k) | ||||||
|  | 	default: | ||||||
|  | 		panic(errors.New("impossible key type")) | ||||||
|  | 		//return nil | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // ECToPublicJWK will output the most minimal version of an EC JWK (no key id, no "use" flag, nada) | ||||||
|  | func ECToPublicJWK(k *ecdsa.PublicKey) *ECJWK { | ||||||
|  | 	return &ECJWK{ | ||||||
|  | 		Curve: k.Curve.Params().Name, | ||||||
|  | 		X:     base64.RawURLEncoding.EncodeToString(k.X.Bytes()), | ||||||
|  | 		Y:     base64.RawURLEncoding.EncodeToString(k.Y.Bytes()), | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // RSAToPublicJWK will output the most minimal version of an RSA JWK (no key id, no "use" flag, nada) | ||||||
|  | func RSAToPublicJWK(p *rsa.PublicKey) *RSAJWK { | ||||||
|  | 	return &RSAJWK{ | ||||||
|  | 		Exp: base64.RawURLEncoding.EncodeToString(big.NewInt(int64(p.E)).Bytes()), | ||||||
|  | 		N:   base64.RawURLEncoding.EncodeToString(p.N.Bytes()), | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | */ | ||||||
							
								
								
									
										63
									
								
								vendor/git.rootprojects.org/root/keypairs/jws.go
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										63
									
								
								vendor/git.rootprojects.org/root/keypairs/jws.go
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							| @ -0,0 +1,63 @@ | |||||||
|  | package keypairs | ||||||
|  | 
 | ||||||
|  | import ( | ||||||
|  | 	"encoding/base64" | ||||||
|  | 	"encoding/json" | ||||||
|  | 	"errors" | ||||||
|  | 	"fmt" | ||||||
|  | 	"strings" | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | // JWS is a parsed JWT, representation as signable/verifiable and human-readable parts | ||||||
|  | type JWS struct { | ||||||
|  | 	Header    Object `json:"header"`    // JSON | ||||||
|  | 	Claims    Object `json:"claims"`    // JSON | ||||||
|  | 	Protected string `json:"protected"` // base64 | ||||||
|  | 	Payload   string `json:"payload"`   // base64 | ||||||
|  | 	Signature string `json:"signature"` // base64 | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // JWSToJWT joins JWS parts into a JWT as {ProtectedHeader}.{SerializedPayload}.{Signature}. | ||||||
|  | func JWSToJWT(jwt *JWS) string { | ||||||
|  | 	return fmt.Sprintf( | ||||||
|  | 		"%s.%s.%s", | ||||||
|  | 		jwt.Protected, | ||||||
|  | 		jwt.Payload, | ||||||
|  | 		jwt.Signature, | ||||||
|  | 	) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // JWTToJWS splits the JWT into its JWS segments | ||||||
|  | func JWTToJWS(jwt string) (jws *JWS) { | ||||||
|  | 	jwt = strings.TrimSpace(jwt) | ||||||
|  | 	parts := strings.Split(jwt, ".") | ||||||
|  | 	if 3 != len(parts) { | ||||||
|  | 		return nil | ||||||
|  | 	} | ||||||
|  | 	return &JWS{ | ||||||
|  | 		Protected: parts[0], | ||||||
|  | 		Payload:   parts[1], | ||||||
|  | 		Signature: parts[2], | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // DecodeComponents decodes JWS Header and Claims | ||||||
|  | func (jws *JWS) DecodeComponents() error { | ||||||
|  | 	protected, err := base64.RawURLEncoding.DecodeString(jws.Protected) | ||||||
|  | 	if nil != err { | ||||||
|  | 		return errors.New("invalid JWS header base64Url encoding") | ||||||
|  | 	} | ||||||
|  | 	if err := json.Unmarshal([]byte(protected), &jws.Header); nil != err { | ||||||
|  | 		return errors.New("invalid JWS header") | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	payload, err := base64.RawURLEncoding.DecodeString(jws.Payload) | ||||||
|  | 	if nil != err { | ||||||
|  | 		return errors.New("invalid JWS payload base64Url encoding") | ||||||
|  | 	} | ||||||
|  | 	if err := json.Unmarshal([]byte(payload), &jws.Claims); nil != err { | ||||||
|  | 		return errors.New("invalid JWS claims") | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	return nil | ||||||
|  | } | ||||||
							
								
								
									
										516
									
								
								vendor/git.rootprojects.org/root/keypairs/keyfetch/fetch.go
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										516
									
								
								vendor/git.rootprojects.org/root/keypairs/keyfetch/fetch.go
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							| @ -0,0 +1,516 @@ | |||||||
|  | // Package keyfetch retrieve and cache PublicKeys | ||||||
|  | // from OIDC (https://example.com/.well-known/openid-configuration) | ||||||
|  | // and Auth0 (https://example.com/.well-known/jwks.json) | ||||||
|  | // JWKs URLs and expires them when `exp` is reached | ||||||
|  | // (or a default expiry if the key does not provide one). | ||||||
|  | // It uses the keypairs package to Unmarshal the JWKs into their | ||||||
|  | // native types (with a very thin shim to provide the type safety | ||||||
|  | // that Go's crypto.PublicKey and crypto.PrivateKey interfaces lack). | ||||||
|  | package keyfetch | ||||||
|  | 
 | ||||||
|  | import ( | ||||||
|  | 	"errors" | ||||||
|  | 	"fmt" | ||||||
|  | 	"log" | ||||||
|  | 	"net/http" | ||||||
|  | 	"net/url" | ||||||
|  | 	"strconv" | ||||||
|  | 	"strings" | ||||||
|  | 	"sync" | ||||||
|  | 	"time" | ||||||
|  | 
 | ||||||
|  | 	"git.rootprojects.org/root/keypairs" | ||||||
|  | 	"git.rootprojects.org/root/keypairs/keyfetch/uncached" | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | // TODO should be ErrInvalidJWKURL | ||||||
|  | 
 | ||||||
|  | // EInvalidJWKURL means that the url did not provide JWKs | ||||||
|  | var EInvalidJWKURL = errors.New("url does not lead to valid JWKs") | ||||||
|  | 
 | ||||||
|  | // KeyCache is an in-memory key cache | ||||||
|  | var KeyCache = map[string]CachableKey{} | ||||||
|  | 
 | ||||||
|  | // KeyCacheMux is used to guard the in-memory cache | ||||||
|  | var KeyCacheMux = sync.Mutex{} | ||||||
|  | 
 | ||||||
|  | // ErrInsecureDomain means that plain http was used where https was expected | ||||||
|  | var ErrInsecureDomain = errors.New("Whitelists should only allow secure URLs (i.e. https://). To allow unsecured private networking (i.e. Docker) pass PrivateWhitelist as a list of private URLs") | ||||||
|  | 
 | ||||||
|  | // TODO Cacheable key (shouldn't this be private)? | ||||||
|  | 
 | ||||||
|  | // CachableKey represents | ||||||
|  | type CachableKey struct { | ||||||
|  | 	Key    keypairs.PublicKey | ||||||
|  | 	Expiry time.Time | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // maybe TODO use this poor-man's enum to allow kids thumbs to be accepted by the same method? | ||||||
|  | /* | ||||||
|  | type KeyID string | ||||||
|  | 
 | ||||||
|  | func (kid KeyID) ID() string { | ||||||
|  | 	return string(kid) | ||||||
|  | } | ||||||
|  | func (kid KeyID) isID() {} | ||||||
|  | 
 | ||||||
|  | type Thumbprint string | ||||||
|  | 
 | ||||||
|  | func (thumb Thumbprint) ID() string { | ||||||
|  | 	return string(thumb) | ||||||
|  | } | ||||||
|  | func (thumb Thumbprint) isID() {} | ||||||
|  | 
 | ||||||
|  | type ID interface { | ||||||
|  | 	ID() string | ||||||
|  | 	isID() | ||||||
|  | } | ||||||
|  | */ | ||||||
|  | 
 | ||||||
|  | // StaleTime defines when public keys should be renewed (15 minutes by default) | ||||||
|  | var StaleTime = 15 * time.Minute | ||||||
|  | 
 | ||||||
|  | // DefaultKeyDuration defines how long a key should be considered fresh (48 hours by default) | ||||||
|  | var DefaultKeyDuration = 48 * time.Hour | ||||||
|  | 
 | ||||||
|  | // MinimumKeyDuration defines the minimum time that a key will be cached (1 hour by default) | ||||||
|  | var MinimumKeyDuration = time.Hour | ||||||
|  | 
 | ||||||
|  | // MaximumKeyDuration defines the maximum time that a key will be cached (72 hours by default) | ||||||
|  | var MaximumKeyDuration = 72 * time.Hour | ||||||
|  | 
 | ||||||
|  | // PublicKeysMap is a newtype for a map of keypairs.PublicKey | ||||||
|  | type PublicKeysMap map[string]keypairs.PublicKey | ||||||
|  | 
 | ||||||
|  | // OIDCJWKs fetches baseURL + ".well-known/openid-configuration" and then fetches and returns the Public Keys. | ||||||
|  | func OIDCJWKs(baseURL string) (PublicKeysMap, error) { | ||||||
|  | 	maps, keys, err := uncached.OIDCJWKs(baseURL) | ||||||
|  | 
 | ||||||
|  | 	if nil != err { | ||||||
|  | 		return nil, err | ||||||
|  | 	} | ||||||
|  | 	cacheKeys(maps, keys, baseURL) | ||||||
|  | 	return keys, err | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // OIDCJWK fetches baseURL + ".well-known/openid-configuration" and then returns the key matching kid (or thumbprint) | ||||||
|  | func OIDCJWK(kidOrThumb, iss string) (keypairs.PublicKey, error) { | ||||||
|  | 	return immediateOneOrFetch(kidOrThumb, iss, uncached.OIDCJWKs) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // WellKnownJWKs fetches baseURL + ".well-known/jwks.json" and caches and returns the keys | ||||||
|  | func WellKnownJWKs(kidOrThumb, iss string) (PublicKeysMap, error) { | ||||||
|  | 	maps, keys, err := uncached.WellKnownJWKs(iss) | ||||||
|  | 
 | ||||||
|  | 	if nil != err { | ||||||
|  | 		return nil, err | ||||||
|  | 	} | ||||||
|  | 	cacheKeys(maps, keys, iss) | ||||||
|  | 	return keys, err | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // WellKnownJWK fetches baseURL + ".well-known/jwks.json" and returns the key matching kid (or thumbprint) | ||||||
|  | func WellKnownJWK(kidOrThumb, iss string) (keypairs.PublicKey, error) { | ||||||
|  | 	return immediateOneOrFetch(kidOrThumb, iss, uncached.WellKnownJWKs) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // JWKs returns a map of keys identified by their thumbprint | ||||||
|  | // (since kid may or may not be present) | ||||||
|  | func JWKs(jwksurl string) (PublicKeysMap, error) { | ||||||
|  | 	maps, keys, err := uncached.JWKs(jwksurl) | ||||||
|  | 
 | ||||||
|  | 	if nil != err { | ||||||
|  | 		return nil, err | ||||||
|  | 	} | ||||||
|  | 	iss := strings.Replace(jwksurl, ".well-known/jwks.json", "", 1) | ||||||
|  | 	cacheKeys(maps, keys, iss) | ||||||
|  | 	return keys, err | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // JWK tries to return a key from cache, falling back to the /.well-known/jwks.json of the issuer | ||||||
|  | func JWK(kidOrThumb, iss string) (keypairs.PublicKey, error) { | ||||||
|  | 	return immediateOneOrFetch(kidOrThumb, iss, uncached.JWKs) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // PEM tries to return a key from cache, falling back to the specified PEM url | ||||||
|  | func PEM(url string) (keypairs.PublicKey, error) { | ||||||
|  | 	// url is kid in this case | ||||||
|  | 	return immediateOneOrFetch(url, url, func(string) (map[string]map[string]string, map[string]keypairs.PublicKey, error) { | ||||||
|  | 		m, key, err := uncached.PEM(url) | ||||||
|  | 		if nil != err { | ||||||
|  | 			return nil, nil, err | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		// put in a map, just for caching | ||||||
|  | 		maps := map[string]map[string]string{} | ||||||
|  | 		maps[key.Thumbprint()] = m | ||||||
|  | 		maps[url] = m | ||||||
|  | 
 | ||||||
|  | 		keys := map[string]keypairs.PublicKey{} | ||||||
|  | 		keys[key.Thumbprint()] = key | ||||||
|  | 		keys[url] = key | ||||||
|  | 
 | ||||||
|  | 		return maps, keys, nil | ||||||
|  | 	}) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // Fetch returns a key from cache, falling back to an exact url as the "issuer" | ||||||
|  | func Fetch(url string) (keypairs.PublicKey, error) { | ||||||
|  | 	// url is kid in this case | ||||||
|  | 	return immediateOneOrFetch(url, url, func(string) (map[string]map[string]string, map[string]keypairs.PublicKey, error) { | ||||||
|  | 		m, key, err := uncached.Fetch(url) | ||||||
|  | 		if nil != err { | ||||||
|  | 			return nil, nil, err | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		// put in a map, just for caching | ||||||
|  | 		maps := map[string]map[string]string{} | ||||||
|  | 		maps[key.Thumbprint()] = m | ||||||
|  | 
 | ||||||
|  | 		keys := map[string]keypairs.PublicKey{} | ||||||
|  | 		keys[key.Thumbprint()] = key | ||||||
|  | 
 | ||||||
|  | 		return maps, keys, nil | ||||||
|  | 	}) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // Get retrieves a key from cache, or returns an error. | ||||||
|  | // The issuer string may be empty if using a thumbprint rather than a kid. | ||||||
|  | func Get(kidOrThumb, iss string) keypairs.PublicKey { | ||||||
|  | 	if pub := get(kidOrThumb, iss); nil != pub { | ||||||
|  | 		return pub.Key | ||||||
|  | 	} | ||||||
|  | 	return nil | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func get(kidOrThumb, iss string) *CachableKey { | ||||||
|  | 	iss = normalizeIssuer(iss) | ||||||
|  | 	KeyCacheMux.Lock() | ||||||
|  | 	defer KeyCacheMux.Unlock() | ||||||
|  | 
 | ||||||
|  | 	// we're safe to check the cache by kid alone | ||||||
|  | 	// by virtue that we never set it by kid alone | ||||||
|  | 	hit, ok := KeyCache[kidOrThumb] | ||||||
|  | 	if ok { | ||||||
|  | 		if now := time.Now(); hit.Expiry.Sub(now) > 0 { | ||||||
|  | 			// only return non-expired keys | ||||||
|  | 			return &hit | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	id := kidOrThumb + "@" + iss | ||||||
|  | 	hit, ok = KeyCache[id] | ||||||
|  | 	if ok { | ||||||
|  | 		if now := time.Now(); hit.Expiry.Sub(now) > 0 { | ||||||
|  | 			// only return non-expired keys | ||||||
|  | 			return &hit | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	return nil | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func immediateOneOrFetch(kidOrThumb, iss string, fetcher myfetcher) (keypairs.PublicKey, error) { | ||||||
|  | 	now := time.Now() | ||||||
|  | 	key := get(kidOrThumb, iss) | ||||||
|  | 
 | ||||||
|  | 	if nil == key { | ||||||
|  | 		return fetchAndSelect(kidOrThumb, iss, fetcher) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	// Fetch just a little before the key actually expires | ||||||
|  | 	if key.Expiry.Sub(now) <= StaleTime { | ||||||
|  | 		go fetchAndSelect(kidOrThumb, iss, fetcher) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	return key.Key, nil | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | type myfetcher func(string) (map[string]map[string]string, map[string]keypairs.PublicKey, error) | ||||||
|  | 
 | ||||||
|  | func fetchAndSelect(id, baseURL string, fetcher myfetcher) (keypairs.PublicKey, error) { | ||||||
|  | 	maps, keys, err := fetcher(baseURL) | ||||||
|  | 	if nil != err { | ||||||
|  | 		return nil, err | ||||||
|  | 	} | ||||||
|  | 	cacheKeys(maps, keys, baseURL) | ||||||
|  | 
 | ||||||
|  | 	for i := range keys { | ||||||
|  | 		key := keys[i] | ||||||
|  | 
 | ||||||
|  | 		if id == key.Thumbprint() { | ||||||
|  | 			return key, nil | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		if id == key.KeyID() { | ||||||
|  | 			return key, nil | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	return nil, fmt.Errorf("Key identified by '%s' was not found at %s", id, baseURL) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func cacheKeys(maps map[string]map[string]string, keys map[string]keypairs.PublicKey, issuer string) { | ||||||
|  | 	for i := range keys { | ||||||
|  | 		key := keys[i] | ||||||
|  | 		m := maps[i] | ||||||
|  | 		iss := issuer | ||||||
|  | 		if "" != m["iss"] { | ||||||
|  | 			iss = m["iss"] | ||||||
|  | 		} | ||||||
|  | 		iss = normalizeIssuer(iss) | ||||||
|  | 		cacheKey(m["kid"], iss, m["exp"], key) | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func cacheKey(kid, iss, expstr string, pub keypairs.PublicKey) error { | ||||||
|  | 	var expiry time.Time | ||||||
|  | 	iss = normalizeIssuer(iss) | ||||||
|  | 
 | ||||||
|  | 	exp, _ := strconv.ParseInt(expstr, 10, 64) | ||||||
|  | 	if 0 == exp { | ||||||
|  | 		// use default | ||||||
|  | 		expiry = time.Now().Add(DefaultKeyDuration) | ||||||
|  | 	} else if exp < time.Now().Add(MinimumKeyDuration).Unix() || exp > time.Now().Add(MaximumKeyDuration).Unix() { | ||||||
|  | 		// use at least one hour | ||||||
|  | 		expiry = time.Now().Add(MinimumKeyDuration) | ||||||
|  | 	} else { | ||||||
|  | 		expiry = time.Unix(exp, 0) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	KeyCacheMux.Lock() | ||||||
|  | 	defer KeyCacheMux.Unlock() | ||||||
|  | 	// Put the key in the cache by both kid and thumbprint, and set the expiry | ||||||
|  | 	id := kid + "@" + iss | ||||||
|  | 	KeyCache[id] = CachableKey{ | ||||||
|  | 		Key:    pub, | ||||||
|  | 		Expiry: expiry, | ||||||
|  | 	} | ||||||
|  | 	// Since thumbprints are crypto secure, iss isn't needed | ||||||
|  | 	thumb := pub.Thumbprint() | ||||||
|  | 	KeyCache[thumb] = CachableKey{ | ||||||
|  | 		Key:    pub, | ||||||
|  | 		Expiry: expiry, | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	return nil | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func clear() { | ||||||
|  | 	KeyCacheMux.Lock() | ||||||
|  | 	defer KeyCacheMux.Unlock() | ||||||
|  | 	KeyCache = map[string]CachableKey{} | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func normalizeIssuer(iss string) string { | ||||||
|  | 	return strings.TrimRight(iss, "/") | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func isTrustedIssuer(iss string, whitelist Whitelist, rs ...*http.Request) bool { | ||||||
|  | 	if "" == iss { | ||||||
|  | 		return false | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	// Normalize the http:// and https:// and parse | ||||||
|  | 	iss = strings.TrimRight(iss, "/") + "/" | ||||||
|  | 	if strings.HasPrefix(iss, "http://") { | ||||||
|  | 		// ignore | ||||||
|  | 	} else if strings.HasPrefix(iss, "//") { | ||||||
|  | 		return false // TODO | ||||||
|  | 	} else if !strings.HasPrefix(iss, "https://") { | ||||||
|  | 		iss = "https://" + iss | ||||||
|  | 	} | ||||||
|  | 	issURL, err := url.Parse(iss) | ||||||
|  | 	if nil != err { | ||||||
|  | 		return false | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	// Check that | ||||||
|  | 	// * schemes match (https: == https:) | ||||||
|  | 	// * paths match (/foo/ == /foo/, always with trailing slash added) | ||||||
|  | 	// * hostnames are compatible (a == b or "sub.foo.com".HasSufix(".foo.com")) | ||||||
|  | 	for i := range []*url.URL(whitelist) { | ||||||
|  | 		u := whitelist[i] | ||||||
|  | 
 | ||||||
|  | 		if issURL.Scheme != u.Scheme { | ||||||
|  | 			continue | ||||||
|  | 		} else if u.Path != strings.TrimRight(issURL.Path, "/")+"/" { | ||||||
|  | 			continue | ||||||
|  | 		} else if issURL.Host != u.Host { | ||||||
|  | 			if '.' == u.Host[0] && strings.HasSuffix(issURL.Host, u.Host) { | ||||||
|  | 				return true | ||||||
|  | 			} | ||||||
|  | 			continue | ||||||
|  | 		} | ||||||
|  | 		// All failures have been handled | ||||||
|  | 		return true | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	// Check if implicit issuer is available | ||||||
|  | 	if 0 == len(rs) { | ||||||
|  | 		return false | ||||||
|  | 	} | ||||||
|  | 	return hasImplicitTrust(issURL, rs[0]) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // hasImplicitTrust relies on the security of DNS and TLS to determine if the | ||||||
|  | // headers of the request can be trusted as identifying the server itself as | ||||||
|  | // a valid issuer, without additional configuration. | ||||||
|  | // | ||||||
|  | // Helpful for testing, but in the wrong hands could easily lead to a zero-day. | ||||||
|  | func hasImplicitTrust(issURL *url.URL, r *http.Request) bool { | ||||||
|  | 	if nil == r { | ||||||
|  | 		return false | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	// Sanity check that, if a load balancer exists, it isn't misconfigured | ||||||
|  | 	proto := r.Header.Get("X-Forwarded-Proto") | ||||||
|  | 	if "" != proto && proto != "https" { | ||||||
|  | 		return false | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	// Get the host | ||||||
|  | 	// * If TLS, block Domain Fronting | ||||||
|  | 	// * Otherwise assume trusted proxy | ||||||
|  | 	// * Otherwise assume test environment | ||||||
|  | 	var host string | ||||||
|  | 	if nil != r.TLS { | ||||||
|  | 		// Note that if this were to be implemented for HTTP/2 it would need to | ||||||
|  | 		// check all names on the certificate, not just the one with which the | ||||||
|  | 		// original connection was established. However, not our problem here. | ||||||
|  | 		// See https://serverfault.com/a/908087/93930 | ||||||
|  | 		if r.TLS.ServerName != r.Host { | ||||||
|  | 			return false | ||||||
|  | 		} | ||||||
|  | 		host = r.Host | ||||||
|  | 	} else { | ||||||
|  | 		host = r.Header.Get("X-Forwarded-Host") | ||||||
|  | 		if "" == host { | ||||||
|  | 			host = r.Host | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	// Same tests as above, adjusted since it can't handle wildcards and, since | ||||||
|  | 	// the path is variable, we make the assumption that a child can trust a | ||||||
|  | 	// parent, but that a parent cannot trust a child. | ||||||
|  | 	if r.Host != issURL.Host { | ||||||
|  | 		return false | ||||||
|  | 	} | ||||||
|  | 	if !strings.HasPrefix(strings.TrimRight(r.URL.Path, "/")+"/", issURL.Path) { | ||||||
|  | 		// Ex: Request URL                                   Token Issuer | ||||||
|  | 		// !"https:example.com/johndoe/api/dothing".HasPrefix("https:example.com/") | ||||||
|  | 		return false | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	return true | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // Whitelist is a newtype for an array of URLs | ||||||
|  | type Whitelist []*url.URL | ||||||
|  | 
 | ||||||
|  | // NewWhitelist turns an array of URLs (such as https://example.com/) into | ||||||
|  | // a parsed array of *url.URLs that can be used by the IsTrustedIssuer function | ||||||
|  | func NewWhitelist(issuers []string, privateList ...[]string) (Whitelist, error) { | ||||||
|  | 	var err error | ||||||
|  | 
 | ||||||
|  | 	list := []*url.URL{} | ||||||
|  | 	if 0 != len(issuers) { | ||||||
|  | 		insecure := false | ||||||
|  | 		list, err = newWhitelist(list, issuers, insecure) | ||||||
|  | 		if nil != err { | ||||||
|  | 			return nil, err | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 	if 0 != len(privateList) && 0 != len(privateList[0]) { | ||||||
|  | 		insecure := true | ||||||
|  | 		list, err = newWhitelist(list, privateList[0], insecure) | ||||||
|  | 		if nil != err { | ||||||
|  | 			return nil, err | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	return Whitelist(list), nil | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func newWhitelist(list []*url.URL, issuers []string, insecure bool) (Whitelist, error) { | ||||||
|  | 	for i := range issuers { | ||||||
|  | 		iss := issuers[i] | ||||||
|  | 		if "" == strings.TrimSpace(iss) { | ||||||
|  | 			fmt.Println("[Warning] You have an empty string in your keyfetch whitelist.") | ||||||
|  | 			continue | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		// Should have a valid http or https prefix | ||||||
|  | 		// TODO support custom prefixes (i.e. app://) ? | ||||||
|  | 		if strings.HasPrefix(iss, "http://") { | ||||||
|  | 			if !insecure { | ||||||
|  | 				log.Println("Oops! You have an insecure domain in your whitelist: ", iss) | ||||||
|  | 				return nil, ErrInsecureDomain | ||||||
|  | 			} | ||||||
|  | 		} else if strings.HasPrefix(iss, "//") { | ||||||
|  | 			// TODO | ||||||
|  | 			return nil, errors.New("Rather than prefixing with // to support multiple protocols, add them seperately:" + iss) | ||||||
|  | 		} else if !strings.HasPrefix(iss, "https://") { | ||||||
|  | 			iss = "https://" + iss | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		// trailing slash as a boundary character, which may or may not denote a directory | ||||||
|  | 		iss = strings.TrimRight(iss, "/") + "/" | ||||||
|  | 		u, err := url.Parse(iss) | ||||||
|  | 		if nil != err { | ||||||
|  | 			return nil, err | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		// Strip any * prefix, for easier comparison later | ||||||
|  | 		// *.example.com => .example.com | ||||||
|  | 		if strings.HasPrefix(u.Host, "*.") { | ||||||
|  | 			u.Host = u.Host[1:] | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		list = append(list, u) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	return list, nil | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | /* | ||||||
|  |   IsTrustedIssuer returns true when the `iss` (i.e. from a token) matches one | ||||||
|  |   in the provided whitelist (also matches wildcard domains). | ||||||
|  | 
 | ||||||
|  |   You may explicitly allow insecure http (i.e. for automated testing) by | ||||||
|  |   including http:// Otherwise the scheme in each item of the whitelist should | ||||||
|  | 	include the "https://" prefix. | ||||||
|  | 
 | ||||||
|  |   SECURITY CONSIDERATIONS (Please Read) | ||||||
|  | 
 | ||||||
|  |   You'll notice that *http.Request is optional. It should only be used under these | ||||||
|  |   three circumstances: | ||||||
|  | 
 | ||||||
|  |     1) Something else guarantees http -> https redirection happens before the | ||||||
|  |        connection gets here AND this server directly handles TLS/SSL. | ||||||
|  | 
 | ||||||
|  |     2) If you're using a load balancer or web server, and this doesn't handle | ||||||
|  |        TLS/SSL directly, that server is _explicitly_ configured to protect | ||||||
|  |        against Domain Fronting attacks. As of 2019, most web servers and load | ||||||
|  |        balancers do not protect against that by default. | ||||||
|  | 
 | ||||||
|  |     3) If you only use it to make your automated integration testing more | ||||||
|  |        and it isn't enabled in production. | ||||||
|  | 
 | ||||||
|  |   Otherwise, DO NOT pass in *http.Request as you will introduce a 0-day | ||||||
|  |   vulnerability allowing an attacker to spoof any token issuer of their choice. | ||||||
|  |   The only reason I allowed this in a public library where non-experts would | ||||||
|  |   encounter it is to make testing easier. | ||||||
|  | */ | ||||||
|  | func (w Whitelist) IsTrustedIssuer(iss string, rs ...*http.Request) bool { | ||||||
|  | 	return isTrustedIssuer(iss, w, rs...) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // String will generate a space-delimited list of whitelisted URLs | ||||||
|  | func (w Whitelist) String() string { | ||||||
|  | 	s := []string{} | ||||||
|  | 	for i := range w { | ||||||
|  | 		s = append(s, w[i].String()) | ||||||
|  | 	} | ||||||
|  | 	return strings.Join(s, " ") | ||||||
|  | } | ||||||
							
								
								
									
										183
									
								
								vendor/git.rootprojects.org/root/keypairs/keyfetch/uncached/fetch.go
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										183
									
								
								vendor/git.rootprojects.org/root/keypairs/keyfetch/uncached/fetch.go
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							| @ -0,0 +1,183 @@ | |||||||
|  | // Package uncached provides uncached versions of go-keypairs/keyfetch | ||||||
|  | package uncached | ||||||
|  | 
 | ||||||
|  | import ( | ||||||
|  | 	"bytes" | ||||||
|  | 	"encoding/json" | ||||||
|  | 	"errors" | ||||||
|  | 	"io" | ||||||
|  | 	"io/ioutil" | ||||||
|  | 	"net" | ||||||
|  | 	"net/http" | ||||||
|  | 	"strings" | ||||||
|  | 	"time" | ||||||
|  | 
 | ||||||
|  | 	"git.rootprojects.org/root/keypairs" | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | // OIDCJWKs gets the OpenID Connect configuration from the baseURL and then calls JWKs with the specified jwks_uri | ||||||
|  | func OIDCJWKs(baseURL string) (map[string]map[string]string, map[string]keypairs.PublicKey, error) { | ||||||
|  | 	baseURL = normalizeBaseURL(baseURL) | ||||||
|  | 	oidcConf := struct { | ||||||
|  | 		JWKSURI string `json:"jwks_uri"` | ||||||
|  | 	}{} | ||||||
|  | 
 | ||||||
|  | 	// must come in as https://<domain>/ | ||||||
|  | 	url := baseURL + ".well-known/openid-configuration" | ||||||
|  | 	err := safeFetch(url, func(body io.Reader) error { | ||||||
|  | 		decoder := json.NewDecoder(body) | ||||||
|  | 		decoder.UseNumber() | ||||||
|  | 		return decoder.Decode(&oidcConf) | ||||||
|  | 	}) | ||||||
|  | 	if nil != err { | ||||||
|  | 		return nil, nil, err | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	return JWKs(oidcConf.JWKSURI) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // WellKnownJWKs calls JWKs with baseURL + /.well-known/jwks.json as constructs the jwks_uri | ||||||
|  | func WellKnownJWKs(baseURL string) (map[string]map[string]string, map[string]keypairs.PublicKey, error) { | ||||||
|  | 	baseURL = normalizeBaseURL(baseURL) | ||||||
|  | 	url := baseURL + ".well-known/jwks.json" | ||||||
|  | 
 | ||||||
|  | 	return JWKs(url) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // JWKs fetches and parses a jwks.json (assuming well-known format) | ||||||
|  | func JWKs(jwksurl string) (map[string]map[string]string, map[string]keypairs.PublicKey, error) { | ||||||
|  | 	keys := map[string]keypairs.PublicKey{} | ||||||
|  | 	maps := map[string]map[string]string{} | ||||||
|  | 	resp := struct { | ||||||
|  | 		Keys []map[string]interface{} `json:"keys"` | ||||||
|  | 	}{ | ||||||
|  | 		Keys: make([]map[string]interface{}, 0, 1), | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	if err := safeFetch(jwksurl, func(body io.Reader) error { | ||||||
|  | 		decoder := json.NewDecoder(body) | ||||||
|  | 		decoder.UseNumber() | ||||||
|  | 		return decoder.Decode(&resp) | ||||||
|  | 	}); nil != err { | ||||||
|  | 		return nil, nil, err | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	for i := range resp.Keys { | ||||||
|  | 		k := resp.Keys[i] | ||||||
|  | 		m := getStringMap(k) | ||||||
|  | 
 | ||||||
|  | 		key, err := keypairs.NewJWKPublicKey(m) | ||||||
|  | 
 | ||||||
|  | 		if nil != err { | ||||||
|  | 			return nil, nil, err | ||||||
|  | 		} | ||||||
|  | 		keys[key.Thumbprint()] = key | ||||||
|  | 		maps[key.Thumbprint()] = m | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	return maps, keys, nil | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // PEM fetches and parses a PEM (assuming well-known format) | ||||||
|  | func PEM(pemurl string) (map[string]string, keypairs.PublicKey, error) { | ||||||
|  | 	var pub keypairs.PublicKey | ||||||
|  | 	if err := safeFetch(pemurl, func(body io.Reader) error { | ||||||
|  | 		pem, err := ioutil.ReadAll(body) | ||||||
|  | 		if nil != err { | ||||||
|  | 			return err | ||||||
|  | 		} | ||||||
|  | 		pub, err = keypairs.ParsePublicKey(pem) | ||||||
|  | 		return err | ||||||
|  | 	}); nil != err { | ||||||
|  | 		return nil, nil, err | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	jwk := map[string]interface{}{} | ||||||
|  | 	body := bytes.NewBuffer(keypairs.MarshalJWKPublicKey(pub)) | ||||||
|  | 	decoder := json.NewDecoder(body) | ||||||
|  | 	decoder.UseNumber() | ||||||
|  | 	_ = decoder.Decode(&jwk) | ||||||
|  | 
 | ||||||
|  | 	m := getStringMap(jwk) | ||||||
|  | 	m["kid"] = pemurl | ||||||
|  | 
 | ||||||
|  | 	switch p := pub.(type) { | ||||||
|  | 	case *keypairs.ECPublicKey: | ||||||
|  | 		p.KID = pemurl | ||||||
|  | 	case *keypairs.RSAPublicKey: | ||||||
|  | 		p.KID = pemurl | ||||||
|  | 	default: | ||||||
|  | 		return nil, nil, errors.New("impossible key type") | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	return m, pub, nil | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // Fetch retrieves a single JWK (plain, bare jwk) from a URL (off-spec) | ||||||
|  | func Fetch(url string) (map[string]string, keypairs.PublicKey, error) { | ||||||
|  | 	var m map[string]interface{} | ||||||
|  | 	if err := safeFetch(url, func(body io.Reader) error { | ||||||
|  | 		decoder := json.NewDecoder(body) | ||||||
|  | 		decoder.UseNumber() | ||||||
|  | 		return decoder.Decode(&m) | ||||||
|  | 	}); nil != err { | ||||||
|  | 		return nil, nil, err | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	n := getStringMap(m) | ||||||
|  | 	key, err := keypairs.NewJWKPublicKey(n) | ||||||
|  | 	if nil != err { | ||||||
|  | 		return nil, nil, err | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	return n, key, nil | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func getStringMap(m map[string]interface{}) map[string]string { | ||||||
|  | 	n := make(map[string]string) | ||||||
|  | 
 | ||||||
|  | 	// TODO get issuer from x5c, if exists | ||||||
|  | 
 | ||||||
|  | 	// convert map[string]interface{} to map[string]string | ||||||
|  | 	for j := range m { | ||||||
|  | 		switch s := m[j].(type) { | ||||||
|  | 		case string: | ||||||
|  | 			n[j] = s | ||||||
|  | 		default: | ||||||
|  | 			// safely ignore | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	return n | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | type decodeFunc func(io.Reader) error | ||||||
|  | 
 | ||||||
|  | // TODO: also limit the body size | ||||||
|  | func safeFetch(url string, decoder decodeFunc) error { | ||||||
|  | 	var netTransport = &http.Transport{ | ||||||
|  | 		Dial: (&net.Dialer{ | ||||||
|  | 			Timeout: 5 * time.Second, | ||||||
|  | 		}).Dial, | ||||||
|  | 		TLSHandshakeTimeout: 5 * time.Second, | ||||||
|  | 	} | ||||||
|  | 	var client = &http.Client{ | ||||||
|  | 		Timeout:   time.Second * 10, | ||||||
|  | 		Transport: netTransport, | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	req, err := http.NewRequest("GET", url, nil) | ||||||
|  | 	req.Header.Set("User-Agent", "go-keypairs/keyfetch") | ||||||
|  | 	req.Header.Set("Accept", "application/json;q=0.9,*/*;q=0.8") | ||||||
|  | 	res, err := client.Do(req) | ||||||
|  | 	if nil != err { | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  | 	defer res.Body.Close() | ||||||
|  | 
 | ||||||
|  | 	return decoder(res.Body) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func normalizeBaseURL(iss string) string { | ||||||
|  | 	return strings.TrimRight(iss, "/") + "/" | ||||||
|  | } | ||||||
							
								
								
									
										645
									
								
								vendor/git.rootprojects.org/root/keypairs/keypairs.go
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										645
									
								
								vendor/git.rootprojects.org/root/keypairs/keypairs.go
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							| @ -0,0 +1,645 @@ | |||||||
|  | package keypairs | ||||||
|  | 
 | ||||||
|  | import ( | ||||||
|  | 	"bytes" | ||||||
|  | 	"crypto" | ||||||
|  | 	"crypto/dsa" | ||||||
|  | 	"crypto/ecdsa" | ||||||
|  | 	"crypto/elliptic" | ||||||
|  | 	"crypto/rsa" | ||||||
|  | 	"crypto/sha256" | ||||||
|  | 	"crypto/x509" | ||||||
|  | 	"encoding/base64" | ||||||
|  | 	"encoding/json" | ||||||
|  | 	"encoding/pem" | ||||||
|  | 	"errors" | ||||||
|  | 	"fmt" | ||||||
|  | 	"io" | ||||||
|  | 	"log" | ||||||
|  | 	"math/big" | ||||||
|  | 	"strings" | ||||||
|  | 	"time" | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | // ErrInvalidPrivateKey means that the key is not a valid Private Key | ||||||
|  | var ErrInvalidPrivateKey = errors.New("PrivateKey must be of type *rsa.PrivateKey or *ecdsa.PrivateKey") | ||||||
|  | 
 | ||||||
|  | // ErrInvalidPublicKey means that the key is not a valid Public Key | ||||||
|  | var ErrInvalidPublicKey = errors.New("PublicKey must be of type *rsa.PublicKey or *ecdsa.PublicKey") | ||||||
|  | 
 | ||||||
|  | // ErrParsePublicKey means that the bytes cannot be parsed in any known format | ||||||
|  | var ErrParsePublicKey = errors.New("PublicKey bytes could not be parsed as PEM or DER (PKIX/SPKI, PKCS1, or X509 Certificate) or JWK") | ||||||
|  | 
 | ||||||
|  | // ErrParsePrivateKey means that the bytes cannot be parsed in any known format | ||||||
|  | var ErrParsePrivateKey = errors.New("PrivateKey bytes could not be parsed as PEM or DER (PKCS8, SEC1, or PKCS1) or JWK") | ||||||
|  | 
 | ||||||
|  | // ErrParseJWK means that the JWK is valid JSON but not a valid JWK | ||||||
|  | var ErrParseJWK = errors.New("JWK is missing required base64-encoded JSON fields") | ||||||
|  | 
 | ||||||
|  | // ErrInvalidKeyType means that the key is not an acceptable type | ||||||
|  | var ErrInvalidKeyType = errors.New("The JWK's 'kty' must be either 'RSA' or 'EC'") | ||||||
|  | 
 | ||||||
|  | // ErrInvalidCurve means that a non-standard curve was used | ||||||
|  | var ErrInvalidCurve = errors.New("The JWK's 'crv' must be either of the NIST standards 'P-256' or 'P-384'") | ||||||
|  | 
 | ||||||
|  | // ErrUnexpectedPublicKey means that a Private Key was expected | ||||||
|  | var ErrUnexpectedPublicKey = errors.New("PrivateKey was given where PublicKey was expected") | ||||||
|  | 
 | ||||||
|  | // ErrUnexpectedPrivateKey means that a Public Key was expected | ||||||
|  | var ErrUnexpectedPrivateKey = errors.New("PublicKey was given where PrivateKey was expected") | ||||||
|  | 
 | ||||||
|  | // ErrDevSwapPrivatePublic means that the developer compiled bad code that swapped public and private keys | ||||||
|  | const ErrDevSwapPrivatePublic = "[Developer Error] You passed either crypto.PrivateKey or crypto.PublicKey where the other was expected." | ||||||
|  | 
 | ||||||
|  | // ErrDevBadKeyType means that the developer compiled bad code that passes the wrong type | ||||||
|  | const ErrDevBadKeyType = "[Developer Error] crypto.PublicKey and crypto.PrivateKey are somewhat deceptive. They're actually empty interfaces that accept any object, even non-crypto objects. You passed an object of type '%T' by mistake." | ||||||
|  | 
 | ||||||
|  | // PrivateKey is a zero-cost typesafe substitue for crypto.PrivateKey | ||||||
|  | type PrivateKey interface { | ||||||
|  | 	Public() crypto.PublicKey | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // PublicKey thinly veils crypto.PublicKey for type safety | ||||||
|  | type PublicKey interface { | ||||||
|  | 	crypto.PublicKey | ||||||
|  | 	Thumbprint() string | ||||||
|  | 	KeyID() string | ||||||
|  | 	Key() crypto.PublicKey | ||||||
|  | 	ExpiresAt() time.Time | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // ECPublicKey adds common methods to *ecdsa.PublicKey for type safety | ||||||
|  | type ECPublicKey struct { | ||||||
|  | 	PublicKey *ecdsa.PublicKey // empty interface | ||||||
|  | 	KID       string | ||||||
|  | 	Expiry    time.Time | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // RSAPublicKey adds common methods to *rsa.PublicKey for type safety | ||||||
|  | type RSAPublicKey struct { | ||||||
|  | 	PublicKey *rsa.PublicKey // empty interface | ||||||
|  | 	KID       string | ||||||
|  | 	Expiry    time.Time | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // Thumbprint returns a JWK thumbprint. See https://stackoverflow.com/questions/42588786/how-to-fingerprint-a-jwk | ||||||
|  | func (p *ECPublicKey) Thumbprint() string { | ||||||
|  | 	return ThumbprintUntypedPublicKey(p.PublicKey) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // KeyID returns the JWK `kid`, which will be the Thumbprint for keys generated with this library | ||||||
|  | func (p *ECPublicKey) KeyID() string { | ||||||
|  | 	return p.KID | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // Key returns the PublicKey | ||||||
|  | func (p *ECPublicKey) Key() crypto.PublicKey { | ||||||
|  | 	return p.PublicKey | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // ExpireAt sets the time at which this Public Key should be considered invalid | ||||||
|  | func (p *ECPublicKey) ExpireAt(t time.Time) { | ||||||
|  | 	p.Expiry = t | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // ExpiresAt gets the time at which this Public Key should be considered invalid | ||||||
|  | func (p *ECPublicKey) ExpiresAt() time.Time { | ||||||
|  | 	return p.Expiry | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // Thumbprint returns a JWK thumbprint. See https://stackoverflow.com/questions/42588786/how-to-fingerprint-a-jwk | ||||||
|  | func (p *RSAPublicKey) Thumbprint() string { | ||||||
|  | 	return ThumbprintUntypedPublicKey(p.PublicKey) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // KeyID returns the JWK `kid`, which will be the Thumbprint for keys generated with this library | ||||||
|  | func (p *RSAPublicKey) KeyID() string { | ||||||
|  | 	return p.KID | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // Key returns the PublicKey | ||||||
|  | func (p *RSAPublicKey) Key() crypto.PublicKey { | ||||||
|  | 	return p.PublicKey | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // ExpireAt sets the time at which this Public Key should be considered invalid | ||||||
|  | func (p *RSAPublicKey) ExpireAt(t time.Time) { | ||||||
|  | 	p.Expiry = t | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // ExpiresAt gets the time at which this Public Key should be considered invalid | ||||||
|  | func (p *RSAPublicKey) ExpiresAt() time.Time { | ||||||
|  | 	return p.Expiry | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // NewPublicKey wraps a crypto.PublicKey to make it typesafe. | ||||||
|  | func NewPublicKey(pub crypto.PublicKey, kid ...string) PublicKey { | ||||||
|  | 	var k PublicKey | ||||||
|  | 	switch p := pub.(type) { | ||||||
|  | 	case *ecdsa.PublicKey: | ||||||
|  | 		eckey := &ECPublicKey{ | ||||||
|  | 			PublicKey: p, | ||||||
|  | 		} | ||||||
|  | 		if 0 != len(kid) { | ||||||
|  | 			eckey.KID = kid[0] | ||||||
|  | 		} else { | ||||||
|  | 			eckey.KID = ThumbprintECPublicKey(p) | ||||||
|  | 		} | ||||||
|  | 		k = eckey | ||||||
|  | 	case *rsa.PublicKey: | ||||||
|  | 		rsakey := &RSAPublicKey{ | ||||||
|  | 			PublicKey: p, | ||||||
|  | 		} | ||||||
|  | 		if 0 != len(kid) { | ||||||
|  | 			rsakey.KID = kid[0] | ||||||
|  | 		} else { | ||||||
|  | 			rsakey.KID = ThumbprintRSAPublicKey(p) | ||||||
|  | 		} | ||||||
|  | 		k = rsakey | ||||||
|  | 	case *ecdsa.PrivateKey: | ||||||
|  | 		panic(errors.New(ErrDevSwapPrivatePublic)) | ||||||
|  | 	case *rsa.PrivateKey: | ||||||
|  | 		panic(errors.New(ErrDevSwapPrivatePublic)) | ||||||
|  | 	case *dsa.PublicKey: | ||||||
|  | 		panic(ErrInvalidPublicKey) | ||||||
|  | 	case *dsa.PrivateKey: | ||||||
|  | 		panic(ErrInvalidPrivateKey) | ||||||
|  | 	default: | ||||||
|  | 		panic(fmt.Errorf(ErrDevBadKeyType, pub)) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	return k | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // MarshalJWKPublicKey outputs a JWK with its key id (kid) and an optional expiration, | ||||||
|  | // making it suitable for use as an OIDC public key. | ||||||
|  | func MarshalJWKPublicKey(key PublicKey, exp ...time.Time) []byte { | ||||||
|  | 	// thumbprint keys are alphabetically sorted and only include the necessary public parts | ||||||
|  | 	switch k := key.Key().(type) { | ||||||
|  | 	case *rsa.PublicKey: | ||||||
|  | 		return MarshalRSAPublicKey(k, exp...) | ||||||
|  | 	case *ecdsa.PublicKey: | ||||||
|  | 		return MarshalECPublicKey(k, exp...) | ||||||
|  | 	case *dsa.PublicKey: | ||||||
|  | 		panic(ErrInvalidPublicKey) | ||||||
|  | 	default: | ||||||
|  | 		// this is unreachable because we know the types that we pass in | ||||||
|  | 		log.Printf("keytype: %t, %+v\n", key, key) | ||||||
|  | 		panic(ErrInvalidPublicKey) | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // ThumbprintPublicKey returns the SHA256 RFC-spec JWK thumbprint | ||||||
|  | func ThumbprintPublicKey(pub PublicKey) string { | ||||||
|  | 	return ThumbprintUntypedPublicKey(pub.Key()) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // ThumbprintUntypedPublicKey is a non-typesafe version of ThumbprintPublicKey | ||||||
|  | // (but will still panic, to help you discover bugs in development rather than production). | ||||||
|  | func ThumbprintUntypedPublicKey(pub crypto.PublicKey) string { | ||||||
|  | 	switch p := pub.(type) { | ||||||
|  | 	case PublicKey: | ||||||
|  | 		return ThumbprintUntypedPublicKey(p.Key()) | ||||||
|  | 	case *ecdsa.PublicKey: | ||||||
|  | 		return ThumbprintECPublicKey(p) | ||||||
|  | 	case *rsa.PublicKey: | ||||||
|  | 		return ThumbprintRSAPublicKey(p) | ||||||
|  | 	default: | ||||||
|  | 		panic(ErrInvalidPublicKey) | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // MarshalECPublicKey will take an EC key and output a JWK, with optional expiration date | ||||||
|  | func MarshalECPublicKey(k *ecdsa.PublicKey, exp ...time.Time) []byte { | ||||||
|  | 	thumb := ThumbprintECPublicKey(k) | ||||||
|  | 	crv := k.Curve.Params().Name | ||||||
|  | 	x := base64.RawURLEncoding.EncodeToString(k.X.Bytes()) | ||||||
|  | 	y := base64.RawURLEncoding.EncodeToString(k.Y.Bytes()) | ||||||
|  | 	expstr := "" | ||||||
|  | 	if 0 != len(exp) { | ||||||
|  | 		expstr = fmt.Sprintf(`"exp":%d,`, exp[0].Unix()) | ||||||
|  | 	} | ||||||
|  | 	return []byte(fmt.Sprintf(`{"kid":%q,"use":"sig",%s"crv":%q,"kty":"EC","x":%q,"y":%q}`, thumb, expstr, crv, x, y)) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // MarshalECPublicKeyWithoutKeyID will output the most minimal version of an EC JWK (no key id, no "use" flag, nada) | ||||||
|  | func MarshalECPublicKeyWithoutKeyID(k *ecdsa.PublicKey) []byte { | ||||||
|  | 	crv := k.Curve.Params().Name | ||||||
|  | 	x := base64.RawURLEncoding.EncodeToString(k.X.Bytes()) | ||||||
|  | 	y := base64.RawURLEncoding.EncodeToString(k.Y.Bytes()) | ||||||
|  | 	return []byte(fmt.Sprintf(`{"crv":%q,"kty":"EC","x":%q,"y":%q}`, crv, x, y)) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // ThumbprintECPublicKey will output a RFC-spec SHA256 JWK thumbprint of an EC public key | ||||||
|  | func ThumbprintECPublicKey(k *ecdsa.PublicKey) string { | ||||||
|  | 	thumbprintable := MarshalECPublicKeyWithoutKeyID(k) | ||||||
|  | 	sha := sha256.Sum256(thumbprintable) | ||||||
|  | 	return base64.RawURLEncoding.EncodeToString(sha[:]) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // MarshalRSAPublicKey will take an RSA key and output a JWK, with optional expiration date | ||||||
|  | func MarshalRSAPublicKey(p *rsa.PublicKey, exp ...time.Time) []byte { | ||||||
|  | 	thumb := ThumbprintRSAPublicKey(p) | ||||||
|  | 	e := base64.RawURLEncoding.EncodeToString(big.NewInt(int64(p.E)).Bytes()) | ||||||
|  | 	n := base64.RawURLEncoding.EncodeToString(p.N.Bytes()) | ||||||
|  | 	expstr := "" | ||||||
|  | 	if 0 != len(exp) { | ||||||
|  | 		expstr = fmt.Sprintf(`"exp":%d,`, exp[0].Unix()) | ||||||
|  | 	} | ||||||
|  | 	return []byte(fmt.Sprintf(`{"kid":%q,"use":"sig",%s"e":%q,"kty":"RSA","n":%q}`, thumb, expstr, e, n)) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // MarshalRSAPublicKeyWithoutKeyID will output the most minimal version of an RSA JWK (no key id, no "use" flag, nada) | ||||||
|  | func MarshalRSAPublicKeyWithoutKeyID(p *rsa.PublicKey) []byte { | ||||||
|  | 	e := base64.RawURLEncoding.EncodeToString(big.NewInt(int64(p.E)).Bytes()) | ||||||
|  | 	n := base64.RawURLEncoding.EncodeToString(p.N.Bytes()) | ||||||
|  | 	return []byte(fmt.Sprintf(`{"e":%q,"kty":"RSA","n":%q}`, e, n)) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // ThumbprintRSAPublicKey will output a RFC-spec SHA256 JWK thumbprint of an EC public key | ||||||
|  | func ThumbprintRSAPublicKey(p *rsa.PublicKey) string { | ||||||
|  | 	thumbprintable := MarshalRSAPublicKeyWithoutKeyID(p) | ||||||
|  | 	sha := sha256.Sum256([]byte(thumbprintable)) | ||||||
|  | 	return base64.RawURLEncoding.EncodeToString(sha[:]) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // ParsePrivateKey will try to parse the bytes you give it | ||||||
|  | // in any of the supported formats: PEM, DER, PKCS8, PKCS1, SEC1, and JWK | ||||||
|  | func ParsePrivateKey(block []byte) (PrivateKey, error) { | ||||||
|  | 	blocks, err := getPEMBytes(block) | ||||||
|  | 	if nil != err { | ||||||
|  | 		return nil, ErrParsePrivateKey | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	// Parse PEM blocks (openssl generates junk metadata blocks for ECs) | ||||||
|  | 	// or the original DER, or the JWK | ||||||
|  | 	for i := range blocks { | ||||||
|  | 		block = blocks[i] | ||||||
|  | 		if key, err := parsePrivateKey(block); nil == err { | ||||||
|  | 			return key, nil | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	for i := range blocks { | ||||||
|  | 		block = blocks[i] | ||||||
|  | 		if _, err := parsePublicKey(block); nil == err { | ||||||
|  | 			return nil, ErrUnexpectedPublicKey | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	// If we didn't parse a key arleady, we failed | ||||||
|  | 	return nil, ErrParsePrivateKey | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // ParsePrivateKeyString calls ParsePrivateKey([]byte(key)) for all you lazy folk. | ||||||
|  | func ParsePrivateKeyString(block string) (PrivateKey, error) { | ||||||
|  | 	return ParsePrivateKey([]byte(block)) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func parsePrivateKey(der []byte) (PrivateKey, error) { | ||||||
|  | 	var key PrivateKey | ||||||
|  | 
 | ||||||
|  | 	//fmt.Println("1. ParsePKCS8PrivateKey") | ||||||
|  | 	xkey, err := x509.ParsePKCS8PrivateKey(der) | ||||||
|  | 	if nil == err { | ||||||
|  | 		switch k := xkey.(type) { | ||||||
|  | 		case *rsa.PrivateKey: | ||||||
|  | 			key = k | ||||||
|  | 		case *ecdsa.PrivateKey: | ||||||
|  | 			key = k | ||||||
|  | 		default: | ||||||
|  | 			err = errors.New("Only RSA and ECDSA (EC) Private Keys are supported") | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	if nil != err { | ||||||
|  | 		//fmt.Println("2. ParseECPrivateKey") | ||||||
|  | 		key, err = x509.ParseECPrivateKey(der) | ||||||
|  | 		if nil != err { | ||||||
|  | 			//fmt.Println("3. ParsePKCS1PrivateKey") | ||||||
|  | 			key, err = x509.ParsePKCS1PrivateKey(der) | ||||||
|  | 			if nil != err { | ||||||
|  | 				//fmt.Println("4. ParseJWKPrivateKey") | ||||||
|  | 				key, err = ParseJWKPrivateKey(der) | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	// But did you know? | ||||||
|  | 	// You must return nil explicitly for interfaces | ||||||
|  | 	// https://golang.org/doc/faq#nil_error | ||||||
|  | 	if nil != err { | ||||||
|  | 		return nil, err | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	return key, nil | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func getPEMBytes(block []byte) ([][]byte, error) { | ||||||
|  | 	var pemblock *pem.Block | ||||||
|  | 	var blocks = make([][]byte, 0, 1) | ||||||
|  | 
 | ||||||
|  | 	// Parse the PEM, if it's a pem | ||||||
|  | 	for { | ||||||
|  | 		pemblock, block = pem.Decode(block) | ||||||
|  | 		if nil != pemblock { | ||||||
|  | 			// got one block, there may be more | ||||||
|  | 			blocks = append(blocks, pemblock.Bytes) | ||||||
|  | 		} else { | ||||||
|  | 			// the last block was not a PEM block | ||||||
|  | 			// therefore the next isn't either | ||||||
|  | 			if 0 != len(block) { | ||||||
|  | 				blocks = append(blocks, block) | ||||||
|  | 			} | ||||||
|  | 			break | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	if len(blocks) > 0 { | ||||||
|  | 		return blocks, nil | ||||||
|  | 	} | ||||||
|  | 	return nil, errors.New("no PEM blocks found") | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // ParsePublicKey will try to parse the bytes you give it | ||||||
|  | // in any of the supported formats: PEM, DER, PKIX/SPKI, PKCS1, x509 Certificate, and JWK | ||||||
|  | func ParsePublicKey(block []byte) (PublicKey, error) { | ||||||
|  | 	blocks, err := getPEMBytes(block) | ||||||
|  | 	if nil != err { | ||||||
|  | 		return nil, ErrParsePublicKey | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	// Parse PEM blocks (openssl generates junk metadata blocks for ECs) | ||||||
|  | 	// or the original DER, or the JWK | ||||||
|  | 	for i := range blocks { | ||||||
|  | 		block = blocks[i] | ||||||
|  | 		if key, err := parsePublicKey(block); nil == err { | ||||||
|  | 			return key, nil | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	for i := range blocks { | ||||||
|  | 		block = blocks[i] | ||||||
|  | 		if _, err := parsePrivateKey(block); nil == err { | ||||||
|  | 			return nil, ErrUnexpectedPrivateKey | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	// If we didn't parse a key arleady, we failed | ||||||
|  | 	return nil, ErrParsePublicKey | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // ParsePublicKeyString calls ParsePublicKey([]byte(key)) for all you lazy folk. | ||||||
|  | func ParsePublicKeyString(block string) (PublicKey, error) { | ||||||
|  | 	return ParsePublicKey([]byte(block)) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func parsePublicKey(der []byte) (PublicKey, error) { | ||||||
|  | 	cert, err := x509.ParseCertificate(der) | ||||||
|  | 	if nil == err { | ||||||
|  | 		switch k := cert.PublicKey.(type) { | ||||||
|  | 		case *rsa.PublicKey: | ||||||
|  | 			return NewPublicKey(k), nil | ||||||
|  | 		case *ecdsa.PublicKey: | ||||||
|  | 			return NewPublicKey(k), nil | ||||||
|  | 		default: | ||||||
|  | 			return nil, errors.New("Only RSA and ECDSA (EC) Public Keys are supported") | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	//fmt.Println("1. ParsePKIXPublicKey") | ||||||
|  | 	xkey, err := x509.ParsePKIXPublicKey(der) | ||||||
|  | 	if nil == err { | ||||||
|  | 		switch k := xkey.(type) { | ||||||
|  | 		case *rsa.PublicKey: | ||||||
|  | 			return NewPublicKey(k), nil | ||||||
|  | 		case *ecdsa.PublicKey: | ||||||
|  | 			return NewPublicKey(k), nil | ||||||
|  | 		default: | ||||||
|  | 			return nil, errors.New("Only RSA and ECDSA (EC) Public Keys are supported") | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	//fmt.Println("3. ParsePKCS1PrublicKey") | ||||||
|  | 	rkey, err := x509.ParsePKCS1PublicKey(der) | ||||||
|  | 	if nil == err { | ||||||
|  | 		//fmt.Println("4. ParseJWKPublicKey") | ||||||
|  | 		return NewPublicKey(rkey), nil | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	return ParseJWKPublicKey(der) | ||||||
|  | 
 | ||||||
|  | 	/* | ||||||
|  | 		// But did you know? | ||||||
|  | 		// You must return nil explicitly for interfaces | ||||||
|  | 		// https://golang.org/doc/faq#nil_error | ||||||
|  | 		if nil != err { | ||||||
|  | 			return nil, err | ||||||
|  | 		} | ||||||
|  | 	*/ | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // NewJWKPublicKey contstructs a PublicKey from the relevant pieces a map[string]string (generic JSON) | ||||||
|  | func NewJWKPublicKey(m map[string]string) (PublicKey, error) { | ||||||
|  | 	switch m["kty"] { | ||||||
|  | 	case "RSA": | ||||||
|  | 		return parseRSAPublicKey(m) | ||||||
|  | 	case "EC": | ||||||
|  | 		return parseECPublicKey(m) | ||||||
|  | 	default: | ||||||
|  | 		return nil, ErrInvalidKeyType | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // ParseJWKPublicKey parses a JSON-encoded JWK and returns a PublicKey, or a (hopefully) helpful error message | ||||||
|  | func ParseJWKPublicKey(b []byte) (PublicKey, error) { | ||||||
|  | 	// RSA and EC have "d" as a private part | ||||||
|  | 	if bytes.Contains(b, []byte(`"d"`)) { | ||||||
|  | 		return nil, ErrUnexpectedPrivateKey | ||||||
|  | 	} | ||||||
|  | 	return newJWKPublicKey(b) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // ParseJWKPublicKeyString calls ParseJWKPublicKey([]byte(key)) for all you lazy folk. | ||||||
|  | func ParseJWKPublicKeyString(s string) (PublicKey, error) { | ||||||
|  | 	if strings.Contains(s, `"d"`) { | ||||||
|  | 		return nil, ErrUnexpectedPrivateKey | ||||||
|  | 	} | ||||||
|  | 	return newJWKPublicKey(s) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // DecodeJWKPublicKey stream-decodes a JSON-encoded JWK and returns a PublicKey, or a (hopefully) helpful error message | ||||||
|  | func DecodeJWKPublicKey(r io.Reader) (PublicKey, error) { | ||||||
|  | 	m := make(map[string]string) | ||||||
|  | 	if err := json.NewDecoder(r).Decode(&m); nil != err { | ||||||
|  | 		return nil, err | ||||||
|  | 	} | ||||||
|  | 	if d := m["d"]; "" != d { | ||||||
|  | 		return nil, ErrUnexpectedPrivateKey | ||||||
|  | 	} | ||||||
|  | 	return newJWKPublicKey(m) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // the underpinnings of the parser as used by the typesafe wrappers | ||||||
|  | func newJWKPublicKey(data interface{}) (PublicKey, error) { | ||||||
|  | 	var m map[string]string | ||||||
|  | 
 | ||||||
|  | 	switch d := data.(type) { | ||||||
|  | 	case map[string]string: | ||||||
|  | 		m = d | ||||||
|  | 	case string: | ||||||
|  | 		if err := json.Unmarshal([]byte(d), &m); nil != err { | ||||||
|  | 			return nil, err | ||||||
|  | 		} | ||||||
|  | 	case []byte: | ||||||
|  | 		if err := json.Unmarshal(d, &m); nil != err { | ||||||
|  | 			return nil, err | ||||||
|  | 		} | ||||||
|  | 	default: | ||||||
|  | 		panic("Developer Error: unsupported interface type") | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	return NewJWKPublicKey(m) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // ParseJWKPrivateKey parses a JSON-encoded JWK and returns a PrivateKey, or a (hopefully) helpful error message | ||||||
|  | func ParseJWKPrivateKey(b []byte) (PrivateKey, error) { | ||||||
|  | 	var m map[string]string | ||||||
|  | 	if err := json.Unmarshal(b, &m); nil != err { | ||||||
|  | 		return nil, err | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	switch m["kty"] { | ||||||
|  | 	case "RSA": | ||||||
|  | 		return parseRSAPrivateKey(m) | ||||||
|  | 	case "EC": | ||||||
|  | 		return parseECPrivateKey(m) | ||||||
|  | 	default: | ||||||
|  | 		return nil, ErrInvalidKeyType | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func parseRSAPublicKey(m map[string]string) (*RSAPublicKey, error) { | ||||||
|  | 	// TODO grab expiry? | ||||||
|  | 	kid, _ := m["kid"] | ||||||
|  | 	n, _ := base64.RawURLEncoding.DecodeString(m["n"]) | ||||||
|  | 	e, _ := base64.RawURLEncoding.DecodeString(m["e"]) | ||||||
|  | 	if 0 == len(n) || 0 == len(e) { | ||||||
|  | 		return nil, ErrParseJWK | ||||||
|  | 	} | ||||||
|  | 	ni := &big.Int{} | ||||||
|  | 	ni.SetBytes(n) | ||||||
|  | 	ei := &big.Int{} | ||||||
|  | 	ei.SetBytes(e) | ||||||
|  | 
 | ||||||
|  | 	pub := &rsa.PublicKey{ | ||||||
|  | 		N: ni, | ||||||
|  | 		E: int(ei.Int64()), | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	return &RSAPublicKey{ | ||||||
|  | 		PublicKey: pub, | ||||||
|  | 		KID:       kid, | ||||||
|  | 	}, nil | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func parseRSAPrivateKey(m map[string]string) (key *rsa.PrivateKey, err error) { | ||||||
|  | 	pub, err := parseRSAPublicKey(m) | ||||||
|  | 	if nil != err { | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	d, _ := base64.RawURLEncoding.DecodeString(m["d"]) | ||||||
|  | 	p, _ := base64.RawURLEncoding.DecodeString(m["p"]) | ||||||
|  | 	q, _ := base64.RawURLEncoding.DecodeString(m["q"]) | ||||||
|  | 	dp, _ := base64.RawURLEncoding.DecodeString(m["dp"]) | ||||||
|  | 	dq, _ := base64.RawURLEncoding.DecodeString(m["dq"]) | ||||||
|  | 	qinv, _ := base64.RawURLEncoding.DecodeString(m["qi"]) | ||||||
|  | 	if 0 == len(d) || 0 == len(p) || 0 == len(dp) || 0 == len(dq) || 0 == len(qinv) { | ||||||
|  | 		return nil, ErrParseJWK | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	di := &big.Int{} | ||||||
|  | 	di.SetBytes(d) | ||||||
|  | 	pi := &big.Int{} | ||||||
|  | 	pi.SetBytes(p) | ||||||
|  | 	qi := &big.Int{} | ||||||
|  | 	qi.SetBytes(q) | ||||||
|  | 	dpi := &big.Int{} | ||||||
|  | 	dpi.SetBytes(dp) | ||||||
|  | 	dqi := &big.Int{} | ||||||
|  | 	dqi.SetBytes(dq) | ||||||
|  | 	qinvi := &big.Int{} | ||||||
|  | 	qinvi.SetBytes(qinv) | ||||||
|  | 
 | ||||||
|  | 	key = &rsa.PrivateKey{ | ||||||
|  | 		PublicKey: *pub.PublicKey, | ||||||
|  | 		D:         di, | ||||||
|  | 		Primes:    []*big.Int{pi, qi}, | ||||||
|  | 		Precomputed: rsa.PrecomputedValues{ | ||||||
|  | 			Dp:   dpi, | ||||||
|  | 			Dq:   dqi, | ||||||
|  | 			Qinv: qinvi, | ||||||
|  | 		}, | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	return | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func parseECPublicKey(m map[string]string) (*ECPublicKey, error) { | ||||||
|  | 	// TODO grab expiry? | ||||||
|  | 	kid, _ := m["kid"] | ||||||
|  | 	x, _ := base64.RawURLEncoding.DecodeString(m["x"]) | ||||||
|  | 	y, _ := base64.RawURLEncoding.DecodeString(m["y"]) | ||||||
|  | 	if 0 == len(x) || 0 == len(y) || 0 == len(m["crv"]) { | ||||||
|  | 		return nil, ErrParseJWK | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	xi := &big.Int{} | ||||||
|  | 	xi.SetBytes(x) | ||||||
|  | 
 | ||||||
|  | 	yi := &big.Int{} | ||||||
|  | 	yi.SetBytes(y) | ||||||
|  | 
 | ||||||
|  | 	var crv elliptic.Curve | ||||||
|  | 	switch m["crv"] { | ||||||
|  | 	case "P-256": | ||||||
|  | 		crv = elliptic.P256() | ||||||
|  | 	case "P-384": | ||||||
|  | 		crv = elliptic.P384() | ||||||
|  | 	case "P-521": | ||||||
|  | 		crv = elliptic.P521() | ||||||
|  | 	default: | ||||||
|  | 		return nil, ErrInvalidCurve | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	pub := &ecdsa.PublicKey{ | ||||||
|  | 		Curve: crv, | ||||||
|  | 		X:     xi, | ||||||
|  | 		Y:     yi, | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	return &ECPublicKey{ | ||||||
|  | 		PublicKey: pub, | ||||||
|  | 		KID:       kid, | ||||||
|  | 	}, nil | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func parseECPrivateKey(m map[string]string) (*ecdsa.PrivateKey, error) { | ||||||
|  | 	pub, err := parseECPublicKey(m) | ||||||
|  | 	if nil != err { | ||||||
|  | 		return nil, err | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	d, _ := base64.RawURLEncoding.DecodeString(m["d"]) | ||||||
|  | 	if 0 == len(d) { | ||||||
|  | 		return nil, ErrParseJWK | ||||||
|  | 	} | ||||||
|  | 	di := &big.Int{} | ||||||
|  | 	di.SetBytes(d) | ||||||
|  | 
 | ||||||
|  | 	return &ecdsa.PrivateKey{ | ||||||
|  | 		PublicKey: *pub.PublicKey, | ||||||
|  | 		D:         di, | ||||||
|  | 	}, nil | ||||||
|  | } | ||||||
							
								
								
									
										171
									
								
								vendor/git.rootprojects.org/root/keypairs/marshal.go
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										171
									
								
								vendor/git.rootprojects.org/root/keypairs/marshal.go
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							| @ -0,0 +1,171 @@ | |||||||
|  | package keypairs | ||||||
|  | 
 | ||||||
|  | import ( | ||||||
|  | 	"crypto" | ||||||
|  | 	"crypto/ecdsa" | ||||||
|  | 	"crypto/rsa" | ||||||
|  | 	"crypto/x509" | ||||||
|  | 	"encoding/base64" | ||||||
|  | 	"encoding/pem" | ||||||
|  | 	"fmt" | ||||||
|  | 	"log" | ||||||
|  | 	"math/big" | ||||||
|  | 	mathrand "math/rand" | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | // MarshalPEMPublicKey outputs the given public key as JWK | ||||||
|  | func MarshalPEMPublicKey(pubkey crypto.PublicKey) ([]byte, error) { | ||||||
|  | 	block, err := marshalDERPublicKey(pubkey) | ||||||
|  | 	if nil != err { | ||||||
|  | 		return nil, err | ||||||
|  | 	} | ||||||
|  | 	return pem.EncodeToMemory(block), nil | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // MarshalDERPublicKey outputs the given public key as JWK | ||||||
|  | func MarshalDERPublicKey(pubkey crypto.PublicKey) ([]byte, error) { | ||||||
|  | 	block, err := marshalDERPublicKey(pubkey) | ||||||
|  | 	if nil != err { | ||||||
|  | 		return nil, err | ||||||
|  | 	} | ||||||
|  | 	return block.Bytes, nil | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // marshalDERPublicKey outputs the given public key as JWK | ||||||
|  | func marshalDERPublicKey(pubkey crypto.PublicKey) (*pem.Block, error) { | ||||||
|  | 
 | ||||||
|  | 	var der []byte | ||||||
|  | 	var typ string | ||||||
|  | 	var err error | ||||||
|  | 	switch k := pubkey.(type) { | ||||||
|  | 	case *rsa.PublicKey: | ||||||
|  | 		der = x509.MarshalPKCS1PublicKey(k) | ||||||
|  | 		typ = "RSA PUBLIC KEY" | ||||||
|  | 	case *ecdsa.PublicKey: | ||||||
|  | 		typ = "PUBLIC KEY" | ||||||
|  | 		der, err = x509.MarshalPKIXPublicKey(k) | ||||||
|  | 		if nil != err { | ||||||
|  | 			return nil, err | ||||||
|  | 		} | ||||||
|  | 	default: | ||||||
|  | 		panic("Developer Error: impossible key type") | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	return &pem.Block{ | ||||||
|  | 		Bytes: der, | ||||||
|  | 		Type:  typ, | ||||||
|  | 	}, nil | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // MarshalJWKPrivateKey outputs the given private key as JWK | ||||||
|  | func MarshalJWKPrivateKey(privkey PrivateKey) []byte { | ||||||
|  | 	// thumbprint keys are alphabetically sorted and only include the necessary public parts | ||||||
|  | 	switch k := privkey.(type) { | ||||||
|  | 	case *rsa.PrivateKey: | ||||||
|  | 		return MarshalRSAPrivateKey(k) | ||||||
|  | 	case *ecdsa.PrivateKey: | ||||||
|  | 		return MarshalECPrivateKey(k) | ||||||
|  | 	default: | ||||||
|  | 		// this is unreachable because we know the types that we pass in | ||||||
|  | 		log.Printf("keytype: %t, %+v\n", privkey, privkey) | ||||||
|  | 		panic(ErrInvalidPublicKey) | ||||||
|  | 		//return nil | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // MarshalDERPrivateKey outputs the given private key as ASN.1 DER | ||||||
|  | func MarshalDERPrivateKey(privkey PrivateKey) ([]byte, error) { | ||||||
|  | 	// thumbprint keys are alphabetically sorted and only include the necessary public parts | ||||||
|  | 	switch k := privkey.(type) { | ||||||
|  | 	case *rsa.PrivateKey: | ||||||
|  | 		return x509.MarshalPKCS1PrivateKey(k), nil | ||||||
|  | 	case *ecdsa.PrivateKey: | ||||||
|  | 		return x509.MarshalECPrivateKey(k) | ||||||
|  | 	default: | ||||||
|  | 		// this is unreachable because we know the types that we pass in | ||||||
|  | 		log.Printf("keytype: %t, %+v\n", privkey, privkey) | ||||||
|  | 		panic(ErrInvalidPublicKey) | ||||||
|  | 		//return nil, nil | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func marshalDERPrivateKey(privkey PrivateKey) (*pem.Block, error) { | ||||||
|  | 	var typ string | ||||||
|  | 	var bytes []byte | ||||||
|  | 	var err error | ||||||
|  | 
 | ||||||
|  | 	switch k := privkey.(type) { | ||||||
|  | 	case *rsa.PrivateKey: | ||||||
|  | 		if 0 == mathrand.Intn(2) { | ||||||
|  | 			typ = "PRIVATE KEY" | ||||||
|  | 			bytes, err = x509.MarshalPKCS8PrivateKey(k) | ||||||
|  | 			if nil != err { | ||||||
|  | 				return nil, err | ||||||
|  | 			} | ||||||
|  | 		} else { | ||||||
|  | 			typ = "RSA PRIVATE KEY" | ||||||
|  | 			bytes = x509.MarshalPKCS1PrivateKey(k) | ||||||
|  | 		} | ||||||
|  | 		return &pem.Block{ | ||||||
|  | 			Type:  typ, | ||||||
|  | 			Bytes: bytes, | ||||||
|  | 		}, nil | ||||||
|  | 	case *ecdsa.PrivateKey: | ||||||
|  | 		if 0 == mathrand.Intn(2) { | ||||||
|  | 			typ = "PRIVATE KEY" | ||||||
|  | 			bytes, err = x509.MarshalPKCS8PrivateKey(k) | ||||||
|  | 		} else { | ||||||
|  | 			typ = "EC PRIVATE KEY" | ||||||
|  | 			bytes, err = x509.MarshalECPrivateKey(k) | ||||||
|  | 		} | ||||||
|  | 		if nil != err { | ||||||
|  | 			return nil, err | ||||||
|  | 		} | ||||||
|  | 		return &pem.Block{ | ||||||
|  | 			Type:  typ, | ||||||
|  | 			Bytes: bytes, | ||||||
|  | 		}, nil | ||||||
|  | 	default: | ||||||
|  | 		// this is unreachable because we know the types that we pass in | ||||||
|  | 		log.Printf("keytype: %t, %+v\n", privkey, privkey) | ||||||
|  | 		panic(ErrInvalidPublicKey) | ||||||
|  | 		//return nil, nil | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // MarshalPEMPrivateKey outputs the given private key as ASN.1 PEM | ||||||
|  | func MarshalPEMPrivateKey(privkey PrivateKey) ([]byte, error) { | ||||||
|  | 	block, err := marshalDERPrivateKey(privkey) | ||||||
|  | 	if nil != err { | ||||||
|  | 		return nil, err | ||||||
|  | 	} | ||||||
|  | 	return pem.EncodeToMemory(block), nil | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // MarshalECPrivateKey will output the given private key as JWK | ||||||
|  | func MarshalECPrivateKey(k *ecdsa.PrivateKey) []byte { | ||||||
|  | 	crv := k.Curve.Params().Name | ||||||
|  | 	d := base64.RawURLEncoding.EncodeToString(k.D.Bytes()) | ||||||
|  | 	x := base64.RawURLEncoding.EncodeToString(k.X.Bytes()) | ||||||
|  | 	y := base64.RawURLEncoding.EncodeToString(k.Y.Bytes()) | ||||||
|  | 	return []byte(fmt.Sprintf( | ||||||
|  | 		`{"crv":%q,"d":%q,"kty":"EC","x":%q,"y":%q}`, | ||||||
|  | 		crv, d, x, y, | ||||||
|  | 	)) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // MarshalRSAPrivateKey will output the given private key as JWK | ||||||
|  | func MarshalRSAPrivateKey(pk *rsa.PrivateKey) []byte { | ||||||
|  | 	e := base64.RawURLEncoding.EncodeToString(big.NewInt(int64(pk.E)).Bytes()) | ||||||
|  | 	n := base64.RawURLEncoding.EncodeToString(pk.N.Bytes()) | ||||||
|  | 	d := base64.RawURLEncoding.EncodeToString(pk.D.Bytes()) | ||||||
|  | 	p := base64.RawURLEncoding.EncodeToString(pk.Primes[0].Bytes()) | ||||||
|  | 	q := base64.RawURLEncoding.EncodeToString(pk.Primes[1].Bytes()) | ||||||
|  | 	dp := base64.RawURLEncoding.EncodeToString(pk.Precomputed.Dp.Bytes()) | ||||||
|  | 	dq := base64.RawURLEncoding.EncodeToString(pk.Precomputed.Dq.Bytes()) | ||||||
|  | 	qi := base64.RawURLEncoding.EncodeToString(pk.Precomputed.Qinv.Bytes()) | ||||||
|  | 	return []byte(fmt.Sprintf( | ||||||
|  | 		`{"d":%q,"dp":%q,"dq":%q,"e":%q,"kty":"RSA","n":%q,"p":%q,"q":%q,"qi":%q}`, | ||||||
|  | 		d, dp, dq, e, n, p, q, qi, | ||||||
|  | 	)) | ||||||
|  | } | ||||||
							
								
								
									
										46
									
								
								vendor/git.rootprojects.org/root/keypairs/mock.go
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										46
									
								
								vendor/git.rootprojects.org/root/keypairs/mock.go
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							| @ -0,0 +1,46 @@ | |||||||
|  | package keypairs | ||||||
|  | 
 | ||||||
|  | import ( | ||||||
|  | 	"crypto/rsa" | ||||||
|  | 	"io" | ||||||
|  | 	"log" | ||||||
|  | 	mathrand "math/rand" | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | // this shananigans is only for testing and debug API stuff | ||||||
|  | func (o *keyOptions) maybeMockReader() io.Reader { | ||||||
|  | 	if !allowMocking { | ||||||
|  | 		panic("mock method called when mocking is not allowed") | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	if 0 == o.mockSeed { | ||||||
|  | 		return randReader | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	log.Println("WARNING: MOCK: using insecure reader") | ||||||
|  | 	return mathrand.New(mathrand.NewSource(o.mockSeed)) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | const maxRetry = 16 | ||||||
|  | 
 | ||||||
|  | func maybeDerandomizeMockKey(privkey PrivateKey, keylen int, opts *keyOptions) PrivateKey { | ||||||
|  | 	if 0 != opts.mockSeed { | ||||||
|  | 		for i := 0; i < maxRetry; i++ { | ||||||
|  | 			otherkey, _ := rsa.GenerateKey(opts.nextReader(), keylen) | ||||||
|  | 			otherCmp := otherkey.D.Cmp(privkey.(*rsa.PrivateKey).D) | ||||||
|  | 			if 0 != otherCmp { | ||||||
|  | 				// There are two possible keys, choose the lesser D value | ||||||
|  | 				// See https://github.com/square/go-jose/issues/189 | ||||||
|  | 				if otherCmp < 0 { | ||||||
|  | 					privkey = otherkey | ||||||
|  | 				} | ||||||
|  | 				break | ||||||
|  | 			} | ||||||
|  | 			if maxRetry == i-1 { | ||||||
|  | 				log.Printf("error: coinflip landed on heads %d times", maxRetry) | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	return privkey | ||||||
|  | } | ||||||
							
								
								
									
										165
									
								
								vendor/git.rootprojects.org/root/keypairs/sign.go
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										165
									
								
								vendor/git.rootprojects.org/root/keypairs/sign.go
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							| @ -0,0 +1,165 @@ | |||||||
|  | package keypairs | ||||||
|  | 
 | ||||||
|  | import ( | ||||||
|  | 	"crypto" | ||||||
|  | 	"crypto/ecdsa" | ||||||
|  | 	"crypto/rsa" | ||||||
|  | 	"crypto/sha256" | ||||||
|  | 	"encoding/base64" | ||||||
|  | 	"encoding/json" | ||||||
|  | 	"errors" | ||||||
|  | 	"fmt" | ||||||
|  | 	"io" | ||||||
|  | 	mathrand "math/rand" // to be used for good, not evil | ||||||
|  | 	"time" | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | // Object is a type alias representing generic JSON data | ||||||
|  | type Object = map[string]interface{} | ||||||
|  | 
 | ||||||
|  | // SignClaims adds `typ`, `kid` (or `jwk`), and `alg` in the header and expects claims for `jti`, `exp`, `iss`, and `iat` | ||||||
|  | func SignClaims(privkey PrivateKey, header Object, claims Object) (*JWS, error) { | ||||||
|  | 	var randsrc io.Reader = randReader | ||||||
|  | 	seed, _ := header["_seed"].(int64) | ||||||
|  | 	if 0 != seed { | ||||||
|  | 		randsrc = mathrand.New(mathrand.NewSource(seed)) | ||||||
|  | 		//delete(header, "_seed") | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	protected, header, err := headerToProtected(NewPublicKey(privkey.Public()), header) | ||||||
|  | 	if nil != err { | ||||||
|  | 		return nil, err | ||||||
|  | 	} | ||||||
|  | 	protected64 := base64.RawURLEncoding.EncodeToString(protected) | ||||||
|  | 
 | ||||||
|  | 	payload, err := claimsToPayload(claims) | ||||||
|  | 	if nil != err { | ||||||
|  | 		return nil, err | ||||||
|  | 	} | ||||||
|  | 	payload64 := base64.RawURLEncoding.EncodeToString(payload) | ||||||
|  | 
 | ||||||
|  | 	signable := fmt.Sprintf(`%s.%s`, protected64, payload64) | ||||||
|  | 	hash := sha256.Sum256([]byte(signable)) | ||||||
|  | 
 | ||||||
|  | 	sig := Sign(privkey, hash[:], randsrc) | ||||||
|  | 	sig64 := base64.RawURLEncoding.EncodeToString(sig) | ||||||
|  | 	//log.Printf("\n(Sign)\nSignable: %s", signable) | ||||||
|  | 	//log.Printf("Hash: %s", hash) | ||||||
|  | 	//log.Printf("Sig: %s", sig64) | ||||||
|  | 
 | ||||||
|  | 	return &JWS{ | ||||||
|  | 		Header:    header, | ||||||
|  | 		Claims:    claims, | ||||||
|  | 		Protected: protected64, | ||||||
|  | 		Payload:   payload64, | ||||||
|  | 		Signature: sig64, | ||||||
|  | 	}, nil | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func headerToProtected(pub PublicKey, header Object) ([]byte, Object, error) { | ||||||
|  | 	if nil == header { | ||||||
|  | 		header = Object{} | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	// Only supporting 2048-bit and P256 keys right now | ||||||
|  | 	// because that's all that's practical and well-supported. | ||||||
|  | 	// No security theatre here. | ||||||
|  | 	alg := "ES256" | ||||||
|  | 	switch pub.Key().(type) { | ||||||
|  | 	case *rsa.PublicKey: | ||||||
|  | 		alg = "RS256" | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	if selfSign, _ := header["_jwk"].(bool); selfSign { | ||||||
|  | 		delete(header, "_jwk") | ||||||
|  | 		any := Object{} | ||||||
|  | 		_ = json.Unmarshal(MarshalJWKPublicKey(pub), &any) | ||||||
|  | 		header["jwk"] = any | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	// TODO what are the acceptable values? JWT. JWS? others? | ||||||
|  | 	header["typ"] = "JWT" | ||||||
|  | 	if _, ok := header["jwk"]; !ok { | ||||||
|  | 		thumbprint := ThumbprintPublicKey(pub) | ||||||
|  | 		kid, _ := header["kid"].(string) | ||||||
|  | 		if "" != kid && thumbprint != kid { | ||||||
|  | 			return nil, nil, errors.New("'kid' should be the key's thumbprint") | ||||||
|  | 		} | ||||||
|  | 		header["kid"] = thumbprint | ||||||
|  | 	} | ||||||
|  | 	header["alg"] = alg | ||||||
|  | 
 | ||||||
|  | 	protected, err := json.Marshal(header) | ||||||
|  | 	if nil != err { | ||||||
|  | 		return nil, nil, err | ||||||
|  | 	} | ||||||
|  | 	return protected, header, nil | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func claimsToPayload(claims Object) ([]byte, error) { | ||||||
|  | 	if nil == claims { | ||||||
|  | 		claims = Object{} | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	var dur time.Duration | ||||||
|  | 	jti, _ := claims["jti"].(string) | ||||||
|  | 	insecure, _ := claims["insecure"].(bool) | ||||||
|  | 
 | ||||||
|  | 	switch exp := claims["exp"].(type) { | ||||||
|  | 	case time.Duration: | ||||||
|  | 		// TODO: MUST this go first? | ||||||
|  | 		// int64(time.Duration) vs time.Duration(int64) | ||||||
|  | 		dur = exp | ||||||
|  | 	case string: | ||||||
|  | 		var err error | ||||||
|  | 		dur, err = time.ParseDuration(exp) | ||||||
|  | 		// TODO s, err := time.ParseDuration(dur) | ||||||
|  | 		if nil != err { | ||||||
|  | 			return nil, err | ||||||
|  | 		} | ||||||
|  | 	case int: | ||||||
|  | 		dur = time.Second * time.Duration(exp) | ||||||
|  | 	case int64: | ||||||
|  | 		dur = time.Second * time.Duration(exp) | ||||||
|  | 	case float64: | ||||||
|  | 		dur = time.Second * time.Duration(exp) | ||||||
|  | 	default: | ||||||
|  | 		dur = 0 | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	if "" == jti && 0 == dur && !insecure { | ||||||
|  | 		return nil, errors.New("token must have jti or exp as to be expirable / cancellable") | ||||||
|  | 	} | ||||||
|  | 	claims["exp"] = time.Now().Add(dur).Unix() | ||||||
|  | 
 | ||||||
|  | 	return json.Marshal(claims) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // Sign signs both RSA and ECDSA. Use `nil` or `crypto/rand.Reader` except for debugging. | ||||||
|  | func Sign(privkey PrivateKey, hash []byte, rand io.Reader) []byte { | ||||||
|  | 	if nil == rand { | ||||||
|  | 		rand = randReader | ||||||
|  | 	} | ||||||
|  | 	var sig []byte | ||||||
|  | 
 | ||||||
|  | 	if len(hash) != 32 { | ||||||
|  | 		panic("only 256-bit hashes for 2048-bit and 256-bit keys are supported") | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	switch k := privkey.(type) { | ||||||
|  | 	case *rsa.PrivateKey: | ||||||
|  | 		sig, _ = rsa.SignPKCS1v15(rand, k, crypto.SHA256, hash) | ||||||
|  | 	case *ecdsa.PrivateKey: | ||||||
|  | 		r, s, _ := ecdsa.Sign(rand, k, hash[:]) | ||||||
|  | 		rb := r.Bytes() | ||||||
|  | 		for len(rb) < 32 { | ||||||
|  | 			rb = append([]byte{0}, rb...) | ||||||
|  | 		} | ||||||
|  | 		sb := s.Bytes() | ||||||
|  | 		for len(rb) < 32 { | ||||||
|  | 			sb = append([]byte{0}, sb...) | ||||||
|  | 		} | ||||||
|  | 		sig = append(rb, sb...) | ||||||
|  | 	} | ||||||
|  | 	return sig | ||||||
|  | } | ||||||
							
								
								
									
										174
									
								
								vendor/git.rootprojects.org/root/keypairs/verify.go
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										174
									
								
								vendor/git.rootprojects.org/root/keypairs/verify.go
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							| @ -0,0 +1,174 @@ | |||||||
|  | package keypairs | ||||||
|  | 
 | ||||||
|  | import ( | ||||||
|  | 	"crypto" | ||||||
|  | 	"crypto/ecdsa" | ||||||
|  | 	"crypto/rsa" | ||||||
|  | 	"crypto/sha256" | ||||||
|  | 	"crypto/subtle" | ||||||
|  | 	"encoding/base64" | ||||||
|  | 	"errors" | ||||||
|  | 	"fmt" | ||||||
|  | 	"log" | ||||||
|  | 	"math/big" | ||||||
|  | 	"time" | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | // VerifyClaims will check the signature of a parsed JWT | ||||||
|  | func VerifyClaims(pubkey PublicKey, jws *JWS) (errs []error) { | ||||||
|  | 	kid, _ := jws.Header["kid"].(string) | ||||||
|  | 	jwkmap, hasJWK := jws.Header["jwk"].(Object) | ||||||
|  | 	//var jwk JWK = nil | ||||||
|  | 
 | ||||||
|  | 	seed, _ := jws.Header["_seed"].(int64) | ||||||
|  | 	seedf64, _ := jws.Header["_seed"].(float64) | ||||||
|  | 	kty, _ := jws.Header["_kty"].(string) | ||||||
|  | 	if 0 == seed { | ||||||
|  | 		seed = int64(seedf64) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	var pub PublicKey = nil | ||||||
|  | 	if hasJWK { | ||||||
|  | 		pub, errs = selfsignCheck(jwkmap, errs) | ||||||
|  | 	} else { | ||||||
|  | 		opts := &keyOptions{mockSeed: seed, KeyType: kty} | ||||||
|  | 		pub, errs = pubkeyCheck(pubkey, kid, opts, errs) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	jti, _ := jws.Claims["jti"].(string) | ||||||
|  | 	expf64, _ := jws.Claims["exp"].(float64) | ||||||
|  | 	exp := int64(expf64) | ||||||
|  | 	if 0 == exp { | ||||||
|  | 		if "" == jti { | ||||||
|  | 			err := errors.New("one of 'jti' or 'exp' must exist for token expiry") | ||||||
|  | 			errs = append(errs, err) | ||||||
|  | 		} | ||||||
|  | 	} else { | ||||||
|  | 		if time.Now().Unix() > exp { | ||||||
|  | 			err := fmt.Errorf("token expired at %d (%s)", exp, time.Unix(exp, 0)) | ||||||
|  | 			errs = append(errs, err) | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	signable := fmt.Sprintf("%s.%s", jws.Protected, jws.Payload) | ||||||
|  | 	hash := sha256.Sum256([]byte(signable)) | ||||||
|  | 	sig, err := base64.RawURLEncoding.DecodeString(jws.Signature) | ||||||
|  | 	if nil != err { | ||||||
|  | 		err := fmt.Errorf("could not decode signature: %w", err) | ||||||
|  | 		errs = append(errs, err) | ||||||
|  | 		return errs | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	//log.Printf("\n(Verify)\nSignable: %s", signable) | ||||||
|  | 	//log.Printf("Hash: %s", hash) | ||||||
|  | 	//log.Printf("Sig: %s", jws.Signature) | ||||||
|  | 	if nil == pub { | ||||||
|  | 		err := fmt.Errorf("token signature could not be verified") | ||||||
|  | 		errs = append(errs, err) | ||||||
|  | 	} else if !Verify(pub, hash[:], sig) { | ||||||
|  | 		err := fmt.Errorf("token signature is not valid") | ||||||
|  | 		errs = append(errs, err) | ||||||
|  | 	} | ||||||
|  | 	return errs | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func selfsignCheck(jwkmap Object, errs []error) (PublicKey, []error) { | ||||||
|  | 	var pub PublicKey = nil | ||||||
|  | 	log.Println("Security TODO: did not check jws.Claims[\"sub\"] against 'jwk'") | ||||||
|  | 	log.Println("Security TODO: did not check jws.Claims[\"iss\"]") | ||||||
|  | 	kty := jwkmap["kty"] | ||||||
|  | 	var err error | ||||||
|  | 	if "RSA" == kty { | ||||||
|  | 		e, _ := jwkmap["e"].(string) | ||||||
|  | 		n, _ := jwkmap["n"].(string) | ||||||
|  | 		k, _ := (&RSAJWK{ | ||||||
|  | 			Exp: e, | ||||||
|  | 			N:   n, | ||||||
|  | 		}).marshalJWK() | ||||||
|  | 		pub, err = ParseJWKPublicKey(k) | ||||||
|  | 		if nil != err { | ||||||
|  | 			return nil, append(errs, err) | ||||||
|  | 		} | ||||||
|  | 	} else { | ||||||
|  | 		crv, _ := jwkmap["crv"].(string) | ||||||
|  | 		x, _ := jwkmap["x"].(string) | ||||||
|  | 		y, _ := jwkmap["y"].(string) | ||||||
|  | 		k, _ := (&ECJWK{ | ||||||
|  | 			Curve: crv, | ||||||
|  | 			X:     x, | ||||||
|  | 			Y:     y, | ||||||
|  | 		}).marshalJWK() | ||||||
|  | 		pub, err = ParseJWKPublicKey(k) | ||||||
|  | 		if nil != err { | ||||||
|  | 			return nil, append(errs, err) | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	return pub, errs | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func pubkeyCheck(pubkey PublicKey, kid string, opts *keyOptions, errs []error) (PublicKey, []error) { | ||||||
|  | 	var pub PublicKey = nil | ||||||
|  | 
 | ||||||
|  | 	if "" == kid { | ||||||
|  | 		err := errors.New("token should have 'kid' or 'jwk' in header to identify the public key") | ||||||
|  | 		errs = append(errs, err) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	if nil == pubkey { | ||||||
|  | 		if allowMocking { | ||||||
|  | 			if 0 == opts.mockSeed { | ||||||
|  | 				err := errors.New("the debug API requires '_seed' to accompany 'kid'") | ||||||
|  | 				errs = append(errs, err) | ||||||
|  | 			} | ||||||
|  | 			if "" == opts.KeyType { | ||||||
|  | 				err := errors.New("the debug API requires '_kty' to accompany '_seed'") | ||||||
|  | 				errs = append(errs, err) | ||||||
|  | 			} | ||||||
|  | 
 | ||||||
|  | 			if 0 == opts.mockSeed || "" == opts.KeyType { | ||||||
|  | 				return nil, errs | ||||||
|  | 			} | ||||||
|  | 			privkey := newPrivateKey(opts) | ||||||
|  | 			pub = NewPublicKey(privkey.Public()) | ||||||
|  | 			return pub, errs | ||||||
|  | 		} | ||||||
|  | 		err := errors.New("no matching public key") | ||||||
|  | 		errs = append(errs, err) | ||||||
|  | 	} else { | ||||||
|  | 		pub = pubkey | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	if nil != pub && "" != kid { | ||||||
|  | 		if 1 != subtle.ConstantTimeCompare([]byte(kid), []byte(pub.Thumbprint())) { | ||||||
|  | 			err := errors.New("'kid' does not match the public key thumbprint") | ||||||
|  | 			errs = append(errs, err) | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 	return pub, errs | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // Verify will check the signature of a hash | ||||||
|  | func Verify(pubkey PublicKey, hash []byte, sig []byte) bool { | ||||||
|  | 
 | ||||||
|  | 	switch pub := pubkey.Key().(type) { | ||||||
|  | 	case *rsa.PublicKey: | ||||||
|  | 		//log.Printf("RSA VERIFY") | ||||||
|  | 		// TODO Size(key) to detect key size ? | ||||||
|  | 		//alg := "SHA256" | ||||||
|  | 		// TODO: this hasn't been tested yet | ||||||
|  | 		if err := rsa.VerifyPKCS1v15(pub, crypto.SHA256, hash, sig); nil != err { | ||||||
|  | 			return false | ||||||
|  | 		} | ||||||
|  | 		return true | ||||||
|  | 	case *ecdsa.PublicKey: | ||||||
|  | 		r := &big.Int{} | ||||||
|  | 		r.SetBytes(sig[0:32]) | ||||||
|  | 		s := &big.Int{} | ||||||
|  | 		s.SetBytes(sig[32:]) | ||||||
|  | 		return ecdsa.Verify(pub, hash, r, s) | ||||||
|  | 	default: | ||||||
|  | 		panic("impossible condition: non-rsa/non-ecdsa key") | ||||||
|  | 		//return false | ||||||
|  | 	} | ||||||
|  | } | ||||||
							
								
								
									
										3
									
								
								vendor/github.com/go-chi/chi/.gitignore
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								vendor/github.com/go-chi/chi/.gitignore
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							| @ -0,0 +1,3 @@ | |||||||
|  | .idea | ||||||
|  | *.sw? | ||||||
|  | .vscode | ||||||
							
								
								
									
										17
									
								
								vendor/github.com/go-chi/chi/.travis.yml
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								vendor/github.com/go-chi/chi/.travis.yml
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							| @ -0,0 +1,17 @@ | |||||||
|  | language: go | ||||||
|  | 
 | ||||||
|  | go: | ||||||
|  |   - 1.10.x | ||||||
|  |   - 1.11.x | ||||||
|  | 
 | ||||||
|  | script: | ||||||
|  |   - go get -d -t ./... | ||||||
|  |   - go vet ./... | ||||||
|  |   - go test ./... | ||||||
|  |   - > | ||||||
|  |     go_version=$(go version); | ||||||
|  |     if [ ${go_version:13:4} = "1.11" ]; then | ||||||
|  |       go get -u golang.org/x/tools/cmd/goimports; | ||||||
|  |       goimports -d -e ./ | grep '.*' && { echo; echo "Aborting due to non-empty goimports output."; exit 1; } || :; | ||||||
|  |     fi | ||||||
|  | 
 | ||||||
							
								
								
									
										139
									
								
								vendor/github.com/go-chi/chi/CHANGELOG.md
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										139
									
								
								vendor/github.com/go-chi/chi/CHANGELOG.md
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							| @ -0,0 +1,139 @@ | |||||||
|  | # Changelog | ||||||
|  | 
 | ||||||
|  | ## v4.0.0 (2019-01-10) | ||||||
|  | 
 | ||||||
|  | - chi v4 requires Go 1.10.3+ (or Go 1.9.7+) - we have deprecated support for Go 1.7 and 1.8 | ||||||
|  | - router: respond with 404 on router with no routes (#362) | ||||||
|  | - router: additional check to ensure wildcard is at the end of a url pattern (#333) | ||||||
|  | - middleware: deprecate use of http.CloseNotifier (#347) | ||||||
|  | - middleware: fix RedirectSlashes to include query params on redirect (#334) | ||||||
|  | - History of changes: see https://github.com/go-chi/chi/compare/v3.3.4...v4.0.0 | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | ## v3.3.4 (2019-01-07) | ||||||
|  | 
 | ||||||
|  | - Minor middleware improvements. No changes to core library/router. Moving v3 into its | ||||||
|  | - own branch as a version of chi for Go 1.7, 1.8, 1.9, 1.10, 1.11 | ||||||
|  | - History of changes: see https://github.com/go-chi/chi/compare/v3.3.3...v3.3.4 | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | ## v3.3.3 (2018-08-27) | ||||||
|  | 
 | ||||||
|  | - Minor release | ||||||
|  | - See https://github.com/go-chi/chi/compare/v3.3.2...v3.3.3 | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | ## v3.3.2 (2017-12-22) | ||||||
|  | 
 | ||||||
|  | - Support to route trailing slashes on mounted sub-routers (#281) | ||||||
|  | - middleware: new `ContentCharset` to check matching charsets. Thank you | ||||||
|  |   @csucu for your community contribution! | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | ## v3.3.1 (2017-11-20) | ||||||
|  | 
 | ||||||
|  | - middleware: new `AllowContentType` handler for explicit whitelist of accepted request Content-Types | ||||||
|  | - middleware: new `SetHeader` handler for short-hand middleware to set a response header key/value | ||||||
|  | - Minor bug fixes | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | ## v3.3.0 (2017-10-10) | ||||||
|  | 
 | ||||||
|  | - New chi.RegisterMethod(method) to add support for custom HTTP methods, see _examples/custom-method for usage | ||||||
|  | - Deprecated LINK and UNLINK methods from the default list, please use `chi.RegisterMethod("LINK")` and `chi.RegisterMethod("UNLINK")` in an `init()` function | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | ## v3.2.1 (2017-08-31) | ||||||
|  | 
 | ||||||
|  | - Add new `Match(rctx *Context, method, path string) bool` method to `Routes` interface | ||||||
|  |   and `Mux`. Match searches the mux's routing tree for a handler that matches the method/path | ||||||
|  | - Add new `RouteMethod` to `*Context` | ||||||
|  | - Add new `Routes` pointer to `*Context` | ||||||
|  | - Add new `middleware.GetHead` to route missing HEAD requests to GET handler | ||||||
|  | - Updated benchmarks (see README) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | ## v3.1.5 (2017-08-02) | ||||||
|  | 
 | ||||||
|  | - Setup golint and go vet for the project | ||||||
|  | - As per golint, we've redefined `func ServerBaseContext(h http.Handler, baseCtx context.Context) http.Handler` | ||||||
|  |   to `func ServerBaseContext(baseCtx context.Context, h http.Handler) http.Handler` | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | ## v3.1.0 (2017-07-10) | ||||||
|  | 
 | ||||||
|  | - Fix a few minor issues after v3 release | ||||||
|  | - Move `docgen` sub-pkg to https://github.com/go-chi/docgen | ||||||
|  | - Move `render` sub-pkg to https://github.com/go-chi/render | ||||||
|  | - Add new `URLFormat` handler to chi/middleware sub-pkg to make working with url mime  | ||||||
|  |   suffixes easier, ie. parsing `/articles/1.json` and `/articles/1.xml`. See comments in | ||||||
|  |   https://github.com/go-chi/chi/blob/master/middleware/url_format.go for example usage. | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | ## v3.0.0 (2017-06-21) | ||||||
|  | 
 | ||||||
|  | - Major update to chi library with many exciting updates, but also some *breaking changes* | ||||||
|  | - URL parameter syntax changed from `/:id` to `/{id}` for even more flexible routing, such as | ||||||
|  |   `/articles/{month}-{day}-{year}-{slug}`, `/articles/{id}`, and `/articles/{id}.{ext}` on the | ||||||
|  |   same router | ||||||
|  | - Support for regexp for routing patterns, in the form of `/{paramKey:regExp}` for example: | ||||||
|  |   `r.Get("/articles/{name:[a-z]+}", h)` and `chi.URLParam(r, "name")` | ||||||
|  | - Add `Method` and `MethodFunc` to `chi.Router` to allow routing definitions such as | ||||||
|  |   `r.Method("GET", "/", h)` which provides a cleaner interface for custom handlers like | ||||||
|  |   in `_examples/custom-handler` | ||||||
|  | - Deprecating `mux#FileServer` helper function. Instead, we encourage users to create their | ||||||
|  |   own using file handler with the stdlib, see `_examples/fileserver` for an example | ||||||
|  | - Add support for LINK/UNLINK http methods via `r.Method()` and `r.MethodFunc()` | ||||||
|  | - Moved the chi project to its own organization, to allow chi-related community packages to | ||||||
|  |   be easily discovered and supported, at: https://github.com/go-chi | ||||||
|  | - *NOTE:* please update your import paths to `"github.com/go-chi/chi"` | ||||||
|  | - *NOTE:* chi v2 is still available at https://github.com/go-chi/chi/tree/v2 | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | ## v2.1.0 (2017-03-30) | ||||||
|  | 
 | ||||||
|  | - Minor improvements and update to the chi core library | ||||||
|  | - Introduced a brand new `chi/render` sub-package to complete the story of building | ||||||
|  |   APIs to offer a pattern for managing well-defined request / response payloads. Please | ||||||
|  |   check out the updated `_examples/rest` example for how it works. | ||||||
|  | - Added `MethodNotAllowed(h http.HandlerFunc)` to chi.Router interface | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | ## v2.0.0 (2017-01-06) | ||||||
|  | 
 | ||||||
|  | - After many months of v2 being in an RC state with many companies and users running it in | ||||||
|  |   production, the inclusion of some improvements to the middlewares, we are very pleased to | ||||||
|  |   announce v2.0.0 of chi. | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | ## v2.0.0-rc1 (2016-07-26) | ||||||
|  | 
 | ||||||
|  | - Huge update! chi v2 is a large refactor targetting Go 1.7+. As of Go 1.7, the popular | ||||||
|  |   community `"net/context"` package has been included in the standard library as `"context"` and | ||||||
|  |   utilized by `"net/http"` and `http.Request` to managing deadlines, cancelation signals and other | ||||||
|  |   request-scoped values. We're very excited about the new context addition and are proud to | ||||||
|  |   introduce chi v2, a minimal and powerful routing package for building large HTTP services, | ||||||
|  |   with zero external dependencies. Chi focuses on idiomatic design and encourages the use of  | ||||||
|  |   stdlib HTTP handlers and middlwares. | ||||||
|  | - chi v2 deprecates its `chi.Handler` interface and requires `http.Handler` or `http.HandlerFunc` | ||||||
|  | - chi v2 stores URL routing parameters and patterns in the standard request context: `r.Context()` | ||||||
|  | - chi v2 lower-level routing context is accessible by `chi.RouteContext(r.Context()) *chi.Context`, | ||||||
|  |   which provides direct access to URL routing parameters, the routing path and the matching | ||||||
|  |   routing patterns. | ||||||
|  | - Users upgrading from chi v1 to v2, need to: | ||||||
|  |   1. Update the old chi.Handler signature, `func(ctx context.Context, w http.ResponseWriter, r *http.Request)` to | ||||||
|  |      the standard http.Handler: `func(w http.ResponseWriter, r *http.Request)` | ||||||
|  |   2. Use `chi.URLParam(r *http.Request, paramKey string) string` | ||||||
|  |      or `URLParamFromCtx(ctx context.Context, paramKey string) string` to access a url parameter value | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | ## v1.0.0 (2016-07-01) | ||||||
|  | 
 | ||||||
|  | - Released chi v1 stable https://github.com/go-chi/chi/tree/v1.0.0 for Go 1.6 and older. | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | ## v0.9.0 (2016-03-31) | ||||||
|  | 
 | ||||||
|  | - Reuse context objects via sync.Pool for zero-allocation routing [#33](https://github.com/go-chi/chi/pull/33) | ||||||
|  | - BREAKING NOTE: due to subtle API changes, previously `chi.URLParams(ctx)["id"]` used to access url parameters | ||||||
|  |   has changed to: `chi.URLParam(ctx, "id")` | ||||||
							
								
								
									
										31
									
								
								vendor/github.com/go-chi/chi/CONTRIBUTING.md
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										31
									
								
								vendor/github.com/go-chi/chi/CONTRIBUTING.md
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							| @ -0,0 +1,31 @@ | |||||||
|  | # Contributing | ||||||
|  | 
 | ||||||
|  | ## Prerequisites | ||||||
|  | 
 | ||||||
|  | 1. [Install Go][go-install]. | ||||||
|  | 2. Download the sources and switch the working directory: | ||||||
|  | 
 | ||||||
|  |     ```bash | ||||||
|  |     go get -u -d github.com/go-chi/chi | ||||||
|  |     cd $GOPATH/src/github.com/go-chi/chi | ||||||
|  |     ``` | ||||||
|  | 
 | ||||||
|  | ## Submitting a Pull Request | ||||||
|  | 
 | ||||||
|  | A typical workflow is: | ||||||
|  | 
 | ||||||
|  | 1. [Fork the repository.][fork] [This tip maybe also helpful.][go-fork-tip] | ||||||
|  | 2. [Create a topic branch.][branch] | ||||||
|  | 3. Add tests for your change. | ||||||
|  | 4. Run `go test`. If your tests pass, return to the step 3. | ||||||
|  | 5. Implement the change and ensure the steps from the previous step pass. | ||||||
|  | 6. Run `goimports -w .`, to ensure the new code conforms to Go formatting guideline. | ||||||
|  | 7. [Add, commit and push your changes.][git-help] | ||||||
|  | 8. [Submit a pull request.][pull-req] | ||||||
|  | 
 | ||||||
|  | [go-install]: https://golang.org/doc/install | ||||||
|  | [go-fork-tip]: http://blog.campoy.cat/2014/03/github-and-go-forking-pull-requests-and.html | ||||||
|  | [fork]: https://help.github.com/articles/fork-a-repo | ||||||
|  | [branch]: http://learn.github.com/p/branching.html | ||||||
|  | [git-help]: https://guides.github.com | ||||||
|  | [pull-req]: https://help.github.com/articles/using-pull-requests | ||||||
							
								
								
									
										20
									
								
								vendor/github.com/go-chi/chi/LICENSE
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								vendor/github.com/go-chi/chi/LICENSE
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							| @ -0,0 +1,20 @@ | |||||||
|  | Copyright (c) 2015-present Peter Kieltyka (https://github.com/pkieltyka), Google Inc. | ||||||
|  | 
 | ||||||
|  | MIT License | ||||||
|  | 
 | ||||||
|  | Permission is hereby granted, free of charge, to any person obtaining a copy of | ||||||
|  | this software and associated documentation files (the "Software"), to deal in | ||||||
|  | the Software without restriction, including without limitation the rights to | ||||||
|  | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of | ||||||
|  | the Software, and to permit persons to whom the Software is furnished to do so, | ||||||
|  | subject to the following conditions: | ||||||
|  | 
 | ||||||
|  | The above copyright notice and this permission notice shall be included in all | ||||||
|  | copies or substantial portions of the Software. | ||||||
|  | 
 | ||||||
|  | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | ||||||
|  | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS | ||||||
|  | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR | ||||||
|  | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER | ||||||
|  | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN | ||||||
|  | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. | ||||||
							
								
								
									
										438
									
								
								vendor/github.com/go-chi/chi/README.md
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										438
									
								
								vendor/github.com/go-chi/chi/README.md
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							| @ -0,0 +1,438 @@ | |||||||
|  | # <img alt="chi" src="https://cdn.rawgit.com/go-chi/chi/master/_examples/chi.svg" width="220" /> | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | [![GoDoc Widget]][GoDoc] [![Travis Widget]][Travis] | ||||||
|  | 
 | ||||||
|  | `chi` is a lightweight, idiomatic and composable router for building Go HTTP services. It's | ||||||
|  | especially good at helping you write large REST API services that are kept maintainable as your | ||||||
|  | project grows and changes. `chi` is built on the new `context` package introduced in Go 1.7 to | ||||||
|  | handle signaling, cancelation and request-scoped values across a handler chain. | ||||||
|  | 
 | ||||||
|  | The focus of the project has been to seek out an elegant and comfortable design for writing | ||||||
|  | REST API servers, written during the development of the Pressly API service that powers our | ||||||
|  | public API service, which in turn powers all of our client-side applications. | ||||||
|  | 
 | ||||||
|  | The key considerations of chi's design are: project structure, maintainability, standard http | ||||||
|  | handlers (stdlib-only), developer productivity, and deconstructing a large system into many small | ||||||
|  | parts. The core router `github.com/go-chi/chi` is quite small (less than 1000 LOC), but we've also | ||||||
|  | included some useful/optional subpackages: [middleware](/middleware), [render](https://github.com/go-chi/render) and [docgen](https://github.com/go-chi/docgen). We hope you enjoy it too! | ||||||
|  | 
 | ||||||
|  | ## Install | ||||||
|  | 
 | ||||||
|  | `go get -u github.com/go-chi/chi` | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | ## Features | ||||||
|  | 
 | ||||||
|  | * **Lightweight** - cloc'd in ~1000 LOC for the chi router | ||||||
|  | * **Fast** - yes, see [benchmarks](#benchmarks) | ||||||
|  | * **100% compatible with net/http** - use any http or middleware pkg in the ecosystem that is also compatible with `net/http` | ||||||
|  | * **Designed for modular/composable APIs** - middlewares, inline middlewares, route groups and subrouter mounting | ||||||
|  | * **Context control** - built on new `context` package, providing value chaining, cancelations and timeouts | ||||||
|  | * **Robust** - in production at Pressly, CloudFlare, Heroku, 99Designs, and many others (see [discussion](https://github.com/go-chi/chi/issues/91)) | ||||||
|  | * **Doc generation** - `docgen` auto-generates routing documentation from your source to JSON or Markdown | ||||||
|  | * **No external dependencies** - plain ol' Go stdlib + net/http | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | ## Examples | ||||||
|  | 
 | ||||||
|  | See [_examples/](https://github.com/go-chi/chi/blob/master/_examples/) for a variety of examples. | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | **As easy as:** | ||||||
|  | 
 | ||||||
|  | ```go | ||||||
|  | package main | ||||||
|  | 
 | ||||||
|  | import ( | ||||||
|  | 	"net/http" | ||||||
|  | 	"github.com/go-chi/chi" | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | func main() { | ||||||
|  | 	r := chi.NewRouter() | ||||||
|  | 	r.Get("/", func(w http.ResponseWriter, r *http.Request) { | ||||||
|  | 		w.Write([]byte("welcome")) | ||||||
|  | 	}) | ||||||
|  | 	http.ListenAndServe(":3000", r) | ||||||
|  | } | ||||||
|  | ``` | ||||||
|  | 
 | ||||||
|  | **REST Preview:** | ||||||
|  | 
 | ||||||
|  | Here is a little preview of how routing looks like with chi. Also take a look at the generated routing docs | ||||||
|  | in JSON ([routes.json](https://github.com/go-chi/chi/blob/master/_examples/rest/routes.json)) and in | ||||||
|  | Markdown ([routes.md](https://github.com/go-chi/chi/blob/master/_examples/rest/routes.md)). | ||||||
|  | 
 | ||||||
|  | I highly recommend reading the source of the [examples](https://github.com/go-chi/chi/blob/master/_examples/) listed | ||||||
|  | above, they will show you all the features of chi and serve as a good form of documentation. | ||||||
|  | 
 | ||||||
|  | ```go | ||||||
|  | import ( | ||||||
|  |   //... | ||||||
|  |   "context" | ||||||
|  |   "github.com/go-chi/chi" | ||||||
|  |   "github.com/go-chi/chi/middleware" | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | func main() { | ||||||
|  |   r := chi.NewRouter() | ||||||
|  | 
 | ||||||
|  |   // A good base middleware stack | ||||||
|  |   r.Use(middleware.RequestID) | ||||||
|  |   r.Use(middleware.RealIP) | ||||||
|  |   r.Use(middleware.Logger) | ||||||
|  |   r.Use(middleware.Recoverer) | ||||||
|  | 
 | ||||||
|  |   // Set a timeout value on the request context (ctx), that will signal | ||||||
|  |   // through ctx.Done() that the request has timed out and further | ||||||
|  |   // processing should be stopped. | ||||||
|  |   r.Use(middleware.Timeout(60 * time.Second)) | ||||||
|  | 
 | ||||||
|  |   r.Get("/", func(w http.ResponseWriter, r *http.Request) { | ||||||
|  |     w.Write([]byte("hi")) | ||||||
|  |   }) | ||||||
|  | 
 | ||||||
|  |   // RESTy routes for "articles" resource | ||||||
|  |   r.Route("/articles", func(r chi.Router) { | ||||||
|  |     r.With(paginate).Get("/", listArticles)                           // GET /articles | ||||||
|  |     r.With(paginate).Get("/{month}-{day}-{year}", listArticlesByDate) // GET /articles/01-16-2017 | ||||||
|  | 
 | ||||||
|  |     r.Post("/", createArticle)                                        // POST /articles | ||||||
|  |     r.Get("/search", searchArticles)                                  // GET /articles/search | ||||||
|  | 
 | ||||||
|  |     // Regexp url parameters: | ||||||
|  |     r.Get("/{articleSlug:[a-z-]+}", getArticleBySlug)                // GET /articles/home-is-toronto | ||||||
|  | 
 | ||||||
|  |     // Subrouters: | ||||||
|  |     r.Route("/{articleID}", func(r chi.Router) { | ||||||
|  |       r.Use(ArticleCtx) | ||||||
|  |       r.Get("/", getArticle)                                          // GET /articles/123 | ||||||
|  |       r.Put("/", updateArticle)                                       // PUT /articles/123 | ||||||
|  |       r.Delete("/", deleteArticle)                                    // DELETE /articles/123 | ||||||
|  |     }) | ||||||
|  |   }) | ||||||
|  | 
 | ||||||
|  |   // Mount the admin sub-router | ||||||
|  |   r.Mount("/admin", adminRouter()) | ||||||
|  | 
 | ||||||
|  |   http.ListenAndServe(":3333", r) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func ArticleCtx(next http.Handler) http.Handler { | ||||||
|  |   return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { | ||||||
|  |     articleID := chi.URLParam(r, "articleID") | ||||||
|  |     article, err := dbGetArticle(articleID) | ||||||
|  |     if err != nil { | ||||||
|  |       http.Error(w, http.StatusText(404), 404) | ||||||
|  |       return | ||||||
|  |     } | ||||||
|  |     ctx := context.WithValue(r.Context(), "article", article) | ||||||
|  |     next.ServeHTTP(w, r.WithContext(ctx)) | ||||||
|  |   }) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func getArticle(w http.ResponseWriter, r *http.Request) { | ||||||
|  |   ctx := r.Context() | ||||||
|  |   article, ok := ctx.Value("article").(*Article) | ||||||
|  |   if !ok { | ||||||
|  |     http.Error(w, http.StatusText(422), 422) | ||||||
|  |     return | ||||||
|  |   } | ||||||
|  |   w.Write([]byte(fmt.Sprintf("title:%s", article.Title))) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // A completely separate router for administrator routes | ||||||
|  | func adminRouter() http.Handler { | ||||||
|  |   r := chi.NewRouter() | ||||||
|  |   r.Use(AdminOnly) | ||||||
|  |   r.Get("/", adminIndex) | ||||||
|  |   r.Get("/accounts", adminListAccounts) | ||||||
|  |   return r | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func AdminOnly(next http.Handler) http.Handler { | ||||||
|  |   return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { | ||||||
|  |     ctx := r.Context() | ||||||
|  |     perm, ok := ctx.Value("acl.permission").(YourPermissionType) | ||||||
|  |     if !ok || !perm.IsAdmin() { | ||||||
|  |       http.Error(w, http.StatusText(403), 403) | ||||||
|  |       return | ||||||
|  |     } | ||||||
|  |     next.ServeHTTP(w, r) | ||||||
|  |   }) | ||||||
|  | } | ||||||
|  | ``` | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | ## Router design | ||||||
|  | 
 | ||||||
|  | chi's router is based on a kind of [Patricia Radix trie](https://en.wikipedia.org/wiki/Radix_tree). | ||||||
|  | The router is fully compatible with `net/http`. | ||||||
|  | 
 | ||||||
|  | Built on top of the tree is the `Router` interface: | ||||||
|  | 
 | ||||||
|  | ```go | ||||||
|  | // Router consisting of the core routing methods used by chi's Mux, | ||||||
|  | // using only the standard net/http. | ||||||
|  | type Router interface { | ||||||
|  | 	http.Handler | ||||||
|  | 	Routes | ||||||
|  | 
 | ||||||
|  | 	// Use appends one of more middlewares onto the Router stack. | ||||||
|  | 	Use(middlewares ...func(http.Handler) http.Handler) | ||||||
|  | 
 | ||||||
|  | 	// With adds inline middlewares for an endpoint handler. | ||||||
|  | 	With(middlewares ...func(http.Handler) http.Handler) Router | ||||||
|  | 
 | ||||||
|  | 	// Group adds a new inline-Router along the current routing | ||||||
|  | 	// path, with a fresh middleware stack for the inline-Router. | ||||||
|  | 	Group(fn func(r Router)) Router | ||||||
|  | 
 | ||||||
|  | 	// Route mounts a sub-Router along a `pattern`` string. | ||||||
|  | 	Route(pattern string, fn func(r Router)) Router | ||||||
|  | 
 | ||||||
|  | 	// Mount attaches another http.Handler along ./pattern/* | ||||||
|  | 	Mount(pattern string, h http.Handler) | ||||||
|  | 
 | ||||||
|  | 	// Handle and HandleFunc adds routes for `pattern` that matches | ||||||
|  | 	// all HTTP methods. | ||||||
|  | 	Handle(pattern string, h http.Handler) | ||||||
|  | 	HandleFunc(pattern string, h http.HandlerFunc) | ||||||
|  | 
 | ||||||
|  | 	// Method and MethodFunc adds routes for `pattern` that matches | ||||||
|  | 	// the `method` HTTP method. | ||||||
|  | 	Method(method, pattern string, h http.Handler) | ||||||
|  | 	MethodFunc(method, pattern string, h http.HandlerFunc) | ||||||
|  | 
 | ||||||
|  | 	// HTTP-method routing along `pattern` | ||||||
|  | 	Connect(pattern string, h http.HandlerFunc) | ||||||
|  | 	Delete(pattern string, h http.HandlerFunc) | ||||||
|  | 	Get(pattern string, h http.HandlerFunc) | ||||||
|  | 	Head(pattern string, h http.HandlerFunc) | ||||||
|  | 	Options(pattern string, h http.HandlerFunc) | ||||||
|  | 	Patch(pattern string, h http.HandlerFunc) | ||||||
|  | 	Post(pattern string, h http.HandlerFunc) | ||||||
|  | 	Put(pattern string, h http.HandlerFunc) | ||||||
|  | 	Trace(pattern string, h http.HandlerFunc) | ||||||
|  | 
 | ||||||
|  | 	// NotFound defines a handler to respond whenever a route could | ||||||
|  | 	// not be found. | ||||||
|  | 	NotFound(h http.HandlerFunc) | ||||||
|  | 
 | ||||||
|  | 	// MethodNotAllowed defines a handler to respond whenever a method is | ||||||
|  | 	// not allowed. | ||||||
|  | 	MethodNotAllowed(h http.HandlerFunc) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // Routes interface adds two methods for router traversal, which is also | ||||||
|  | // used by the github.com/go-chi/docgen package to generate documentation for Routers. | ||||||
|  | type Routes interface { | ||||||
|  | 	// Routes returns the routing tree in an easily traversable structure. | ||||||
|  | 	Routes() []Route | ||||||
|  | 
 | ||||||
|  | 	// Middlewares returns the list of middlewares in use by the router. | ||||||
|  | 	Middlewares() Middlewares | ||||||
|  | 
 | ||||||
|  | 	// Match searches the routing tree for a handler that matches | ||||||
|  | 	// the method/path - similar to routing a http request, but without | ||||||
|  | 	// executing the handler thereafter. | ||||||
|  | 	Match(rctx *Context, method, path string) bool | ||||||
|  | } | ||||||
|  | ``` | ||||||
|  | 
 | ||||||
|  | Each routing method accepts a URL `pattern` and chain of `handlers`. The URL pattern | ||||||
|  | supports named params (ie. `/users/{userID}`) and wildcards (ie. `/admin/*`). URL parameters | ||||||
|  | can be fetched at runtime by calling `chi.URLParam(r, "userID")` for named parameters | ||||||
|  | and `chi.URLParam(r, "*")` for a wildcard parameter. | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | ### Middleware handlers | ||||||
|  | 
 | ||||||
|  | chi's middlewares are just stdlib net/http middleware handlers. There is nothing special | ||||||
|  | about them, which means the router and all the tooling is designed to be compatible and | ||||||
|  | friendly with any middleware in the community. This offers much better extensibility and reuse | ||||||
|  | of packages and is at the heart of chi's purpose. | ||||||
|  | 
 | ||||||
|  | Here is an example of a standard net/http middleware handler using the new request context | ||||||
|  | available in Go. This middleware sets a hypothetical user identifier on the request | ||||||
|  | context and calls the next handler in the chain. | ||||||
|  | 
 | ||||||
|  | ```go | ||||||
|  | // HTTP middleware setting a value on the request context | ||||||
|  | func MyMiddleware(next http.Handler) http.Handler { | ||||||
|  |   return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { | ||||||
|  |     ctx := context.WithValue(r.Context(), "user", "123") | ||||||
|  |     next.ServeHTTP(w, r.WithContext(ctx)) | ||||||
|  |   }) | ||||||
|  | } | ||||||
|  | ``` | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | ### Request handlers | ||||||
|  | 
 | ||||||
|  | chi uses standard net/http request handlers. This little snippet is an example of a http.Handler | ||||||
|  | func that reads a user identifier from the request context - hypothetically, identifying | ||||||
|  | the user sending an authenticated request, validated+set by a previous middleware handler. | ||||||
|  | 
 | ||||||
|  | ```go | ||||||
|  | // HTTP handler accessing data from the request context. | ||||||
|  | func MyRequestHandler(w http.ResponseWriter, r *http.Request) { | ||||||
|  |   user := r.Context().Value("user").(string) | ||||||
|  |   w.Write([]byte(fmt.Sprintf("hi %s", user))) | ||||||
|  | } | ||||||
|  | ``` | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | ### URL parameters | ||||||
|  | 
 | ||||||
|  | chi's router parses and stores URL parameters right onto the request context. Here is | ||||||
|  | an example of how to access URL params in your net/http handlers. And of course, middlewares | ||||||
|  | are able to access the same information. | ||||||
|  | 
 | ||||||
|  | ```go | ||||||
|  | // HTTP handler accessing the url routing parameters. | ||||||
|  | func MyRequestHandler(w http.ResponseWriter, r *http.Request) { | ||||||
|  |   userID := chi.URLParam(r, "userID") // from a route like /users/{userID} | ||||||
|  | 
 | ||||||
|  |   ctx := r.Context() | ||||||
|  |   key := ctx.Value("key").(string) | ||||||
|  | 
 | ||||||
|  |   w.Write([]byte(fmt.Sprintf("hi %v, %v", userID, key))) | ||||||
|  | } | ||||||
|  | ``` | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | ## Middlewares | ||||||
|  | 
 | ||||||
|  | chi comes equipped with an optional `middleware` package, providing a suite of standard | ||||||
|  | `net/http` middlewares. Please note, any middleware in the ecosystem that is also compatible | ||||||
|  | with `net/http` can be used with chi's mux. | ||||||
|  | 
 | ||||||
|  | ### Core middlewares | ||||||
|  | 
 | ||||||
|  | ----------------------------------------------------------------------------------------------------------- | ||||||
|  | | chi/middleware Handler | description                                                                     | | ||||||
|  | |:----------------------|:--------------------------------------------------------------------------------- | ||||||
|  | | AllowContentType      | Explicit whitelist of accepted request Content-Types                            | | ||||||
|  | | Compress              | Gzip compression for clients that accept compressed responses                   | | ||||||
|  | | GetHead               | Automatically route undefined HEAD requests to GET handlers                     | | ||||||
|  | | Heartbeat             | Monitoring endpoint to check the servers pulse                                  | | ||||||
|  | | Logger                | Logs the start and end of each request with the elapsed processing time         | | ||||||
|  | | NoCache               | Sets response headers to prevent clients from caching                           | | ||||||
|  | | Profiler              | Easily attach net/http/pprof to your routers                                    | | ||||||
|  | | RealIP                | Sets a http.Request's RemoteAddr to either X-Forwarded-For or X-Real-IP         | | ||||||
|  | | Recoverer             | Gracefully absorb panics and prints the stack trace                             | | ||||||
|  | | RequestID             | Injects a request ID into the context of each request                           | | ||||||
|  | | RedirectSlashes       | Redirect slashes on routing paths                                               | | ||||||
|  | | SetHeader             | Short-hand middleware to set a response header key/value                        | | ||||||
|  | | StripSlashes          | Strip slashes on routing paths                                                  | | ||||||
|  | | Throttle              | Puts a ceiling on the number of concurrent requests                             | | ||||||
|  | | Timeout               | Signals to the request context when the timeout deadline is reached             | | ||||||
|  | | URLFormat             | Parse extension from url and put it on request context                          | | ||||||
|  | | WithValue             | Short-hand middleware to set a key/value on the request context                 | | ||||||
|  | ----------------------------------------------------------------------------------------------------------- | ||||||
|  | 
 | ||||||
|  | ### Auxiliary middlewares & packages | ||||||
|  | 
 | ||||||
|  | Please see https://github.com/go-chi for additional packages. | ||||||
|  | 
 | ||||||
|  | -------------------------------------------------------------------------------------------------------------------- | ||||||
|  | | package                                            | description                                                 | | ||||||
|  | |:---------------------------------------------------|:------------------------------------------------------------- | ||||||
|  | | [cors](https://github.com/go-chi/cors)             | Cross-origin resource sharing (CORS)                        | | ||||||
|  | | [docgen](https://github.com/go-chi/docgen)         | Print chi.Router routes at runtime                          | | ||||||
|  | | [jwtauth](https://github.com/go-chi/jwtauth)       | JWT authentication                                          | | ||||||
|  | | [hostrouter](https://github.com/go-chi/hostrouter) | Domain/host based request routing                           | | ||||||
|  | | [httpcoala](https://github.com/go-chi/httpcoala)   | HTTP request coalescer                                      | | ||||||
|  | | [chi-authz](https://github.com/casbin/chi-authz)   | Request ACL via https://github.com/hsluoyz/casbin           | | ||||||
|  | | [phi](https://github.com/fate-lovely/phi)          | Port chi to [fasthttp](https://github.com/valyala/fasthttp) | | ||||||
|  | -------------------------------------------------------------------------------------------------------------------- | ||||||
|  | 
 | ||||||
|  | please [submit a PR](./CONTRIBUTING.md) if you'd like to include a link to a chi-compatible middleware | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | ## context? | ||||||
|  | 
 | ||||||
|  | `context` is a tiny pkg that provides simple interface to signal context across call stacks | ||||||
|  | and goroutines. It was originally written by [Sameer Ajmani](https://github.com/Sajmani) | ||||||
|  | and is available in stdlib since go1.7. | ||||||
|  | 
 | ||||||
|  | Learn more at https://blog.golang.org/context | ||||||
|  | 
 | ||||||
|  | and.. | ||||||
|  | * Docs: https://golang.org/pkg/context | ||||||
|  | * Source: https://github.com/golang/go/tree/master/src/context | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | ## Benchmarks | ||||||
|  | 
 | ||||||
|  | The benchmark suite: https://github.com/pkieltyka/go-http-routing-benchmark | ||||||
|  | 
 | ||||||
|  | Results as of Jan 9, 2019 with Go 1.11.4 on Linux X1 Carbon laptop | ||||||
|  | 
 | ||||||
|  | ```shell | ||||||
|  | BenchmarkChi_Param            3000000         475 ns/op       432 B/op      3 allocs/op | ||||||
|  | BenchmarkChi_Param5           2000000         696 ns/op       432 B/op      3 allocs/op | ||||||
|  | BenchmarkChi_Param20          1000000        1275 ns/op       432 B/op      3 allocs/op | ||||||
|  | BenchmarkChi_ParamWrite       3000000         505 ns/op       432 B/op      3 allocs/op | ||||||
|  | BenchmarkChi_GithubStatic     3000000         508 ns/op       432 B/op      3 allocs/op | ||||||
|  | BenchmarkChi_GithubParam      2000000         669 ns/op       432 B/op      3 allocs/op | ||||||
|  | BenchmarkChi_GithubAll          10000      134627 ns/op     87699 B/op    609 allocs/op | ||||||
|  | BenchmarkChi_GPlusStatic      3000000         402 ns/op       432 B/op      3 allocs/op | ||||||
|  | BenchmarkChi_GPlusParam       3000000         500 ns/op       432 B/op      3 allocs/op | ||||||
|  | BenchmarkChi_GPlus2Params     3000000         586 ns/op       432 B/op      3 allocs/op | ||||||
|  | BenchmarkChi_GPlusAll          200000        7237 ns/op      5616 B/op     39 allocs/op | ||||||
|  | BenchmarkChi_ParseStatic      3000000         408 ns/op       432 B/op      3 allocs/op | ||||||
|  | BenchmarkChi_ParseParam       3000000         488 ns/op       432 B/op      3 allocs/op | ||||||
|  | BenchmarkChi_Parse2Params     3000000         551 ns/op       432 B/op      3 allocs/op | ||||||
|  | BenchmarkChi_ParseAll          100000       13508 ns/op     11232 B/op     78 allocs/op | ||||||
|  | BenchmarkChi_StaticAll          20000       81933 ns/op     67826 B/op    471 allocs/op | ||||||
|  | ``` | ||||||
|  | 
 | ||||||
|  | Comparison with other routers: https://gist.github.com/pkieltyka/123032f12052520aaccab752bd3e78cc | ||||||
|  | 
 | ||||||
|  | NOTE: the allocs in the benchmark above are from the calls to http.Request's | ||||||
|  | `WithContext(context.Context)` method that clones the http.Request, sets the `Context()` | ||||||
|  | on the duplicated (alloc'd) request and returns it the new request object. This is just | ||||||
|  | how setting context on a request in Go works. | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | ## Credits | ||||||
|  | 
 | ||||||
|  | * Carl Jackson for https://github.com/zenazn/goji | ||||||
|  |   * Parts of chi's thinking comes from goji, and chi's middleware package | ||||||
|  |     sources from goji. | ||||||
|  | * Armon Dadgar for https://github.com/armon/go-radix | ||||||
|  | * Contributions: [@VojtechVitek](https://github.com/VojtechVitek) | ||||||
|  | 
 | ||||||
|  | We'll be more than happy to see [your contributions](./CONTRIBUTING.md)! | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | ## Beyond REST | ||||||
|  | 
 | ||||||
|  | chi is just a http router that lets you decompose request handling into many smaller layers. | ||||||
|  | Many companies including Pressly.com (of course) use chi to write REST services for their public | ||||||
|  | APIs. But, REST is just a convention for managing state via HTTP, and there's a lot of other pieces | ||||||
|  | required to write a complete client-server system or network of microservices. | ||||||
|  | 
 | ||||||
|  | Looking ahead beyond REST, I also recommend some newer works in the field coming from | ||||||
|  | [gRPC](https://github.com/grpc/grpc-go), [NATS](https://nats.io), [go-kit](https://github.com/go-kit/kit) | ||||||
|  | and even [graphql](https://github.com/graphql-go/graphql). They're all pretty cool with their | ||||||
|  | own unique approaches and benefits. Specifically, I'd look at gRPC since it makes client-server | ||||||
|  | communication feel like a single program on a single computer, no need to hand-write a client library | ||||||
|  | and the request/response payloads are typed contracts. NATS is pretty amazing too as a super | ||||||
|  | fast and lightweight pub-sub transport that can speak protobufs, with nice service discovery - | ||||||
|  | an excellent combination with gRPC. | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | ## License | ||||||
|  | 
 | ||||||
|  | Copyright (c) 2015-present [Peter Kieltyka](https://github.com/pkieltyka) | ||||||
|  | 
 | ||||||
|  | Licensed under [MIT License](./LICENSE) | ||||||
|  | 
 | ||||||
|  | [GoDoc]: https://godoc.org/github.com/go-chi/chi | ||||||
|  | [GoDoc Widget]: https://godoc.org/github.com/go-chi/chi?status.svg | ||||||
|  | [Travis]: https://travis-ci.org/go-chi/chi | ||||||
|  | [Travis Widget]: https://travis-ci.org/go-chi/chi.svg?branch=master | ||||||
							
								
								
									
										49
									
								
								vendor/github.com/go-chi/chi/chain.go
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										49
									
								
								vendor/github.com/go-chi/chi/chain.go
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							| @ -0,0 +1,49 @@ | |||||||
|  | package chi | ||||||
|  | 
 | ||||||
|  | import "net/http" | ||||||
|  | 
 | ||||||
|  | // Chain returns a Middlewares type from a slice of middleware handlers. | ||||||
|  | func Chain(middlewares ...func(http.Handler) http.Handler) Middlewares { | ||||||
|  | 	return Middlewares(middlewares) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // Handler builds and returns a http.Handler from the chain of middlewares, | ||||||
|  | // with `h http.Handler` as the final handler. | ||||||
|  | func (mws Middlewares) Handler(h http.Handler) http.Handler { | ||||||
|  | 	return &ChainHandler{mws, h, chain(mws, h)} | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // HandlerFunc builds and returns a http.Handler from the chain of middlewares, | ||||||
|  | // with `h http.Handler` as the final handler. | ||||||
|  | func (mws Middlewares) HandlerFunc(h http.HandlerFunc) http.Handler { | ||||||
|  | 	return &ChainHandler{mws, h, chain(mws, h)} | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // ChainHandler is a http.Handler with support for handler composition and | ||||||
|  | // execution. | ||||||
|  | type ChainHandler struct { | ||||||
|  | 	Middlewares Middlewares | ||||||
|  | 	Endpoint    http.Handler | ||||||
|  | 	chain       http.Handler | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (c *ChainHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { | ||||||
|  | 	c.chain.ServeHTTP(w, r) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // chain builds a http.Handler composed of an inline middleware stack and endpoint | ||||||
|  | // handler in the order they are passed. | ||||||
|  | func chain(middlewares []func(http.Handler) http.Handler, endpoint http.Handler) http.Handler { | ||||||
|  | 	// Return ahead of time if there aren't any middlewares for the chain | ||||||
|  | 	if len(middlewares) == 0 { | ||||||
|  | 		return endpoint | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	// Wrap the end handler with the middleware chain | ||||||
|  | 	h := middlewares[len(middlewares)-1](endpoint) | ||||||
|  | 	for i := len(middlewares) - 2; i >= 0; i-- { | ||||||
|  | 		h = middlewares[i](h) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	return h | ||||||
|  | } | ||||||
							
								
								
									
										134
									
								
								vendor/github.com/go-chi/chi/chi.go
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										134
									
								
								vendor/github.com/go-chi/chi/chi.go
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							| @ -0,0 +1,134 @@ | |||||||
|  | // | ||||||
|  | // Package chi is a small, idiomatic and composable router for building HTTP services. | ||||||
|  | // | ||||||
|  | // chi requires Go 1.7 or newer. | ||||||
|  | // | ||||||
|  | // Example: | ||||||
|  | //  package main | ||||||
|  | // | ||||||
|  | //  import ( | ||||||
|  | //  	"net/http" | ||||||
|  | // | ||||||
|  | //  	"github.com/go-chi/chi" | ||||||
|  | //  	"github.com/go-chi/chi/middleware" | ||||||
|  | //  ) | ||||||
|  | // | ||||||
|  | //  func main() { | ||||||
|  | //  	r := chi.NewRouter() | ||||||
|  | //  	r.Use(middleware.Logger) | ||||||
|  | //  	r.Use(middleware.Recoverer) | ||||||
|  | // | ||||||
|  | //  	r.Get("/", func(w http.ResponseWriter, r *http.Request) { | ||||||
|  | //  		w.Write([]byte("root.")) | ||||||
|  | //  	}) | ||||||
|  | // | ||||||
|  | //  	http.ListenAndServe(":3333", r) | ||||||
|  | //  } | ||||||
|  | // | ||||||
|  | // See github.com/go-chi/chi/_examples/ for more in-depth examples. | ||||||
|  | // | ||||||
|  | // URL patterns allow for easy matching of path components in HTTP | ||||||
|  | // requests. The matching components can then be accessed using | ||||||
|  | // chi.URLParam(). All patterns must begin with a slash. | ||||||
|  | // | ||||||
|  | // A simple named placeholder {name} matches any sequence of characters | ||||||
|  | // up to the next / or the end of the URL. Trailing slashes on paths must | ||||||
|  | // be handled explicitly. | ||||||
|  | // | ||||||
|  | // A placeholder with a name followed by a colon allows a regular | ||||||
|  | // expression match, for example {number:\\d+}. The regular expression | ||||||
|  | // syntax is Go's normal regexp RE2 syntax, except that regular expressions | ||||||
|  | // including { or } are not supported, and / will never be | ||||||
|  | // matched. An anonymous regexp pattern is allowed, using an empty string | ||||||
|  | // before the colon in the placeholder, such as {:\\d+} | ||||||
|  | // | ||||||
|  | // The special placeholder of asterisk matches the rest of the requested | ||||||
|  | // URL. Any trailing characters in the pattern are ignored. This is the only | ||||||
|  | // placeholder which will match / characters. | ||||||
|  | // | ||||||
|  | // Examples: | ||||||
|  | //  "/user/{name}" matches "/user/jsmith" but not "/user/jsmith/info" or "/user/jsmith/" | ||||||
|  | //  "/user/{name}/info" matches "/user/jsmith/info" | ||||||
|  | //  "/page/*" matches "/page/intro/latest" | ||||||
|  | //  "/page/*/index" also matches "/page/intro/latest" | ||||||
|  | //  "/date/{yyyy:\\d\\d\\d\\d}/{mm:\\d\\d}/{dd:\\d\\d}" matches "/date/2017/04/01" | ||||||
|  | // | ||||||
|  | package chi | ||||||
|  | 
 | ||||||
|  | import "net/http" | ||||||
|  | 
 | ||||||
|  | // NewRouter returns a new Mux object that implements the Router interface. | ||||||
|  | func NewRouter() *Mux { | ||||||
|  | 	return NewMux() | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // Router consisting of the core routing methods used by chi's Mux, | ||||||
|  | // using only the standard net/http. | ||||||
|  | type Router interface { | ||||||
|  | 	http.Handler | ||||||
|  | 	Routes | ||||||
|  | 
 | ||||||
|  | 	// Use appends one of more middlewares onto the Router stack. | ||||||
|  | 	Use(middlewares ...func(http.Handler) http.Handler) | ||||||
|  | 
 | ||||||
|  | 	// With adds inline middlewares for an endpoint handler. | ||||||
|  | 	With(middlewares ...func(http.Handler) http.Handler) Router | ||||||
|  | 
 | ||||||
|  | 	// Group adds a new inline-Router along the current routing | ||||||
|  | 	// path, with a fresh middleware stack for the inline-Router. | ||||||
|  | 	Group(fn func(r Router)) Router | ||||||
|  | 
 | ||||||
|  | 	// Route mounts a sub-Router along a `pattern`` string. | ||||||
|  | 	Route(pattern string, fn func(r Router)) Router | ||||||
|  | 
 | ||||||
|  | 	// Mount attaches another http.Handler along ./pattern/* | ||||||
|  | 	Mount(pattern string, h http.Handler) | ||||||
|  | 
 | ||||||
|  | 	// Handle and HandleFunc adds routes for `pattern` that matches | ||||||
|  | 	// all HTTP methods. | ||||||
|  | 	Handle(pattern string, h http.Handler) | ||||||
|  | 	HandleFunc(pattern string, h http.HandlerFunc) | ||||||
|  | 
 | ||||||
|  | 	// Method and MethodFunc adds routes for `pattern` that matches | ||||||
|  | 	// the `method` HTTP method. | ||||||
|  | 	Method(method, pattern string, h http.Handler) | ||||||
|  | 	MethodFunc(method, pattern string, h http.HandlerFunc) | ||||||
|  | 
 | ||||||
|  | 	// HTTP-method routing along `pattern` | ||||||
|  | 	Connect(pattern string, h http.HandlerFunc) | ||||||
|  | 	Delete(pattern string, h http.HandlerFunc) | ||||||
|  | 	Get(pattern string, h http.HandlerFunc) | ||||||
|  | 	Head(pattern string, h http.HandlerFunc) | ||||||
|  | 	Options(pattern string, h http.HandlerFunc) | ||||||
|  | 	Patch(pattern string, h http.HandlerFunc) | ||||||
|  | 	Post(pattern string, h http.HandlerFunc) | ||||||
|  | 	Put(pattern string, h http.HandlerFunc) | ||||||
|  | 	Trace(pattern string, h http.HandlerFunc) | ||||||
|  | 
 | ||||||
|  | 	// NotFound defines a handler to respond whenever a route could | ||||||
|  | 	// not be found. | ||||||
|  | 	NotFound(h http.HandlerFunc) | ||||||
|  | 
 | ||||||
|  | 	// MethodNotAllowed defines a handler to respond whenever a method is | ||||||
|  | 	// not allowed. | ||||||
|  | 	MethodNotAllowed(h http.HandlerFunc) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // Routes interface adds two methods for router traversal, which is also | ||||||
|  | // used by the `docgen` subpackage to generation documentation for Routers. | ||||||
|  | type Routes interface { | ||||||
|  | 	// Routes returns the routing tree in an easily traversable structure. | ||||||
|  | 	Routes() []Route | ||||||
|  | 
 | ||||||
|  | 	// Middlewares returns the list of middlewares in use by the router. | ||||||
|  | 	Middlewares() Middlewares | ||||||
|  | 
 | ||||||
|  | 	// Match searches the routing tree for a handler that matches | ||||||
|  | 	// the method/path - similar to routing a http request, but without | ||||||
|  | 	// executing the handler thereafter. | ||||||
|  | 	Match(rctx *Context, method, path string) bool | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // Middlewares type is a slice of standard middleware handlers with methods | ||||||
|  | // to compose middleware chains and http.Handler's. | ||||||
|  | type Middlewares []func(http.Handler) http.Handler | ||||||
							
								
								
									
										161
									
								
								vendor/github.com/go-chi/chi/context.go
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										161
									
								
								vendor/github.com/go-chi/chi/context.go
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							| @ -0,0 +1,161 @@ | |||||||
|  | package chi | ||||||
|  | 
 | ||||||
|  | import ( | ||||||
|  | 	"context" | ||||||
|  | 	"net" | ||||||
|  | 	"net/http" | ||||||
|  | 	"strings" | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | var ( | ||||||
|  | 	// RouteCtxKey is the context.Context key to store the request context. | ||||||
|  | 	RouteCtxKey = &contextKey{"RouteContext"} | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | // Context is the default routing context set on the root node of a | ||||||
|  | // request context to track route patterns, URL parameters and | ||||||
|  | // an optional routing path. | ||||||
|  | type Context struct { | ||||||
|  | 	Routes Routes | ||||||
|  | 
 | ||||||
|  | 	// Routing path/method override used during the route search. | ||||||
|  | 	// See Mux#routeHTTP method. | ||||||
|  | 	RoutePath   string | ||||||
|  | 	RouteMethod string | ||||||
|  | 
 | ||||||
|  | 	// Routing pattern stack throughout the lifecycle of the request, | ||||||
|  | 	// across all connected routers. It is a record of all matching | ||||||
|  | 	// patterns across a stack of sub-routers. | ||||||
|  | 	RoutePatterns []string | ||||||
|  | 
 | ||||||
|  | 	// URLParams are the stack of routeParams captured during the | ||||||
|  | 	// routing lifecycle across a stack of sub-routers. | ||||||
|  | 	URLParams RouteParams | ||||||
|  | 
 | ||||||
|  | 	// The endpoint routing pattern that matched the request URI path | ||||||
|  | 	// or `RoutePath` of the current sub-router. This value will update | ||||||
|  | 	// during the lifecycle of a request passing through a stack of | ||||||
|  | 	// sub-routers. | ||||||
|  | 	routePattern string | ||||||
|  | 
 | ||||||
|  | 	// Route parameters matched for the current sub-router. It is | ||||||
|  | 	// intentionally unexported so it cant be tampered. | ||||||
|  | 	routeParams RouteParams | ||||||
|  | 
 | ||||||
|  | 	// methodNotAllowed hint | ||||||
|  | 	methodNotAllowed bool | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // NewRouteContext returns a new routing Context object. | ||||||
|  | func NewRouteContext() *Context { | ||||||
|  | 	return &Context{} | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // Reset a routing context to its initial state. | ||||||
|  | func (x *Context) Reset() { | ||||||
|  | 	x.Routes = nil | ||||||
|  | 	x.RoutePath = "" | ||||||
|  | 	x.RouteMethod = "" | ||||||
|  | 	x.RoutePatterns = x.RoutePatterns[:0] | ||||||
|  | 	x.URLParams.Keys = x.URLParams.Keys[:0] | ||||||
|  | 	x.URLParams.Values = x.URLParams.Values[:0] | ||||||
|  | 
 | ||||||
|  | 	x.routePattern = "" | ||||||
|  | 	x.routeParams.Keys = x.routeParams.Keys[:0] | ||||||
|  | 	x.routeParams.Values = x.routeParams.Values[:0] | ||||||
|  | 	x.methodNotAllowed = false | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // URLParam returns the corresponding URL parameter value from the request | ||||||
|  | // routing context. | ||||||
|  | func (x *Context) URLParam(key string) string { | ||||||
|  | 	for k := len(x.URLParams.Keys) - 1; k >= 0; k-- { | ||||||
|  | 		if x.URLParams.Keys[k] == key { | ||||||
|  | 			return x.URLParams.Values[k] | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 	return "" | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // RoutePattern builds the routing pattern string for the particular | ||||||
|  | // request, at the particular point during routing. This means, the value | ||||||
|  | // will change throughout the execution of a request in a router. That is | ||||||
|  | // why its advised to only use this value after calling the next handler. | ||||||
|  | // | ||||||
|  | // For example, | ||||||
|  | // | ||||||
|  | //   func Instrument(next http.Handler) http.Handler { | ||||||
|  | //     return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { | ||||||
|  | //       next.ServeHTTP(w, r) | ||||||
|  | //       routePattern := chi.RouteContext(r.Context()).RoutePattern() | ||||||
|  | //       measure(w, r, routePattern) | ||||||
|  | //   	 }) | ||||||
|  | //   } | ||||||
|  | func (x *Context) RoutePattern() string { | ||||||
|  | 	routePattern := strings.Join(x.RoutePatterns, "") | ||||||
|  | 	return strings.Replace(routePattern, "/*/", "/", -1) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // RouteContext returns chi's routing Context object from a | ||||||
|  | // http.Request Context. | ||||||
|  | func RouteContext(ctx context.Context) *Context { | ||||||
|  | 	return ctx.Value(RouteCtxKey).(*Context) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // URLParam returns the url parameter from a http.Request object. | ||||||
|  | func URLParam(r *http.Request, key string) string { | ||||||
|  | 	if rctx := RouteContext(r.Context()); rctx != nil { | ||||||
|  | 		return rctx.URLParam(key) | ||||||
|  | 	} | ||||||
|  | 	return "" | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // URLParamFromCtx returns the url parameter from a http.Request Context. | ||||||
|  | func URLParamFromCtx(ctx context.Context, key string) string { | ||||||
|  | 	if rctx := RouteContext(ctx); rctx != nil { | ||||||
|  | 		return rctx.URLParam(key) | ||||||
|  | 	} | ||||||
|  | 	return "" | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // RouteParams is a structure to track URL routing parameters efficiently. | ||||||
|  | type RouteParams struct { | ||||||
|  | 	Keys, Values []string | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // Add will append a URL parameter to the end of the route param | ||||||
|  | func (s *RouteParams) Add(key, value string) { | ||||||
|  | 	(*s).Keys = append((*s).Keys, key) | ||||||
|  | 	(*s).Values = append((*s).Values, value) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // ServerBaseContext wraps an http.Handler to set the request context to the | ||||||
|  | // `baseCtx`. | ||||||
|  | func ServerBaseContext(baseCtx context.Context, h http.Handler) http.Handler { | ||||||
|  | 	fn := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { | ||||||
|  | 		ctx := r.Context() | ||||||
|  | 		baseCtx := baseCtx | ||||||
|  | 
 | ||||||
|  | 		// Copy over default net/http server context keys | ||||||
|  | 		if v, ok := ctx.Value(http.ServerContextKey).(*http.Server); ok { | ||||||
|  | 			baseCtx = context.WithValue(baseCtx, http.ServerContextKey, v) | ||||||
|  | 		} | ||||||
|  | 		if v, ok := ctx.Value(http.LocalAddrContextKey).(net.Addr); ok { | ||||||
|  | 			baseCtx = context.WithValue(baseCtx, http.LocalAddrContextKey, v) | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		h.ServeHTTP(w, r.WithContext(baseCtx)) | ||||||
|  | 	}) | ||||||
|  | 	return fn | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // contextKey is a value for use with context.WithValue. It's used as | ||||||
|  | // a pointer so it fits in an interface{} without allocation. This technique | ||||||
|  | // for defining context keys was copied from Go 1.7's new use of context in net/http. | ||||||
|  | type contextKey struct { | ||||||
|  | 	name string | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (k *contextKey) String() string { | ||||||
|  | 	return "chi context value " + k.name | ||||||
|  | } | ||||||
							
								
								
									
										460
									
								
								vendor/github.com/go-chi/chi/mux.go
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										460
									
								
								vendor/github.com/go-chi/chi/mux.go
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							| @ -0,0 +1,460 @@ | |||||||
|  | package chi | ||||||
|  | 
 | ||||||
|  | import ( | ||||||
|  | 	"context" | ||||||
|  | 	"fmt" | ||||||
|  | 	"net/http" | ||||||
|  | 	"strings" | ||||||
|  | 	"sync" | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | var _ Router = &Mux{} | ||||||
|  | 
 | ||||||
|  | // Mux is a simple HTTP route multiplexer that parses a request path, | ||||||
|  | // records any URL params, and executes an end handler. It implements | ||||||
|  | // the http.Handler interface and is friendly with the standard library. | ||||||
|  | // | ||||||
|  | // Mux is designed to be fast, minimal and offer a powerful API for building | ||||||
|  | // modular and composable HTTP services with a large set of handlers. It's | ||||||
|  | // particularly useful for writing large REST API services that break a handler | ||||||
|  | // into many smaller parts composed of middlewares and end handlers. | ||||||
|  | type Mux struct { | ||||||
|  | 	// The radix trie router | ||||||
|  | 	tree *node | ||||||
|  | 
 | ||||||
|  | 	// The middleware stack | ||||||
|  | 	middlewares []func(http.Handler) http.Handler | ||||||
|  | 
 | ||||||
|  | 	// Controls the behaviour of middleware chain generation when a mux | ||||||
|  | 	// is registered as an inline group inside another mux. | ||||||
|  | 	inline bool | ||||||
|  | 	parent *Mux | ||||||
|  | 
 | ||||||
|  | 	// The computed mux handler made of the chained middleware stack and | ||||||
|  | 	// the tree router | ||||||
|  | 	handler http.Handler | ||||||
|  | 
 | ||||||
|  | 	// Routing context pool | ||||||
|  | 	pool *sync.Pool | ||||||
|  | 
 | ||||||
|  | 	// Custom route not found handler | ||||||
|  | 	notFoundHandler http.HandlerFunc | ||||||
|  | 
 | ||||||
|  | 	// Custom method not allowed handler | ||||||
|  | 	methodNotAllowedHandler http.HandlerFunc | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // NewMux returns a newly initialized Mux object that implements the Router | ||||||
|  | // interface. | ||||||
|  | func NewMux() *Mux { | ||||||
|  | 	mux := &Mux{tree: &node{}, pool: &sync.Pool{}} | ||||||
|  | 	mux.pool.New = func() interface{} { | ||||||
|  | 		return NewRouteContext() | ||||||
|  | 	} | ||||||
|  | 	return mux | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // ServeHTTP is the single method of the http.Handler interface that makes | ||||||
|  | // Mux interoperable with the standard library. It uses a sync.Pool to get and | ||||||
|  | // reuse routing contexts for each request. | ||||||
|  | func (mx *Mux) ServeHTTP(w http.ResponseWriter, r *http.Request) { | ||||||
|  | 	// Ensure the mux has some routes defined on the mux | ||||||
|  | 	if mx.handler == nil { | ||||||
|  | 		mx.NotFoundHandler().ServeHTTP(w, r) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	// Check if a routing context already exists from a parent router. | ||||||
|  | 	rctx, _ := r.Context().Value(RouteCtxKey).(*Context) | ||||||
|  | 	if rctx != nil { | ||||||
|  | 		mx.handler.ServeHTTP(w, r) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	// Fetch a RouteContext object from the sync pool, and call the computed | ||||||
|  | 	// mx.handler that is comprised of mx.middlewares + mx.routeHTTP. | ||||||
|  | 	// Once the request is finished, reset the routing context and put it back | ||||||
|  | 	// into the pool for reuse from another request. | ||||||
|  | 	rctx = mx.pool.Get().(*Context) | ||||||
|  | 	rctx.Reset() | ||||||
|  | 	rctx.Routes = mx | ||||||
|  | 	r = r.WithContext(context.WithValue(r.Context(), RouteCtxKey, rctx)) | ||||||
|  | 	mx.handler.ServeHTTP(w, r) | ||||||
|  | 	mx.pool.Put(rctx) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // Use appends a middleware handler to the Mux middleware stack. | ||||||
|  | // | ||||||
|  | // The middleware stack for any Mux will execute before searching for a matching | ||||||
|  | // route to a specific handler, which provides opportunity to respond early, | ||||||
|  | // change the course of the request execution, or set request-scoped values for | ||||||
|  | // the next http.Handler. | ||||||
|  | func (mx *Mux) Use(middlewares ...func(http.Handler) http.Handler) { | ||||||
|  | 	if mx.handler != nil { | ||||||
|  | 		panic("chi: all middlewares must be defined before routes on a mux") | ||||||
|  | 	} | ||||||
|  | 	mx.middlewares = append(mx.middlewares, middlewares...) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // Handle adds the route `pattern` that matches any http method to | ||||||
|  | // execute the `handler` http.Handler. | ||||||
|  | func (mx *Mux) Handle(pattern string, handler http.Handler) { | ||||||
|  | 	mx.handle(mALL, pattern, handler) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // HandleFunc adds the route `pattern` that matches any http method to | ||||||
|  | // execute the `handlerFn` http.HandlerFunc. | ||||||
|  | func (mx *Mux) HandleFunc(pattern string, handlerFn http.HandlerFunc) { | ||||||
|  | 	mx.handle(mALL, pattern, handlerFn) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // Method adds the route `pattern` that matches `method` http method to | ||||||
|  | // execute the `handler` http.Handler. | ||||||
|  | func (mx *Mux) Method(method, pattern string, handler http.Handler) { | ||||||
|  | 	m, ok := methodMap[strings.ToUpper(method)] | ||||||
|  | 	if !ok { | ||||||
|  | 		panic(fmt.Sprintf("chi: '%s' http method is not supported.", method)) | ||||||
|  | 	} | ||||||
|  | 	mx.handle(m, pattern, handler) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // MethodFunc adds the route `pattern` that matches `method` http method to | ||||||
|  | // execute the `handlerFn` http.HandlerFunc. | ||||||
|  | func (mx *Mux) MethodFunc(method, pattern string, handlerFn http.HandlerFunc) { | ||||||
|  | 	mx.Method(method, pattern, handlerFn) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // Connect adds the route `pattern` that matches a CONNECT http method to | ||||||
|  | // execute the `handlerFn` http.HandlerFunc. | ||||||
|  | func (mx *Mux) Connect(pattern string, handlerFn http.HandlerFunc) { | ||||||
|  | 	mx.handle(mCONNECT, pattern, handlerFn) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // Delete adds the route `pattern` that matches a DELETE http method to | ||||||
|  | // execute the `handlerFn` http.HandlerFunc. | ||||||
|  | func (mx *Mux) Delete(pattern string, handlerFn http.HandlerFunc) { | ||||||
|  | 	mx.handle(mDELETE, pattern, handlerFn) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // Get adds the route `pattern` that matches a GET http method to | ||||||
|  | // execute the `handlerFn` http.HandlerFunc. | ||||||
|  | func (mx *Mux) Get(pattern string, handlerFn http.HandlerFunc) { | ||||||
|  | 	mx.handle(mGET, pattern, handlerFn) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // Head adds the route `pattern` that matches a HEAD http method to | ||||||
|  | // execute the `handlerFn` http.HandlerFunc. | ||||||
|  | func (mx *Mux) Head(pattern string, handlerFn http.HandlerFunc) { | ||||||
|  | 	mx.handle(mHEAD, pattern, handlerFn) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // Options adds the route `pattern` that matches a OPTIONS http method to | ||||||
|  | // execute the `handlerFn` http.HandlerFunc. | ||||||
|  | func (mx *Mux) Options(pattern string, handlerFn http.HandlerFunc) { | ||||||
|  | 	mx.handle(mOPTIONS, pattern, handlerFn) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // Patch adds the route `pattern` that matches a PATCH http method to | ||||||
|  | // execute the `handlerFn` http.HandlerFunc. | ||||||
|  | func (mx *Mux) Patch(pattern string, handlerFn http.HandlerFunc) { | ||||||
|  | 	mx.handle(mPATCH, pattern, handlerFn) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // Post adds the route `pattern` that matches a POST http method to | ||||||
|  | // execute the `handlerFn` http.HandlerFunc. | ||||||
|  | func (mx *Mux) Post(pattern string, handlerFn http.HandlerFunc) { | ||||||
|  | 	mx.handle(mPOST, pattern, handlerFn) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // Put adds the route `pattern` that matches a PUT http method to | ||||||
|  | // execute the `handlerFn` http.HandlerFunc. | ||||||
|  | func (mx *Mux) Put(pattern string, handlerFn http.HandlerFunc) { | ||||||
|  | 	mx.handle(mPUT, pattern, handlerFn) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // Trace adds the route `pattern` that matches a TRACE http method to | ||||||
|  | // execute the `handlerFn` http.HandlerFunc. | ||||||
|  | func (mx *Mux) Trace(pattern string, handlerFn http.HandlerFunc) { | ||||||
|  | 	mx.handle(mTRACE, pattern, handlerFn) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // NotFound sets a custom http.HandlerFunc for routing paths that could | ||||||
|  | // not be found. The default 404 handler is `http.NotFound`. | ||||||
|  | func (mx *Mux) NotFound(handlerFn http.HandlerFunc) { | ||||||
|  | 	// Build NotFound handler chain | ||||||
|  | 	m := mx | ||||||
|  | 	hFn := handlerFn | ||||||
|  | 	if mx.inline && mx.parent != nil { | ||||||
|  | 		m = mx.parent | ||||||
|  | 		hFn = Chain(mx.middlewares...).HandlerFunc(hFn).ServeHTTP | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	// Update the notFoundHandler from this point forward | ||||||
|  | 	m.notFoundHandler = hFn | ||||||
|  | 	m.updateSubRoutes(func(subMux *Mux) { | ||||||
|  | 		if subMux.notFoundHandler == nil { | ||||||
|  | 			subMux.NotFound(hFn) | ||||||
|  | 		} | ||||||
|  | 	}) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // MethodNotAllowed sets a custom http.HandlerFunc for routing paths where the | ||||||
|  | // method is unresolved. The default handler returns a 405 with an empty body. | ||||||
|  | func (mx *Mux) MethodNotAllowed(handlerFn http.HandlerFunc) { | ||||||
|  | 	// Build MethodNotAllowed handler chain | ||||||
|  | 	m := mx | ||||||
|  | 	hFn := handlerFn | ||||||
|  | 	if mx.inline && mx.parent != nil { | ||||||
|  | 		m = mx.parent | ||||||
|  | 		hFn = Chain(mx.middlewares...).HandlerFunc(hFn).ServeHTTP | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	// Update the methodNotAllowedHandler from this point forward | ||||||
|  | 	m.methodNotAllowedHandler = hFn | ||||||
|  | 	m.updateSubRoutes(func(subMux *Mux) { | ||||||
|  | 		if subMux.methodNotAllowedHandler == nil { | ||||||
|  | 			subMux.MethodNotAllowed(hFn) | ||||||
|  | 		} | ||||||
|  | 	}) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // With adds inline middlewares for an endpoint handler. | ||||||
|  | func (mx *Mux) With(middlewares ...func(http.Handler) http.Handler) Router { | ||||||
|  | 	// Similarly as in handle(), we must build the mux handler once further | ||||||
|  | 	// middleware registration isn't allowed for this stack, like now. | ||||||
|  | 	if !mx.inline && mx.handler == nil { | ||||||
|  | 		mx.buildRouteHandler() | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	// Copy middlewares from parent inline muxs | ||||||
|  | 	var mws Middlewares | ||||||
|  | 	if mx.inline { | ||||||
|  | 		mws = make(Middlewares, len(mx.middlewares)) | ||||||
|  | 		copy(mws, mx.middlewares) | ||||||
|  | 	} | ||||||
|  | 	mws = append(mws, middlewares...) | ||||||
|  | 
 | ||||||
|  | 	im := &Mux{pool: mx.pool, inline: true, parent: mx, tree: mx.tree, middlewares: mws} | ||||||
|  | 
 | ||||||
|  | 	return im | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // Group creates a new inline-Mux with a fresh middleware stack. It's useful | ||||||
|  | // for a group of handlers along the same routing path that use an additional | ||||||
|  | // set of middlewares. See _examples/. | ||||||
|  | func (mx *Mux) Group(fn func(r Router)) Router { | ||||||
|  | 	im := mx.With().(*Mux) | ||||||
|  | 	if fn != nil { | ||||||
|  | 		fn(im) | ||||||
|  | 	} | ||||||
|  | 	return im | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // Route creates a new Mux with a fresh middleware stack and mounts it | ||||||
|  | // along the `pattern` as a subrouter. Effectively, this is a short-hand | ||||||
|  | // call to Mount. See _examples/. | ||||||
|  | func (mx *Mux) Route(pattern string, fn func(r Router)) Router { | ||||||
|  | 	subRouter := NewRouter() | ||||||
|  | 	if fn != nil { | ||||||
|  | 		fn(subRouter) | ||||||
|  | 	} | ||||||
|  | 	mx.Mount(pattern, subRouter) | ||||||
|  | 	return subRouter | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // Mount attaches another http.Handler or chi Router as a subrouter along a routing | ||||||
|  | // path. It's very useful to split up a large API as many independent routers and | ||||||
|  | // compose them as a single service using Mount. See _examples/. | ||||||
|  | // | ||||||
|  | // Note that Mount() simply sets a wildcard along the `pattern` that will continue | ||||||
|  | // routing at the `handler`, which in most cases is another chi.Router. As a result, | ||||||
|  | // if you define two Mount() routes on the exact same pattern the mount will panic. | ||||||
|  | func (mx *Mux) Mount(pattern string, handler http.Handler) { | ||||||
|  | 	// Provide runtime safety for ensuring a pattern isn't mounted on an existing | ||||||
|  | 	// routing pattern. | ||||||
|  | 	if mx.tree.findPattern(pattern+"*") || mx.tree.findPattern(pattern+"/*") { | ||||||
|  | 		panic(fmt.Sprintf("chi: attempting to Mount() a handler on an existing path, '%s'", pattern)) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	// Assign sub-Router's with the parent not found & method not allowed handler if not specified. | ||||||
|  | 	subr, ok := handler.(*Mux) | ||||||
|  | 	if ok && subr.notFoundHandler == nil && mx.notFoundHandler != nil { | ||||||
|  | 		subr.NotFound(mx.notFoundHandler) | ||||||
|  | 	} | ||||||
|  | 	if ok && subr.methodNotAllowedHandler == nil && mx.methodNotAllowedHandler != nil { | ||||||
|  | 		subr.MethodNotAllowed(mx.methodNotAllowedHandler) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	// Wrap the sub-router in a handlerFunc to scope the request path for routing. | ||||||
|  | 	mountHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { | ||||||
|  | 		rctx := RouteContext(r.Context()) | ||||||
|  | 		rctx.RoutePath = mx.nextRoutePath(rctx) | ||||||
|  | 		handler.ServeHTTP(w, r) | ||||||
|  | 	}) | ||||||
|  | 
 | ||||||
|  | 	if pattern == "" || pattern[len(pattern)-1] != '/' { | ||||||
|  | 		mx.handle(mALL|mSTUB, pattern, mountHandler) | ||||||
|  | 		mx.handle(mALL|mSTUB, pattern+"/", mountHandler) | ||||||
|  | 		pattern += "/" | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	method := mALL | ||||||
|  | 	subroutes, _ := handler.(Routes) | ||||||
|  | 	if subroutes != nil { | ||||||
|  | 		method |= mSTUB | ||||||
|  | 	} | ||||||
|  | 	n := mx.handle(method, pattern+"*", mountHandler) | ||||||
|  | 
 | ||||||
|  | 	if subroutes != nil { | ||||||
|  | 		n.subroutes = subroutes | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // Routes returns a slice of routing information from the tree, | ||||||
|  | // useful for traversing available routes of a router. | ||||||
|  | func (mx *Mux) Routes() []Route { | ||||||
|  | 	return mx.tree.routes() | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // Middlewares returns a slice of middleware handler functions. | ||||||
|  | func (mx *Mux) Middlewares() Middlewares { | ||||||
|  | 	return mx.middlewares | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // Match searches the routing tree for a handler that matches the method/path. | ||||||
|  | // It's similar to routing a http request, but without executing the handler | ||||||
|  | // thereafter. | ||||||
|  | // | ||||||
|  | // Note: the *Context state is updated during execution, so manage | ||||||
|  | // the state carefully or make a NewRouteContext(). | ||||||
|  | func (mx *Mux) Match(rctx *Context, method, path string) bool { | ||||||
|  | 	m, ok := methodMap[method] | ||||||
|  | 	if !ok { | ||||||
|  | 		return false | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	node, _, h := mx.tree.FindRoute(rctx, m, path) | ||||||
|  | 
 | ||||||
|  | 	if node != nil && node.subroutes != nil { | ||||||
|  | 		rctx.RoutePath = mx.nextRoutePath(rctx) | ||||||
|  | 		return node.subroutes.Match(rctx, method, rctx.RoutePath) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	return h != nil | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // NotFoundHandler returns the default Mux 404 responder whenever a route | ||||||
|  | // cannot be found. | ||||||
|  | func (mx *Mux) NotFoundHandler() http.HandlerFunc { | ||||||
|  | 	if mx.notFoundHandler != nil { | ||||||
|  | 		return mx.notFoundHandler | ||||||
|  | 	} | ||||||
|  | 	return http.NotFound | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // MethodNotAllowedHandler returns the default Mux 405 responder whenever | ||||||
|  | // a method cannot be resolved for a route. | ||||||
|  | func (mx *Mux) MethodNotAllowedHandler() http.HandlerFunc { | ||||||
|  | 	if mx.methodNotAllowedHandler != nil { | ||||||
|  | 		return mx.methodNotAllowedHandler | ||||||
|  | 	} | ||||||
|  | 	return methodNotAllowedHandler | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // buildRouteHandler builds the single mux handler that is a chain of the middleware | ||||||
|  | // stack, as defined by calls to Use(), and the tree router (Mux) itself. After this | ||||||
|  | // point, no other middlewares can be registered on this Mux's stack. But you can still | ||||||
|  | // compose additional middlewares via Group()'s or using a chained middleware handler. | ||||||
|  | func (mx *Mux) buildRouteHandler() { | ||||||
|  | 	mx.handler = chain(mx.middlewares, http.HandlerFunc(mx.routeHTTP)) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // handle registers a http.Handler in the routing tree for a particular http method | ||||||
|  | // and routing pattern. | ||||||
|  | func (mx *Mux) handle(method methodTyp, pattern string, handler http.Handler) *node { | ||||||
|  | 	if len(pattern) == 0 || pattern[0] != '/' { | ||||||
|  | 		panic(fmt.Sprintf("chi: routing pattern must begin with '/' in '%s'", pattern)) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	// Build the final routing handler for this Mux. | ||||||
|  | 	if !mx.inline && mx.handler == nil { | ||||||
|  | 		mx.buildRouteHandler() | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	// Build endpoint handler with inline middlewares for the route | ||||||
|  | 	var h http.Handler | ||||||
|  | 	if mx.inline { | ||||||
|  | 		mx.handler = http.HandlerFunc(mx.routeHTTP) | ||||||
|  | 		h = Chain(mx.middlewares...).Handler(handler) | ||||||
|  | 	} else { | ||||||
|  | 		h = handler | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	// Add the endpoint to the tree and return the node | ||||||
|  | 	return mx.tree.InsertRoute(method, pattern, h) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // routeHTTP routes a http.Request through the Mux routing tree to serve | ||||||
|  | // the matching handler for a particular http method. | ||||||
|  | func (mx *Mux) routeHTTP(w http.ResponseWriter, r *http.Request) { | ||||||
|  | 	// Grab the route context object | ||||||
|  | 	rctx := r.Context().Value(RouteCtxKey).(*Context) | ||||||
|  | 
 | ||||||
|  | 	// The request routing path | ||||||
|  | 	routePath := rctx.RoutePath | ||||||
|  | 	if routePath == "" { | ||||||
|  | 		if r.URL.RawPath != "" { | ||||||
|  | 			routePath = r.URL.RawPath | ||||||
|  | 		} else { | ||||||
|  | 			routePath = r.URL.Path | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	// Check if method is supported by chi | ||||||
|  | 	if rctx.RouteMethod == "" { | ||||||
|  | 		rctx.RouteMethod = r.Method | ||||||
|  | 	} | ||||||
|  | 	method, ok := methodMap[rctx.RouteMethod] | ||||||
|  | 	if !ok { | ||||||
|  | 		mx.MethodNotAllowedHandler().ServeHTTP(w, r) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	// Find the route | ||||||
|  | 	if _, _, h := mx.tree.FindRoute(rctx, method, routePath); h != nil { | ||||||
|  | 		h.ServeHTTP(w, r) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 	if rctx.methodNotAllowed { | ||||||
|  | 		mx.MethodNotAllowedHandler().ServeHTTP(w, r) | ||||||
|  | 	} else { | ||||||
|  | 		mx.NotFoundHandler().ServeHTTP(w, r) | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (mx *Mux) nextRoutePath(rctx *Context) string { | ||||||
|  | 	routePath := "/" | ||||||
|  | 	nx := len(rctx.routeParams.Keys) - 1 // index of last param in list | ||||||
|  | 	if nx >= 0 && rctx.routeParams.Keys[nx] == "*" && len(rctx.routeParams.Values) > nx { | ||||||
|  | 		routePath += rctx.routeParams.Values[nx] | ||||||
|  | 	} | ||||||
|  | 	return routePath | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // Recursively update data on child routers. | ||||||
|  | func (mx *Mux) updateSubRoutes(fn func(subMux *Mux)) { | ||||||
|  | 	for _, r := range mx.tree.routes() { | ||||||
|  | 		subMux, ok := r.SubRoutes.(*Mux) | ||||||
|  | 		if !ok { | ||||||
|  | 			continue | ||||||
|  | 		} | ||||||
|  | 		fn(subMux) | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // methodNotAllowedHandler is a helper function to respond with a 405, | ||||||
|  | // method not allowed. | ||||||
|  | func methodNotAllowedHandler(w http.ResponseWriter, r *http.Request) { | ||||||
|  | 	w.WriteHeader(405) | ||||||
|  | 	w.Write(nil) | ||||||
|  | } | ||||||
							
								
								
									
										847
									
								
								vendor/github.com/go-chi/chi/tree.go
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										847
									
								
								vendor/github.com/go-chi/chi/tree.go
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							| @ -0,0 +1,847 @@ | |||||||
|  | package chi | ||||||
|  | 
 | ||||||
|  | // Radix tree implementation below is a based on the original work by | ||||||
|  | // Armon Dadgar in https://github.com/armon/go-radix/blob/master/radix.go | ||||||
|  | // (MIT licensed). It's been heavily modified for use as a HTTP routing tree. | ||||||
|  | 
 | ||||||
|  | import ( | ||||||
|  | 	"fmt" | ||||||
|  | 	"math" | ||||||
|  | 	"net/http" | ||||||
|  | 	"regexp" | ||||||
|  | 	"sort" | ||||||
|  | 	"strconv" | ||||||
|  | 	"strings" | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | type methodTyp int | ||||||
|  | 
 | ||||||
|  | const ( | ||||||
|  | 	mSTUB methodTyp = 1 << iota | ||||||
|  | 	mCONNECT | ||||||
|  | 	mDELETE | ||||||
|  | 	mGET | ||||||
|  | 	mHEAD | ||||||
|  | 	mOPTIONS | ||||||
|  | 	mPATCH | ||||||
|  | 	mPOST | ||||||
|  | 	mPUT | ||||||
|  | 	mTRACE | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | var mALL = mCONNECT | mDELETE | mGET | mHEAD | | ||||||
|  | 	mOPTIONS | mPATCH | mPOST | mPUT | mTRACE | ||||||
|  | 
 | ||||||
|  | var methodMap = map[string]methodTyp{ | ||||||
|  | 	http.MethodConnect: mCONNECT, | ||||||
|  | 	http.MethodDelete:  mDELETE, | ||||||
|  | 	http.MethodGet:     mGET, | ||||||
|  | 	http.MethodHead:    mHEAD, | ||||||
|  | 	http.MethodOptions: mOPTIONS, | ||||||
|  | 	http.MethodPatch:   mPATCH, | ||||||
|  | 	http.MethodPost:    mPOST, | ||||||
|  | 	http.MethodPut:     mPUT, | ||||||
|  | 	http.MethodTrace:   mTRACE, | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // RegisterMethod adds support for custom HTTP method handlers, available | ||||||
|  | // via Router#Method and Router#MethodFunc | ||||||
|  | func RegisterMethod(method string) { | ||||||
|  | 	if method == "" { | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 	method = strings.ToUpper(method) | ||||||
|  | 	if _, ok := methodMap[method]; ok { | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 	n := len(methodMap) | ||||||
|  | 	if n > strconv.IntSize { | ||||||
|  | 		panic(fmt.Sprintf("chi: max number of methods reached (%d)", strconv.IntSize)) | ||||||
|  | 	} | ||||||
|  | 	mt := methodTyp(math.Exp2(float64(n))) | ||||||
|  | 	methodMap[method] = mt | ||||||
|  | 	mALL |= mt | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | type nodeTyp uint8 | ||||||
|  | 
 | ||||||
|  | const ( | ||||||
|  | 	ntStatic   nodeTyp = iota // /home | ||||||
|  | 	ntRegexp                  // /{id:[0-9]+} | ||||||
|  | 	ntParam                   // /{user} | ||||||
|  | 	ntCatchAll                // /api/v1/* | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | type node struct { | ||||||
|  | 	// node type: static, regexp, param, catchAll | ||||||
|  | 	typ nodeTyp | ||||||
|  | 
 | ||||||
|  | 	// first byte of the prefix | ||||||
|  | 	label byte | ||||||
|  | 
 | ||||||
|  | 	// first byte of the child prefix | ||||||
|  | 	tail byte | ||||||
|  | 
 | ||||||
|  | 	// prefix is the common prefix we ignore | ||||||
|  | 	prefix string | ||||||
|  | 
 | ||||||
|  | 	// regexp matcher for regexp nodes | ||||||
|  | 	rex *regexp.Regexp | ||||||
|  | 
 | ||||||
|  | 	// HTTP handler endpoints on the leaf node | ||||||
|  | 	endpoints endpoints | ||||||
|  | 
 | ||||||
|  | 	// subroutes on the leaf node | ||||||
|  | 	subroutes Routes | ||||||
|  | 
 | ||||||
|  | 	// child nodes should be stored in-order for iteration, | ||||||
|  | 	// in groups of the node type. | ||||||
|  | 	children [ntCatchAll + 1]nodes | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // endpoints is a mapping of http method constants to handlers | ||||||
|  | // for a given route. | ||||||
|  | type endpoints map[methodTyp]*endpoint | ||||||
|  | 
 | ||||||
|  | type endpoint struct { | ||||||
|  | 	// endpoint handler | ||||||
|  | 	handler http.Handler | ||||||
|  | 
 | ||||||
|  | 	// pattern is the routing pattern for handler nodes | ||||||
|  | 	pattern string | ||||||
|  | 
 | ||||||
|  | 	// parameter keys recorded on handler nodes | ||||||
|  | 	paramKeys []string | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (s endpoints) Value(method methodTyp) *endpoint { | ||||||
|  | 	mh, ok := s[method] | ||||||
|  | 	if !ok { | ||||||
|  | 		mh = &endpoint{} | ||||||
|  | 		s[method] = mh | ||||||
|  | 	} | ||||||
|  | 	return mh | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (n *node) InsertRoute(method methodTyp, pattern string, handler http.Handler) *node { | ||||||
|  | 	var parent *node | ||||||
|  | 	search := pattern | ||||||
|  | 
 | ||||||
|  | 	for { | ||||||
|  | 		// Handle key exhaustion | ||||||
|  | 		if len(search) == 0 { | ||||||
|  | 			// Insert or update the node's leaf handler | ||||||
|  | 			n.setEndpoint(method, handler, pattern) | ||||||
|  | 			return n | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		// We're going to be searching for a wild node next, | ||||||
|  | 		// in this case, we need to get the tail | ||||||
|  | 		var label = search[0] | ||||||
|  | 		var segTail byte | ||||||
|  | 		var segEndIdx int | ||||||
|  | 		var segTyp nodeTyp | ||||||
|  | 		var segRexpat string | ||||||
|  | 		if label == '{' || label == '*' { | ||||||
|  | 			segTyp, _, segRexpat, segTail, _, segEndIdx = patNextSegment(search) | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		var prefix string | ||||||
|  | 		if segTyp == ntRegexp { | ||||||
|  | 			prefix = segRexpat | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		// Look for the edge to attach to | ||||||
|  | 		parent = n | ||||||
|  | 		n = n.getEdge(segTyp, label, segTail, prefix) | ||||||
|  | 
 | ||||||
|  | 		// No edge, create one | ||||||
|  | 		if n == nil { | ||||||
|  | 			child := &node{label: label, tail: segTail, prefix: search} | ||||||
|  | 			hn := parent.addChild(child, search) | ||||||
|  | 			hn.setEndpoint(method, handler, pattern) | ||||||
|  | 
 | ||||||
|  | 			return hn | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		// Found an edge to match the pattern | ||||||
|  | 
 | ||||||
|  | 		if n.typ > ntStatic { | ||||||
|  | 			// We found a param node, trim the param from the search path and continue. | ||||||
|  | 			// This param/wild pattern segment would already be on the tree from a previous | ||||||
|  | 			// call to addChild when creating a new node. | ||||||
|  | 			search = search[segEndIdx:] | ||||||
|  | 			continue | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		// Static nodes fall below here. | ||||||
|  | 		// Determine longest prefix of the search key on match. | ||||||
|  | 		commonPrefix := longestPrefix(search, n.prefix) | ||||||
|  | 		if commonPrefix == len(n.prefix) { | ||||||
|  | 			// the common prefix is as long as the current node's prefix we're attempting to insert. | ||||||
|  | 			// keep the search going. | ||||||
|  | 			search = search[commonPrefix:] | ||||||
|  | 			continue | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		// Split the node | ||||||
|  | 		child := &node{ | ||||||
|  | 			typ:    ntStatic, | ||||||
|  | 			prefix: search[:commonPrefix], | ||||||
|  | 		} | ||||||
|  | 		parent.replaceChild(search[0], segTail, child) | ||||||
|  | 
 | ||||||
|  | 		// Restore the existing node | ||||||
|  | 		n.label = n.prefix[commonPrefix] | ||||||
|  | 		n.prefix = n.prefix[commonPrefix:] | ||||||
|  | 		child.addChild(n, n.prefix) | ||||||
|  | 
 | ||||||
|  | 		// If the new key is a subset, set the method/handler on this node and finish. | ||||||
|  | 		search = search[commonPrefix:] | ||||||
|  | 		if len(search) == 0 { | ||||||
|  | 			child.setEndpoint(method, handler, pattern) | ||||||
|  | 			return child | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		// Create a new edge for the node | ||||||
|  | 		subchild := &node{ | ||||||
|  | 			typ:    ntStatic, | ||||||
|  | 			label:  search[0], | ||||||
|  | 			prefix: search, | ||||||
|  | 		} | ||||||
|  | 		hn := child.addChild(subchild, search) | ||||||
|  | 		hn.setEndpoint(method, handler, pattern) | ||||||
|  | 		return hn | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // addChild appends the new `child` node to the tree using the `pattern` as the trie key. | ||||||
|  | // For a URL router like chi's, we split the static, param, regexp and wildcard segments | ||||||
|  | // into different nodes. In addition, addChild will recursively call itself until every | ||||||
|  | // pattern segment is added to the url pattern tree as individual nodes, depending on type. | ||||||
|  | func (n *node) addChild(child *node, prefix string) *node { | ||||||
|  | 	search := prefix | ||||||
|  | 
 | ||||||
|  | 	// handler leaf node added to the tree is the child. | ||||||
|  | 	// this may be overridden later down the flow | ||||||
|  | 	hn := child | ||||||
|  | 
 | ||||||
|  | 	// Parse next segment | ||||||
|  | 	segTyp, _, segRexpat, segTail, segStartIdx, segEndIdx := patNextSegment(search) | ||||||
|  | 
 | ||||||
|  | 	// Add child depending on next up segment | ||||||
|  | 	switch segTyp { | ||||||
|  | 
 | ||||||
|  | 	case ntStatic: | ||||||
|  | 		// Search prefix is all static (that is, has no params in path) | ||||||
|  | 		// noop | ||||||
|  | 
 | ||||||
|  | 	default: | ||||||
|  | 		// Search prefix contains a param, regexp or wildcard | ||||||
|  | 
 | ||||||
|  | 		if segTyp == ntRegexp { | ||||||
|  | 			rex, err := regexp.Compile(segRexpat) | ||||||
|  | 			if err != nil { | ||||||
|  | 				panic(fmt.Sprintf("chi: invalid regexp pattern '%s' in route param", segRexpat)) | ||||||
|  | 			} | ||||||
|  | 			child.prefix = segRexpat | ||||||
|  | 			child.rex = rex | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		if segStartIdx == 0 { | ||||||
|  | 			// Route starts with a param | ||||||
|  | 			child.typ = segTyp | ||||||
|  | 
 | ||||||
|  | 			if segTyp == ntCatchAll { | ||||||
|  | 				segStartIdx = -1 | ||||||
|  | 			} else { | ||||||
|  | 				segStartIdx = segEndIdx | ||||||
|  | 			} | ||||||
|  | 			if segStartIdx < 0 { | ||||||
|  | 				segStartIdx = len(search) | ||||||
|  | 			} | ||||||
|  | 			child.tail = segTail // for params, we set the tail | ||||||
|  | 
 | ||||||
|  | 			if segStartIdx != len(search) { | ||||||
|  | 				// add static edge for the remaining part, split the end. | ||||||
|  | 				// its not possible to have adjacent param nodes, so its certainly | ||||||
|  | 				// going to be a static node next. | ||||||
|  | 
 | ||||||
|  | 				search = search[segStartIdx:] // advance search position | ||||||
|  | 
 | ||||||
|  | 				nn := &node{ | ||||||
|  | 					typ:    ntStatic, | ||||||
|  | 					label:  search[0], | ||||||
|  | 					prefix: search, | ||||||
|  | 				} | ||||||
|  | 				hn = child.addChild(nn, search) | ||||||
|  | 			} | ||||||
|  | 
 | ||||||
|  | 		} else if segStartIdx > 0 { | ||||||
|  | 			// Route has some param | ||||||
|  | 
 | ||||||
|  | 			// starts with a static segment | ||||||
|  | 			child.typ = ntStatic | ||||||
|  | 			child.prefix = search[:segStartIdx] | ||||||
|  | 			child.rex = nil | ||||||
|  | 
 | ||||||
|  | 			// add the param edge node | ||||||
|  | 			search = search[segStartIdx:] | ||||||
|  | 
 | ||||||
|  | 			nn := &node{ | ||||||
|  | 				typ:   segTyp, | ||||||
|  | 				label: search[0], | ||||||
|  | 				tail:  segTail, | ||||||
|  | 			} | ||||||
|  | 			hn = child.addChild(nn, search) | ||||||
|  | 
 | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	n.children[child.typ] = append(n.children[child.typ], child) | ||||||
|  | 	n.children[child.typ].Sort() | ||||||
|  | 	return hn | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (n *node) replaceChild(label, tail byte, child *node) { | ||||||
|  | 	for i := 0; i < len(n.children[child.typ]); i++ { | ||||||
|  | 		if n.children[child.typ][i].label == label && n.children[child.typ][i].tail == tail { | ||||||
|  | 			n.children[child.typ][i] = child | ||||||
|  | 			n.children[child.typ][i].label = label | ||||||
|  | 			n.children[child.typ][i].tail = tail | ||||||
|  | 			return | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 	panic("chi: replacing missing child") | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (n *node) getEdge(ntyp nodeTyp, label, tail byte, prefix string) *node { | ||||||
|  | 	nds := n.children[ntyp] | ||||||
|  | 	for i := 0; i < len(nds); i++ { | ||||||
|  | 		if nds[i].label == label && nds[i].tail == tail { | ||||||
|  | 			if ntyp == ntRegexp && nds[i].prefix != prefix { | ||||||
|  | 				continue | ||||||
|  | 			} | ||||||
|  | 			return nds[i] | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 	return nil | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (n *node) setEndpoint(method methodTyp, handler http.Handler, pattern string) { | ||||||
|  | 	// Set the handler for the method type on the node | ||||||
|  | 	if n.endpoints == nil { | ||||||
|  | 		n.endpoints = make(endpoints, 0) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	paramKeys := patParamKeys(pattern) | ||||||
|  | 
 | ||||||
|  | 	if method&mSTUB == mSTUB { | ||||||
|  | 		n.endpoints.Value(mSTUB).handler = handler | ||||||
|  | 	} | ||||||
|  | 	if method&mALL == mALL { | ||||||
|  | 		h := n.endpoints.Value(mALL) | ||||||
|  | 		h.handler = handler | ||||||
|  | 		h.pattern = pattern | ||||||
|  | 		h.paramKeys = paramKeys | ||||||
|  | 		for _, m := range methodMap { | ||||||
|  | 			h := n.endpoints.Value(m) | ||||||
|  | 			h.handler = handler | ||||||
|  | 			h.pattern = pattern | ||||||
|  | 			h.paramKeys = paramKeys | ||||||
|  | 		} | ||||||
|  | 	} else { | ||||||
|  | 		h := n.endpoints.Value(method) | ||||||
|  | 		h.handler = handler | ||||||
|  | 		h.pattern = pattern | ||||||
|  | 		h.paramKeys = paramKeys | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (n *node) FindRoute(rctx *Context, method methodTyp, path string) (*node, endpoints, http.Handler) { | ||||||
|  | 	// Reset the context routing pattern and params | ||||||
|  | 	rctx.routePattern = "" | ||||||
|  | 	rctx.routeParams.Keys = rctx.routeParams.Keys[:0] | ||||||
|  | 	rctx.routeParams.Values = rctx.routeParams.Values[:0] | ||||||
|  | 
 | ||||||
|  | 	// Find the routing handlers for the path | ||||||
|  | 	rn := n.findRoute(rctx, method, path) | ||||||
|  | 	if rn == nil { | ||||||
|  | 		return nil, nil, nil | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	// Record the routing params in the request lifecycle | ||||||
|  | 	rctx.URLParams.Keys = append(rctx.URLParams.Keys, rctx.routeParams.Keys...) | ||||||
|  | 	rctx.URLParams.Values = append(rctx.URLParams.Values, rctx.routeParams.Values...) | ||||||
|  | 
 | ||||||
|  | 	// Record the routing pattern in the request lifecycle | ||||||
|  | 	if rn.endpoints[method].pattern != "" { | ||||||
|  | 		rctx.routePattern = rn.endpoints[method].pattern | ||||||
|  | 		rctx.RoutePatterns = append(rctx.RoutePatterns, rctx.routePattern) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	return rn, rn.endpoints, rn.endpoints[method].handler | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // Recursive edge traversal by checking all nodeTyp groups along the way. | ||||||
|  | // It's like searching through a multi-dimensional radix trie. | ||||||
|  | func (n *node) findRoute(rctx *Context, method methodTyp, path string) *node { | ||||||
|  | 	nn := n | ||||||
|  | 	search := path | ||||||
|  | 
 | ||||||
|  | 	for t, nds := range nn.children { | ||||||
|  | 		ntyp := nodeTyp(t) | ||||||
|  | 		if len(nds) == 0 { | ||||||
|  | 			continue | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		var xn *node | ||||||
|  | 		xsearch := search | ||||||
|  | 
 | ||||||
|  | 		var label byte | ||||||
|  | 		if search != "" { | ||||||
|  | 			label = search[0] | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		switch ntyp { | ||||||
|  | 		case ntStatic: | ||||||
|  | 			xn = nds.findEdge(label) | ||||||
|  | 			if xn == nil || !strings.HasPrefix(xsearch, xn.prefix) { | ||||||
|  | 				continue | ||||||
|  | 			} | ||||||
|  | 			xsearch = xsearch[len(xn.prefix):] | ||||||
|  | 
 | ||||||
|  | 		case ntParam, ntRegexp: | ||||||
|  | 			// short-circuit and return no matching route for empty param values | ||||||
|  | 			if xsearch == "" { | ||||||
|  | 				continue | ||||||
|  | 			} | ||||||
|  | 
 | ||||||
|  | 			// serially loop through each node grouped by the tail delimiter | ||||||
|  | 			for idx := 0; idx < len(nds); idx++ { | ||||||
|  | 				xn = nds[idx] | ||||||
|  | 
 | ||||||
|  | 				// label for param nodes is the delimiter byte | ||||||
|  | 				p := strings.IndexByte(xsearch, xn.tail) | ||||||
|  | 
 | ||||||
|  | 				if p < 0 { | ||||||
|  | 					if xn.tail == '/' { | ||||||
|  | 						p = len(xsearch) | ||||||
|  | 					} else { | ||||||
|  | 						continue | ||||||
|  | 					} | ||||||
|  | 				} | ||||||
|  | 
 | ||||||
|  | 				if ntyp == ntRegexp && xn.rex != nil { | ||||||
|  | 					if xn.rex.Match([]byte(xsearch[:p])) == false { | ||||||
|  | 						continue | ||||||
|  | 					} | ||||||
|  | 				} else if strings.IndexByte(xsearch[:p], '/') != -1 { | ||||||
|  | 					// avoid a match across path segments | ||||||
|  | 					continue | ||||||
|  | 				} | ||||||
|  | 
 | ||||||
|  | 				rctx.routeParams.Values = append(rctx.routeParams.Values, xsearch[:p]) | ||||||
|  | 				xsearch = xsearch[p:] | ||||||
|  | 				break | ||||||
|  | 			} | ||||||
|  | 
 | ||||||
|  | 		default: | ||||||
|  | 			// catch-all nodes | ||||||
|  | 			rctx.routeParams.Values = append(rctx.routeParams.Values, search) | ||||||
|  | 			xn = nds[0] | ||||||
|  | 			xsearch = "" | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		if xn == nil { | ||||||
|  | 			continue | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		// did we find it yet? | ||||||
|  | 		if len(xsearch) == 0 { | ||||||
|  | 			if xn.isLeaf() { | ||||||
|  | 				h, _ := xn.endpoints[method] | ||||||
|  | 				if h != nil && h.handler != nil { | ||||||
|  | 					rctx.routeParams.Keys = append(rctx.routeParams.Keys, h.paramKeys...) | ||||||
|  | 					return xn | ||||||
|  | 				} | ||||||
|  | 
 | ||||||
|  | 				// flag that the routing context found a route, but not a corresponding | ||||||
|  | 				// supported method | ||||||
|  | 				rctx.methodNotAllowed = true | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		// recursively find the next node.. | ||||||
|  | 		fin := xn.findRoute(rctx, method, xsearch) | ||||||
|  | 		if fin != nil { | ||||||
|  | 			return fin | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		// Did not find final handler, let's remove the param here if it was set | ||||||
|  | 		if xn.typ > ntStatic { | ||||||
|  | 			if len(rctx.routeParams.Values) > 0 { | ||||||
|  | 				rctx.routeParams.Values = rctx.routeParams.Values[:len(rctx.routeParams.Values)-1] | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	return nil | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (n *node) findEdge(ntyp nodeTyp, label byte) *node { | ||||||
|  | 	nds := n.children[ntyp] | ||||||
|  | 	num := len(nds) | ||||||
|  | 	idx := 0 | ||||||
|  | 
 | ||||||
|  | 	switch ntyp { | ||||||
|  | 	case ntStatic, ntParam, ntRegexp: | ||||||
|  | 		i, j := 0, num-1 | ||||||
|  | 		for i <= j { | ||||||
|  | 			idx = i + (j-i)/2 | ||||||
|  | 			if label > nds[idx].label { | ||||||
|  | 				i = idx + 1 | ||||||
|  | 			} else if label < nds[idx].label { | ||||||
|  | 				j = idx - 1 | ||||||
|  | 			} else { | ||||||
|  | 				i = num // breaks cond | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 		if nds[idx].label != label { | ||||||
|  | 			return nil | ||||||
|  | 		} | ||||||
|  | 		return nds[idx] | ||||||
|  | 
 | ||||||
|  | 	default: // catch all | ||||||
|  | 		return nds[idx] | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (n *node) isEmpty() bool { | ||||||
|  | 	for _, nds := range n.children { | ||||||
|  | 		if len(nds) > 0 { | ||||||
|  | 			return false | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 	return true | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (n *node) isLeaf() bool { | ||||||
|  | 	return n.endpoints != nil | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (n *node) findPattern(pattern string) bool { | ||||||
|  | 	nn := n | ||||||
|  | 	for _, nds := range nn.children { | ||||||
|  | 		if len(nds) == 0 { | ||||||
|  | 			continue | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		n = nn.findEdge(nds[0].typ, pattern[0]) | ||||||
|  | 		if n == nil { | ||||||
|  | 			continue | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		var idx int | ||||||
|  | 		var xpattern string | ||||||
|  | 
 | ||||||
|  | 		switch n.typ { | ||||||
|  | 		case ntStatic: | ||||||
|  | 			idx = longestPrefix(pattern, n.prefix) | ||||||
|  | 			if idx < len(n.prefix) { | ||||||
|  | 				continue | ||||||
|  | 			} | ||||||
|  | 
 | ||||||
|  | 		case ntParam, ntRegexp: | ||||||
|  | 			idx = strings.IndexByte(pattern, '}') + 1 | ||||||
|  | 
 | ||||||
|  | 		case ntCatchAll: | ||||||
|  | 			idx = longestPrefix(pattern, "*") | ||||||
|  | 
 | ||||||
|  | 		default: | ||||||
|  | 			panic("chi: unknown node type") | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		xpattern = pattern[idx:] | ||||||
|  | 		if len(xpattern) == 0 { | ||||||
|  | 			return true | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		return n.findPattern(xpattern) | ||||||
|  | 	} | ||||||
|  | 	return false | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (n *node) routes() []Route { | ||||||
|  | 	rts := []Route{} | ||||||
|  | 
 | ||||||
|  | 	n.walk(func(eps endpoints, subroutes Routes) bool { | ||||||
|  | 		if eps[mSTUB] != nil && eps[mSTUB].handler != nil && subroutes == nil { | ||||||
|  | 			return false | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		// Group methodHandlers by unique patterns | ||||||
|  | 		pats := make(map[string]endpoints, 0) | ||||||
|  | 
 | ||||||
|  | 		for mt, h := range eps { | ||||||
|  | 			if h.pattern == "" { | ||||||
|  | 				continue | ||||||
|  | 			} | ||||||
|  | 			p, ok := pats[h.pattern] | ||||||
|  | 			if !ok { | ||||||
|  | 				p = endpoints{} | ||||||
|  | 				pats[h.pattern] = p | ||||||
|  | 			} | ||||||
|  | 			p[mt] = h | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		for p, mh := range pats { | ||||||
|  | 			hs := make(map[string]http.Handler, 0) | ||||||
|  | 			if mh[mALL] != nil && mh[mALL].handler != nil { | ||||||
|  | 				hs["*"] = mh[mALL].handler | ||||||
|  | 			} | ||||||
|  | 
 | ||||||
|  | 			for mt, h := range mh { | ||||||
|  | 				if h.handler == nil { | ||||||
|  | 					continue | ||||||
|  | 				} | ||||||
|  | 				m := methodTypString(mt) | ||||||
|  | 				if m == "" { | ||||||
|  | 					continue | ||||||
|  | 				} | ||||||
|  | 				hs[m] = h.handler | ||||||
|  | 			} | ||||||
|  | 
 | ||||||
|  | 			rt := Route{p, hs, subroutes} | ||||||
|  | 			rts = append(rts, rt) | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		return false | ||||||
|  | 	}) | ||||||
|  | 
 | ||||||
|  | 	return rts | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (n *node) walk(fn func(eps endpoints, subroutes Routes) bool) bool { | ||||||
|  | 	// Visit the leaf values if any | ||||||
|  | 	if (n.endpoints != nil || n.subroutes != nil) && fn(n.endpoints, n.subroutes) { | ||||||
|  | 		return true | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	// Recurse on the children | ||||||
|  | 	for _, ns := range n.children { | ||||||
|  | 		for _, cn := range ns { | ||||||
|  | 			if cn.walk(fn) { | ||||||
|  | 				return true | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 	return false | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // patNextSegment returns the next segment details from a pattern: | ||||||
|  | // node type, param key, regexp string, param tail byte, param starting index, param ending index | ||||||
|  | func patNextSegment(pattern string) (nodeTyp, string, string, byte, int, int) { | ||||||
|  | 	ps := strings.Index(pattern, "{") | ||||||
|  | 	ws := strings.Index(pattern, "*") | ||||||
|  | 
 | ||||||
|  | 	if ps < 0 && ws < 0 { | ||||||
|  | 		return ntStatic, "", "", 0, 0, len(pattern) // we return the entire thing | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	// Sanity check | ||||||
|  | 	if ws >= 0 && ws != len(pattern)-1 { | ||||||
|  | 		panic("chi: wildcard '*' must be the last value in a route. trim trailing text or use a '{param}' instead") | ||||||
|  | 	} | ||||||
|  | 	if ps >= 0 && ws >= 0 && ws < ps { | ||||||
|  | 		panic("chi: wildcard '*' must be the last pattern in a route, otherwise use a '{param}'") | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	var tail byte = '/' // Default endpoint tail to / byte | ||||||
|  | 
 | ||||||
|  | 	if ps >= 0 { | ||||||
|  | 		// Param/Regexp pattern is next | ||||||
|  | 		nt := ntParam | ||||||
|  | 
 | ||||||
|  | 		// Read to closing } taking into account opens and closes in curl count (cc) | ||||||
|  | 		cc := 0 | ||||||
|  | 		pe := ps | ||||||
|  | 		for i, c := range pattern[ps:] { | ||||||
|  | 			if c == '{' { | ||||||
|  | 				cc++ | ||||||
|  | 			} else if c == '}' { | ||||||
|  | 				cc-- | ||||||
|  | 				if cc == 0 { | ||||||
|  | 					pe = ps + i | ||||||
|  | 					break | ||||||
|  | 				} | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 		if pe == ps { | ||||||
|  | 			panic("chi: route param closing delimiter '}' is missing") | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		key := pattern[ps+1 : pe] | ||||||
|  | 		pe++ // set end to next position | ||||||
|  | 
 | ||||||
|  | 		if pe < len(pattern) { | ||||||
|  | 			tail = pattern[pe] | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		var rexpat string | ||||||
|  | 		if idx := strings.Index(key, ":"); idx >= 0 { | ||||||
|  | 			nt = ntRegexp | ||||||
|  | 			rexpat = key[idx+1:] | ||||||
|  | 			key = key[:idx] | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		if len(rexpat) > 0 { | ||||||
|  | 			if rexpat[0] != '^' { | ||||||
|  | 				rexpat = "^" + rexpat | ||||||
|  | 			} | ||||||
|  | 			if rexpat[len(rexpat)-1] != '$' { | ||||||
|  | 				rexpat = rexpat + "$" | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		return nt, key, rexpat, tail, ps, pe | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	// Wildcard pattern as finale | ||||||
|  | 	// TODO: should we panic if there is stuff after the * ??? | ||||||
|  | 	return ntCatchAll, "*", "", 0, ws, len(pattern) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func patParamKeys(pattern string) []string { | ||||||
|  | 	pat := pattern | ||||||
|  | 	paramKeys := []string{} | ||||||
|  | 	for { | ||||||
|  | 		ptyp, paramKey, _, _, _, e := patNextSegment(pat) | ||||||
|  | 		if ptyp == ntStatic { | ||||||
|  | 			return paramKeys | ||||||
|  | 		} | ||||||
|  | 		for i := 0; i < len(paramKeys); i++ { | ||||||
|  | 			if paramKeys[i] == paramKey { | ||||||
|  | 				panic(fmt.Sprintf("chi: routing pattern '%s' contains duplicate param key, '%s'", pattern, paramKey)) | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 		paramKeys = append(paramKeys, paramKey) | ||||||
|  | 		pat = pat[e:] | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // longestPrefix finds the length of the shared prefix | ||||||
|  | // of two strings | ||||||
|  | func longestPrefix(k1, k2 string) int { | ||||||
|  | 	max := len(k1) | ||||||
|  | 	if l := len(k2); l < max { | ||||||
|  | 		max = l | ||||||
|  | 	} | ||||||
|  | 	var i int | ||||||
|  | 	for i = 0; i < max; i++ { | ||||||
|  | 		if k1[i] != k2[i] { | ||||||
|  | 			break | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 	return i | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func methodTypString(method methodTyp) string { | ||||||
|  | 	for s, t := range methodMap { | ||||||
|  | 		if method == t { | ||||||
|  | 			return s | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 	return "" | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | type nodes []*node | ||||||
|  | 
 | ||||||
|  | // Sort the list of nodes by label | ||||||
|  | func (ns nodes) Sort()              { sort.Sort(ns); ns.tailSort() } | ||||||
|  | func (ns nodes) Len() int           { return len(ns) } | ||||||
|  | func (ns nodes) Swap(i, j int)      { ns[i], ns[j] = ns[j], ns[i] } | ||||||
|  | func (ns nodes) Less(i, j int) bool { return ns[i].label < ns[j].label } | ||||||
|  | 
 | ||||||
|  | // tailSort pushes nodes with '/' as the tail to the end of the list for param nodes. | ||||||
|  | // The list order determines the traversal order. | ||||||
|  | func (ns nodes) tailSort() { | ||||||
|  | 	for i := len(ns) - 1; i >= 0; i-- { | ||||||
|  | 		if ns[i].typ > ntStatic && ns[i].tail == '/' { | ||||||
|  | 			ns.Swap(i, len(ns)-1) | ||||||
|  | 			return | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (ns nodes) findEdge(label byte) *node { | ||||||
|  | 	num := len(ns) | ||||||
|  | 	idx := 0 | ||||||
|  | 	i, j := 0, num-1 | ||||||
|  | 	for i <= j { | ||||||
|  | 		idx = i + (j-i)/2 | ||||||
|  | 		if label > ns[idx].label { | ||||||
|  | 			i = idx + 1 | ||||||
|  | 		} else if label < ns[idx].label { | ||||||
|  | 			j = idx - 1 | ||||||
|  | 		} else { | ||||||
|  | 			i = num // breaks cond | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 	if ns[idx].label != label { | ||||||
|  | 		return nil | ||||||
|  | 	} | ||||||
|  | 	return ns[idx] | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // Route describes the details of a routing handler. | ||||||
|  | type Route struct { | ||||||
|  | 	Pattern   string | ||||||
|  | 	Handlers  map[string]http.Handler | ||||||
|  | 	SubRoutes Routes | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // WalkFunc is the type of the function called for each method and route visited by Walk. | ||||||
|  | type WalkFunc func(method string, route string, handler http.Handler, middlewares ...func(http.Handler) http.Handler) error | ||||||
|  | 
 | ||||||
|  | // Walk walks any router tree that implements Routes interface. | ||||||
|  | func Walk(r Routes, walkFn WalkFunc) error { | ||||||
|  | 	return walk(r, walkFn, "") | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func walk(r Routes, walkFn WalkFunc, parentRoute string, parentMw ...func(http.Handler) http.Handler) error { | ||||||
|  | 	for _, route := range r.Routes() { | ||||||
|  | 		mws := make([]func(http.Handler) http.Handler, len(parentMw)) | ||||||
|  | 		copy(mws, parentMw) | ||||||
|  | 		mws = append(mws, r.Middlewares()...) | ||||||
|  | 
 | ||||||
|  | 		if route.SubRoutes != nil { | ||||||
|  | 			if err := walk(route.SubRoutes, walkFn, parentRoute+route.Pattern, mws...); err != nil { | ||||||
|  | 				return err | ||||||
|  | 			} | ||||||
|  | 			continue | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		for method, handler := range route.Handlers { | ||||||
|  | 			if method == "*" { | ||||||
|  | 				// Ignore a "catchAll" method, since we pass down all the specific methods for each route. | ||||||
|  | 				continue | ||||||
|  | 			} | ||||||
|  | 
 | ||||||
|  | 			fullRoute := parentRoute + route.Pattern | ||||||
|  | 
 | ||||||
|  | 			if chain, ok := handler.(*ChainHandler); ok { | ||||||
|  | 				if err := walkFn(method, fullRoute, chain.Endpoint, append(mws, chain.Middlewares...)...); err != nil { | ||||||
|  | 					return err | ||||||
|  | 				} | ||||||
|  | 			} else { | ||||||
|  | 				if err := walkFn(method, fullRoute, handler, mws...); err != nil { | ||||||
|  | 					return err | ||||||
|  | 				} | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	return nil | ||||||
|  | } | ||||||
							
								
								
									
										9
									
								
								vendor/github.com/google/uuid/.travis.yml
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								vendor/github.com/google/uuid/.travis.yml
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							| @ -0,0 +1,9 @@ | |||||||
|  | language: go | ||||||
|  | 
 | ||||||
|  | go: | ||||||
|  |   - 1.4.3 | ||||||
|  |   - 1.5.3 | ||||||
|  |   - tip | ||||||
|  | 
 | ||||||
|  | script: | ||||||
|  |   - go test -v ./... | ||||||
							
								
								
									
										10
									
								
								vendor/github.com/google/uuid/CONTRIBUTING.md
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								vendor/github.com/google/uuid/CONTRIBUTING.md
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							| @ -0,0 +1,10 @@ | |||||||
|  | # How to contribute | ||||||
|  | 
 | ||||||
|  | We definitely welcome patches and contribution to this project! | ||||||
|  | 
 | ||||||
|  | ### Legal requirements | ||||||
|  | 
 | ||||||
|  | In order to protect both you and ourselves, you will need to sign the | ||||||
|  | [Contributor License Agreement](https://cla.developers.google.com/clas). | ||||||
|  | 
 | ||||||
|  | You may have already signed it for other Google projects. | ||||||
							
								
								
									
										9
									
								
								vendor/github.com/google/uuid/CONTRIBUTORS
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								vendor/github.com/google/uuid/CONTRIBUTORS
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							| @ -0,0 +1,9 @@ | |||||||
|  | Paul Borman <borman@google.com> | ||||||
|  | bmatsuo | ||||||
|  | shawnps | ||||||
|  | theory | ||||||
|  | jboverfelt | ||||||
|  | dsymonds | ||||||
|  | cd1 | ||||||
|  | wallclockbuilder | ||||||
|  | dansouza | ||||||
							
								
								
									
										27
									
								
								vendor/github.com/google/uuid/LICENSE
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										27
									
								
								vendor/github.com/google/uuid/LICENSE
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							| @ -0,0 +1,27 @@ | |||||||
|  | Copyright (c) 2009,2014 Google Inc. All rights reserved. | ||||||
|  | 
 | ||||||
|  | Redistribution and use in source and binary forms, with or without | ||||||
|  | modification, are permitted provided that the following conditions are | ||||||
|  | met: | ||||||
|  | 
 | ||||||
|  |    * Redistributions of source code must retain the above copyright | ||||||
|  | notice, this list of conditions and the following disclaimer. | ||||||
|  |    * Redistributions in binary form must reproduce the above | ||||||
|  | copyright notice, this list of conditions and the following disclaimer | ||||||
|  | in the documentation and/or other materials provided with the | ||||||
|  | distribution. | ||||||
|  |    * Neither the name of Google Inc. nor the names of its | ||||||
|  | contributors may be used to endorse or promote products derived from | ||||||
|  | this software without specific prior written permission. | ||||||
|  | 
 | ||||||
|  | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | ||||||
|  | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | ||||||
|  | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | ||||||
|  | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | ||||||
|  | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | ||||||
|  | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | ||||||
|  | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | ||||||
|  | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | ||||||
|  | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | ||||||
|  | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | ||||||
|  | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | ||||||
							
								
								
									
										19
									
								
								vendor/github.com/google/uuid/README.md
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								vendor/github.com/google/uuid/README.md
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							| @ -0,0 +1,19 @@ | |||||||
|  | # uuid  | ||||||
|  | The uuid package generates and inspects UUIDs based on | ||||||
|  | [RFC 4122](http://tools.ietf.org/html/rfc4122) | ||||||
|  | and DCE 1.1: Authentication and Security Services.  | ||||||
|  | 
 | ||||||
|  | This package is based on the github.com/pborman/uuid package (previously named | ||||||
|  | code.google.com/p/go-uuid).  It differs from these earlier packages in that | ||||||
|  | a UUID is a 16 byte array rather than a byte slice.  One loss due to this | ||||||
|  | change is the ability to represent an invalid UUID (vs a NIL UUID). | ||||||
|  | 
 | ||||||
|  | ###### Install | ||||||
|  | `go get github.com/google/uuid` | ||||||
|  | 
 | ||||||
|  | ###### Documentation  | ||||||
|  | [](http://godoc.org/github.com/google/uuid) | ||||||
|  | 
 | ||||||
|  | Full `go doc` style documentation for the package can be viewed online without | ||||||
|  | installing this package by using the GoDoc site here:  | ||||||
|  | http://godoc.org/github.com/google/uuid | ||||||
							
								
								
									
										80
									
								
								vendor/github.com/google/uuid/dce.go
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										80
									
								
								vendor/github.com/google/uuid/dce.go
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							| @ -0,0 +1,80 @@ | |||||||
|  | // Copyright 2016 Google Inc.  All rights reserved. | ||||||
|  | // Use of this source code is governed by a BSD-style | ||||||
|  | // license that can be found in the LICENSE file. | ||||||
|  | 
 | ||||||
|  | package uuid | ||||||
|  | 
 | ||||||
|  | import ( | ||||||
|  | 	"encoding/binary" | ||||||
|  | 	"fmt" | ||||||
|  | 	"os" | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | // A Domain represents a Version 2 domain | ||||||
|  | type Domain byte | ||||||
|  | 
 | ||||||
|  | // Domain constants for DCE Security (Version 2) UUIDs. | ||||||
|  | const ( | ||||||
|  | 	Person = Domain(0) | ||||||
|  | 	Group  = Domain(1) | ||||||
|  | 	Org    = Domain(2) | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | // NewDCESecurity returns a DCE Security (Version 2) UUID. | ||||||
|  | // | ||||||
|  | // The domain should be one of Person, Group or Org. | ||||||
|  | // On a POSIX system the id should be the users UID for the Person | ||||||
|  | // domain and the users GID for the Group.  The meaning of id for | ||||||
|  | // the domain Org or on non-POSIX systems is site defined. | ||||||
|  | // | ||||||
|  | // For a given domain/id pair the same token may be returned for up to | ||||||
|  | // 7 minutes and 10 seconds. | ||||||
|  | func NewDCESecurity(domain Domain, id uint32) (UUID, error) { | ||||||
|  | 	uuid, err := NewUUID() | ||||||
|  | 	if err == nil { | ||||||
|  | 		uuid[6] = (uuid[6] & 0x0f) | 0x20 // Version 2 | ||||||
|  | 		uuid[9] = byte(domain) | ||||||
|  | 		binary.BigEndian.PutUint32(uuid[0:], id) | ||||||
|  | 	} | ||||||
|  | 	return uuid, err | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // NewDCEPerson returns a DCE Security (Version 2) UUID in the person | ||||||
|  | // domain with the id returned by os.Getuid. | ||||||
|  | // | ||||||
|  | //  NewDCESecurity(Person, uint32(os.Getuid())) | ||||||
|  | func NewDCEPerson() (UUID, error) { | ||||||
|  | 	return NewDCESecurity(Person, uint32(os.Getuid())) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // NewDCEGroup returns a DCE Security (Version 2) UUID in the group | ||||||
|  | // domain with the id returned by os.Getgid. | ||||||
|  | // | ||||||
|  | //  NewDCESecurity(Group, uint32(os.Getgid())) | ||||||
|  | func NewDCEGroup() (UUID, error) { | ||||||
|  | 	return NewDCESecurity(Group, uint32(os.Getgid())) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // Domain returns the domain for a Version 2 UUID.  Domains are only defined | ||||||
|  | // for Version 2 UUIDs. | ||||||
|  | func (uuid UUID) Domain() Domain { | ||||||
|  | 	return Domain(uuid[9]) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // ID returns the id for a Version 2 UUID. IDs are only defined for Version 2 | ||||||
|  | // UUIDs. | ||||||
|  | func (uuid UUID) ID() uint32 { | ||||||
|  | 	return binary.BigEndian.Uint32(uuid[0:4]) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (d Domain) String() string { | ||||||
|  | 	switch d { | ||||||
|  | 	case Person: | ||||||
|  | 		return "Person" | ||||||
|  | 	case Group: | ||||||
|  | 		return "Group" | ||||||
|  | 	case Org: | ||||||
|  | 		return "Org" | ||||||
|  | 	} | ||||||
|  | 	return fmt.Sprintf("Domain%d", int(d)) | ||||||
|  | } | ||||||
							
								
								
									
										12
									
								
								vendor/github.com/google/uuid/doc.go
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								vendor/github.com/google/uuid/doc.go
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							| @ -0,0 +1,12 @@ | |||||||
|  | // Copyright 2016 Google Inc.  All rights reserved. | ||||||
|  | // Use of this source code is governed by a BSD-style | ||||||
|  | // license that can be found in the LICENSE file. | ||||||
|  | 
 | ||||||
|  | // Package uuid generates and inspects UUIDs. | ||||||
|  | // | ||||||
|  | // UUIDs are based on RFC 4122 and DCE 1.1: Authentication and Security | ||||||
|  | // Services. | ||||||
|  | // | ||||||
|  | // A UUID is a 16 byte (128 bit) array.  UUIDs may be used as keys to | ||||||
|  | // maps or compared directly. | ||||||
|  | package uuid | ||||||
							
								
								
									
										1
									
								
								vendor/github.com/google/uuid/go.mod
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								vendor/github.com/google/uuid/go.mod
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							| @ -0,0 +1 @@ | |||||||
|  | module github.com/google/uuid | ||||||
							
								
								
									
										53
									
								
								vendor/github.com/google/uuid/hash.go
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										53
									
								
								vendor/github.com/google/uuid/hash.go
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							| @ -0,0 +1,53 @@ | |||||||
|  | // Copyright 2016 Google Inc.  All rights reserved. | ||||||
|  | // Use of this source code is governed by a BSD-style | ||||||
|  | // license that can be found in the LICENSE file. | ||||||
|  | 
 | ||||||
|  | package uuid | ||||||
|  | 
 | ||||||
|  | import ( | ||||||
|  | 	"crypto/md5" | ||||||
|  | 	"crypto/sha1" | ||||||
|  | 	"hash" | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | // Well known namespace IDs and UUIDs | ||||||
|  | var ( | ||||||
|  | 	NameSpaceDNS  = Must(Parse("6ba7b810-9dad-11d1-80b4-00c04fd430c8")) | ||||||
|  | 	NameSpaceURL  = Must(Parse("6ba7b811-9dad-11d1-80b4-00c04fd430c8")) | ||||||
|  | 	NameSpaceOID  = Must(Parse("6ba7b812-9dad-11d1-80b4-00c04fd430c8")) | ||||||
|  | 	NameSpaceX500 = Must(Parse("6ba7b814-9dad-11d1-80b4-00c04fd430c8")) | ||||||
|  | 	Nil           UUID // empty UUID, all zeros | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | // NewHash returns a new UUID derived from the hash of space concatenated with | ||||||
|  | // data generated by h.  The hash should be at least 16 byte in length.  The | ||||||
|  | // first 16 bytes of the hash are used to form the UUID.  The version of the | ||||||
|  | // UUID will be the lower 4 bits of version.  NewHash is used to implement | ||||||
|  | // NewMD5 and NewSHA1. | ||||||
|  | func NewHash(h hash.Hash, space UUID, data []byte, version int) UUID { | ||||||
|  | 	h.Reset() | ||||||
|  | 	h.Write(space[:]) | ||||||
|  | 	h.Write(data) | ||||||
|  | 	s := h.Sum(nil) | ||||||
|  | 	var uuid UUID | ||||||
|  | 	copy(uuid[:], s) | ||||||
|  | 	uuid[6] = (uuid[6] & 0x0f) | uint8((version&0xf)<<4) | ||||||
|  | 	uuid[8] = (uuid[8] & 0x3f) | 0x80 // RFC 4122 variant | ||||||
|  | 	return uuid | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // NewMD5 returns a new MD5 (Version 3) UUID based on the | ||||||
|  | // supplied name space and data.  It is the same as calling: | ||||||
|  | // | ||||||
|  | //  NewHash(md5.New(), space, data, 3) | ||||||
|  | func NewMD5(space UUID, data []byte) UUID { | ||||||
|  | 	return NewHash(md5.New(), space, data, 3) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // NewSHA1 returns a new SHA1 (Version 5) UUID based on the | ||||||
|  | // supplied name space and data.  It is the same as calling: | ||||||
|  | // | ||||||
|  | //  NewHash(sha1.New(), space, data, 5) | ||||||
|  | func NewSHA1(space UUID, data []byte) UUID { | ||||||
|  | 	return NewHash(sha1.New(), space, data, 5) | ||||||
|  | } | ||||||
							
								
								
									
										37
									
								
								vendor/github.com/google/uuid/marshal.go
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										37
									
								
								vendor/github.com/google/uuid/marshal.go
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							| @ -0,0 +1,37 @@ | |||||||
|  | // Copyright 2016 Google Inc.  All rights reserved. | ||||||
|  | // Use of this source code is governed by a BSD-style | ||||||
|  | // license that can be found in the LICENSE file. | ||||||
|  | 
 | ||||||
|  | package uuid | ||||||
|  | 
 | ||||||
|  | import "fmt" | ||||||
|  | 
 | ||||||
|  | // MarshalText implements encoding.TextMarshaler. | ||||||
|  | func (uuid UUID) MarshalText() ([]byte, error) { | ||||||
|  | 	var js [36]byte | ||||||
|  | 	encodeHex(js[:], uuid) | ||||||
|  | 	return js[:], nil | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // UnmarshalText implements encoding.TextUnmarshaler. | ||||||
|  | func (uuid *UUID) UnmarshalText(data []byte) error { | ||||||
|  | 	id, err := ParseBytes(data) | ||||||
|  | 	if err == nil { | ||||||
|  | 		*uuid = id | ||||||
|  | 	} | ||||||
|  | 	return err | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // MarshalBinary implements encoding.BinaryMarshaler. | ||||||
|  | func (uuid UUID) MarshalBinary() ([]byte, error) { | ||||||
|  | 	return uuid[:], nil | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // UnmarshalBinary implements encoding.BinaryUnmarshaler. | ||||||
|  | func (uuid *UUID) UnmarshalBinary(data []byte) error { | ||||||
|  | 	if len(data) != 16 { | ||||||
|  | 		return fmt.Errorf("invalid UUID (got %d bytes)", len(data)) | ||||||
|  | 	} | ||||||
|  | 	copy(uuid[:], data) | ||||||
|  | 	return nil | ||||||
|  | } | ||||||
							
								
								
									
										90
									
								
								vendor/github.com/google/uuid/node.go
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										90
									
								
								vendor/github.com/google/uuid/node.go
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							| @ -0,0 +1,90 @@ | |||||||
|  | // Copyright 2016 Google Inc.  All rights reserved. | ||||||
|  | // Use of this source code is governed by a BSD-style | ||||||
|  | // license that can be found in the LICENSE file. | ||||||
|  | 
 | ||||||
|  | package uuid | ||||||
|  | 
 | ||||||
|  | import ( | ||||||
|  | 	"sync" | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | var ( | ||||||
|  | 	nodeMu sync.Mutex | ||||||
|  | 	ifname string  // name of interface being used | ||||||
|  | 	nodeID [6]byte // hardware for version 1 UUIDs | ||||||
|  | 	zeroID [6]byte // nodeID with only 0's | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | // NodeInterface returns the name of the interface from which the NodeID was | ||||||
|  | // derived.  The interface "user" is returned if the NodeID was set by | ||||||
|  | // SetNodeID. | ||||||
|  | func NodeInterface() string { | ||||||
|  | 	defer nodeMu.Unlock() | ||||||
|  | 	nodeMu.Lock() | ||||||
|  | 	return ifname | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // SetNodeInterface selects the hardware address to be used for Version 1 UUIDs. | ||||||
|  | // If name is "" then the first usable interface found will be used or a random | ||||||
|  | // Node ID will be generated.  If a named interface cannot be found then false | ||||||
|  | // is returned. | ||||||
|  | // | ||||||
|  | // SetNodeInterface never fails when name is "". | ||||||
|  | func SetNodeInterface(name string) bool { | ||||||
|  | 	defer nodeMu.Unlock() | ||||||
|  | 	nodeMu.Lock() | ||||||
|  | 	return setNodeInterface(name) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func setNodeInterface(name string) bool { | ||||||
|  | 	iname, addr := getHardwareInterface(name) // null implementation for js | ||||||
|  | 	if iname != "" && addr != nil { | ||||||
|  | 		ifname = iname | ||||||
|  | 		copy(nodeID[:], addr) | ||||||
|  | 		return true | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	// We found no interfaces with a valid hardware address.  If name | ||||||
|  | 	// does not specify a specific interface generate a random Node ID | ||||||
|  | 	// (section 4.1.6) | ||||||
|  | 	if name == "" { | ||||||
|  | 		ifname = "random" | ||||||
|  | 		randomBits(nodeID[:]) | ||||||
|  | 		return true | ||||||
|  | 	} | ||||||
|  | 	return false | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // NodeID returns a slice of a copy of the current Node ID, setting the Node ID | ||||||
|  | // if not already set. | ||||||
|  | func NodeID() []byte { | ||||||
|  | 	defer nodeMu.Unlock() | ||||||
|  | 	nodeMu.Lock() | ||||||
|  | 	if nodeID == zeroID { | ||||||
|  | 		setNodeInterface("") | ||||||
|  | 	} | ||||||
|  | 	nid := nodeID | ||||||
|  | 	return nid[:] | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // SetNodeID sets the Node ID to be used for Version 1 UUIDs.  The first 6 bytes | ||||||
|  | // of id are used.  If id is less than 6 bytes then false is returned and the | ||||||
|  | // Node ID is not set. | ||||||
|  | func SetNodeID(id []byte) bool { | ||||||
|  | 	if len(id) < 6 { | ||||||
|  | 		return false | ||||||
|  | 	} | ||||||
|  | 	defer nodeMu.Unlock() | ||||||
|  | 	nodeMu.Lock() | ||||||
|  | 	copy(nodeID[:], id) | ||||||
|  | 	ifname = "user" | ||||||
|  | 	return true | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // NodeID returns the 6 byte node id encoded in uuid.  It returns nil if uuid is | ||||||
|  | // not valid.  The NodeID is only well defined for version 1 and 2 UUIDs. | ||||||
|  | func (uuid UUID) NodeID() []byte { | ||||||
|  | 	var node [6]byte | ||||||
|  | 	copy(node[:], uuid[10:]) | ||||||
|  | 	return node[:] | ||||||
|  | } | ||||||
							
								
								
									
										12
									
								
								vendor/github.com/google/uuid/node_js.go
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								vendor/github.com/google/uuid/node_js.go
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							| @ -0,0 +1,12 @@ | |||||||
|  | // Copyright 2017 Google Inc.  All rights reserved. | ||||||
|  | // Use of this source code is governed by a BSD-style | ||||||
|  | // license that can be found in the LICENSE file. | ||||||
|  | 
 | ||||||
|  | // +build js | ||||||
|  | 
 | ||||||
|  | package uuid | ||||||
|  | 
 | ||||||
|  | // getHardwareInterface returns nil values for the JS version of the code. | ||||||
|  | // This remvoves the "net" dependency, because it is not used in the browser. | ||||||
|  | // Using the "net" library inflates the size of the transpiled JS code by 673k bytes. | ||||||
|  | func getHardwareInterface(name string) (string, []byte) { return "", nil } | ||||||
							
								
								
									
										33
									
								
								vendor/github.com/google/uuid/node_net.go
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										33
									
								
								vendor/github.com/google/uuid/node_net.go
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							| @ -0,0 +1,33 @@ | |||||||
|  | // Copyright 2017 Google Inc.  All rights reserved. | ||||||
|  | // Use of this source code is governed by a BSD-style | ||||||
|  | // license that can be found in the LICENSE file. | ||||||
|  | 
 | ||||||
|  | // +build !js | ||||||
|  | 
 | ||||||
|  | package uuid | ||||||
|  | 
 | ||||||
|  | import "net" | ||||||
|  | 
 | ||||||
|  | var interfaces []net.Interface // cached list of interfaces | ||||||
|  | 
 | ||||||
|  | // getHardwareInterface returns the name and hardware address of interface name. | ||||||
|  | // If name is "" then the name and hardware address of one of the system's | ||||||
|  | // interfaces is returned.  If no interfaces are found (name does not exist or | ||||||
|  | // there are no interfaces) then "", nil is returned. | ||||||
|  | // | ||||||
|  | // Only addresses of at least 6 bytes are returned. | ||||||
|  | func getHardwareInterface(name string) (string, []byte) { | ||||||
|  | 	if interfaces == nil { | ||||||
|  | 		var err error | ||||||
|  | 		interfaces, err = net.Interfaces() | ||||||
|  | 		if err != nil { | ||||||
|  | 			return "", nil | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 	for _, ifs := range interfaces { | ||||||
|  | 		if len(ifs.HardwareAddr) >= 6 && (name == "" || name == ifs.Name) { | ||||||
|  | 			return ifs.Name, ifs.HardwareAddr | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 	return "", nil | ||||||
|  | } | ||||||
							
								
								
									
										59
									
								
								vendor/github.com/google/uuid/sql.go
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										59
									
								
								vendor/github.com/google/uuid/sql.go
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							| @ -0,0 +1,59 @@ | |||||||
|  | // Copyright 2016 Google Inc.  All rights reserved. | ||||||
|  | // Use of this source code is governed by a BSD-style | ||||||
|  | // license that can be found in the LICENSE file. | ||||||
|  | 
 | ||||||
|  | package uuid | ||||||
|  | 
 | ||||||
|  | import ( | ||||||
|  | 	"database/sql/driver" | ||||||
|  | 	"fmt" | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | // Scan implements sql.Scanner so UUIDs can be read from databases transparently | ||||||
|  | // Currently, database types that map to string and []byte are supported. Please | ||||||
|  | // consult database-specific driver documentation for matching types. | ||||||
|  | func (uuid *UUID) Scan(src interface{}) error { | ||||||
|  | 	switch src := src.(type) { | ||||||
|  | 	case nil: | ||||||
|  | 		return nil | ||||||
|  | 
 | ||||||
|  | 	case string: | ||||||
|  | 		// if an empty UUID comes from a table, we return a null UUID | ||||||
|  | 		if src == "" { | ||||||
|  | 			return nil | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		// see Parse for required string format | ||||||
|  | 		u, err := Parse(src) | ||||||
|  | 		if err != nil { | ||||||
|  | 			return fmt.Errorf("Scan: %v", err) | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		*uuid = u | ||||||
|  | 
 | ||||||
|  | 	case []byte: | ||||||
|  | 		// if an empty UUID comes from a table, we return a null UUID | ||||||
|  | 		if len(src) == 0 { | ||||||
|  | 			return nil | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		// assumes a simple slice of bytes if 16 bytes | ||||||
|  | 		// otherwise attempts to parse | ||||||
|  | 		if len(src) != 16 { | ||||||
|  | 			return uuid.Scan(string(src)) | ||||||
|  | 		} | ||||||
|  | 		copy((*uuid)[:], src) | ||||||
|  | 
 | ||||||
|  | 	default: | ||||||
|  | 		return fmt.Errorf("Scan: unable to scan type %T into UUID", src) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	return nil | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // Value implements sql.Valuer so that UUIDs can be written to databases | ||||||
|  | // transparently. Currently, UUIDs map to strings. Please consult | ||||||
|  | // database-specific driver documentation for matching types. | ||||||
|  | func (uuid UUID) Value() (driver.Value, error) { | ||||||
|  | 	return uuid.String(), nil | ||||||
|  | } | ||||||
							
								
								
									
										123
									
								
								vendor/github.com/google/uuid/time.go
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										123
									
								
								vendor/github.com/google/uuid/time.go
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							| @ -0,0 +1,123 @@ | |||||||
|  | // Copyright 2016 Google Inc.  All rights reserved. | ||||||
|  | // Use of this source code is governed by a BSD-style | ||||||
|  | // license that can be found in the LICENSE file. | ||||||
|  | 
 | ||||||
|  | package uuid | ||||||
|  | 
 | ||||||
|  | import ( | ||||||
|  | 	"encoding/binary" | ||||||
|  | 	"sync" | ||||||
|  | 	"time" | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | // A Time represents a time as the number of 100's of nanoseconds since 15 Oct | ||||||
|  | // 1582. | ||||||
|  | type Time int64 | ||||||
|  | 
 | ||||||
|  | const ( | ||||||
|  | 	lillian    = 2299160          // Julian day of 15 Oct 1582 | ||||||
|  | 	unix       = 2440587          // Julian day of 1 Jan 1970 | ||||||
|  | 	epoch      = unix - lillian   // Days between epochs | ||||||
|  | 	g1582      = epoch * 86400    // seconds between epochs | ||||||
|  | 	g1582ns100 = g1582 * 10000000 // 100s of a nanoseconds between epochs | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | var ( | ||||||
|  | 	timeMu   sync.Mutex | ||||||
|  | 	lasttime uint64 // last time we returned | ||||||
|  | 	clockSeq uint16 // clock sequence for this run | ||||||
|  | 
 | ||||||
|  | 	timeNow = time.Now // for testing | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | // UnixTime converts t the number of seconds and nanoseconds using the Unix | ||||||
|  | // epoch of 1 Jan 1970. | ||||||
|  | func (t Time) UnixTime() (sec, nsec int64) { | ||||||
|  | 	sec = int64(t - g1582ns100) | ||||||
|  | 	nsec = (sec % 10000000) * 100 | ||||||
|  | 	sec /= 10000000 | ||||||
|  | 	return sec, nsec | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // GetTime returns the current Time (100s of nanoseconds since 15 Oct 1582) and | ||||||
|  | // clock sequence as well as adjusting the clock sequence as needed.  An error | ||||||
|  | // is returned if the current time cannot be determined. | ||||||
|  | func GetTime() (Time, uint16, error) { | ||||||
|  | 	defer timeMu.Unlock() | ||||||
|  | 	timeMu.Lock() | ||||||
|  | 	return getTime() | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func getTime() (Time, uint16, error) { | ||||||
|  | 	t := timeNow() | ||||||
|  | 
 | ||||||
|  | 	// If we don't have a clock sequence already, set one. | ||||||
|  | 	if clockSeq == 0 { | ||||||
|  | 		setClockSequence(-1) | ||||||
|  | 	} | ||||||
|  | 	now := uint64(t.UnixNano()/100) + g1582ns100 | ||||||
|  | 
 | ||||||
|  | 	// If time has gone backwards with this clock sequence then we | ||||||
|  | 	// increment the clock sequence | ||||||
|  | 	if now <= lasttime { | ||||||
|  | 		clockSeq = ((clockSeq + 1) & 0x3fff) | 0x8000 | ||||||
|  | 	} | ||||||
|  | 	lasttime = now | ||||||
|  | 	return Time(now), clockSeq, nil | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // ClockSequence returns the current clock sequence, generating one if not | ||||||
|  | // already set.  The clock sequence is only used for Version 1 UUIDs. | ||||||
|  | // | ||||||
|  | // The uuid package does not use global static storage for the clock sequence or | ||||||
|  | // the last time a UUID was generated.  Unless SetClockSequence is used, a new | ||||||
|  | // random clock sequence is generated the first time a clock sequence is | ||||||
|  | // requested by ClockSequence, GetTime, or NewUUID.  (section 4.2.1.1) | ||||||
|  | func ClockSequence() int { | ||||||
|  | 	defer timeMu.Unlock() | ||||||
|  | 	timeMu.Lock() | ||||||
|  | 	return clockSequence() | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func clockSequence() int { | ||||||
|  | 	if clockSeq == 0 { | ||||||
|  | 		setClockSequence(-1) | ||||||
|  | 	} | ||||||
|  | 	return int(clockSeq & 0x3fff) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // SetClockSequence sets the clock sequence to the lower 14 bits of seq.  Setting to | ||||||
|  | // -1 causes a new sequence to be generated. | ||||||
|  | func SetClockSequence(seq int) { | ||||||
|  | 	defer timeMu.Unlock() | ||||||
|  | 	timeMu.Lock() | ||||||
|  | 	setClockSequence(seq) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func setClockSequence(seq int) { | ||||||
|  | 	if seq == -1 { | ||||||
|  | 		var b [2]byte | ||||||
|  | 		randomBits(b[:]) // clock sequence | ||||||
|  | 		seq = int(b[0])<<8 | int(b[1]) | ||||||
|  | 	} | ||||||
|  | 	oldSeq := clockSeq | ||||||
|  | 	clockSeq = uint16(seq&0x3fff) | 0x8000 // Set our variant | ||||||
|  | 	if oldSeq != clockSeq { | ||||||
|  | 		lasttime = 0 | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // Time returns the time in 100s of nanoseconds since 15 Oct 1582 encoded in | ||||||
|  | // uuid.  The time is only defined for version 1 and 2 UUIDs. | ||||||
|  | func (uuid UUID) Time() Time { | ||||||
|  | 	time := int64(binary.BigEndian.Uint32(uuid[0:4])) | ||||||
|  | 	time |= int64(binary.BigEndian.Uint16(uuid[4:6])) << 32 | ||||||
|  | 	time |= int64(binary.BigEndian.Uint16(uuid[6:8])&0xfff) << 48 | ||||||
|  | 	return Time(time) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // ClockSequence returns the clock sequence encoded in uuid. | ||||||
|  | // The clock sequence is only well defined for version 1 and 2 UUIDs. | ||||||
|  | func (uuid UUID) ClockSequence() int { | ||||||
|  | 	return int(binary.BigEndian.Uint16(uuid[8:10])) & 0x3fff | ||||||
|  | } | ||||||
							
								
								
									
										43
									
								
								vendor/github.com/google/uuid/util.go
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										43
									
								
								vendor/github.com/google/uuid/util.go
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							| @ -0,0 +1,43 @@ | |||||||
|  | // Copyright 2016 Google Inc.  All rights reserved. | ||||||
|  | // Use of this source code is governed by a BSD-style | ||||||
|  | // license that can be found in the LICENSE file. | ||||||
|  | 
 | ||||||
|  | package uuid | ||||||
|  | 
 | ||||||
|  | import ( | ||||||
|  | 	"io" | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | // randomBits completely fills slice b with random data. | ||||||
|  | func randomBits(b []byte) { | ||||||
|  | 	if _, err := io.ReadFull(rander, b); err != nil { | ||||||
|  | 		panic(err.Error()) // rand should never fail | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // xvalues returns the value of a byte as a hexadecimal digit or 255. | ||||||
|  | var xvalues = [256]byte{ | ||||||
|  | 	255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, | ||||||
|  | 	255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, | ||||||
|  | 	255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, | ||||||
|  | 	0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 255, 255, 255, 255, 255, 255, | ||||||
|  | 	255, 10, 11, 12, 13, 14, 15, 255, 255, 255, 255, 255, 255, 255, 255, 255, | ||||||
|  | 	255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, | ||||||
|  | 	255, 10, 11, 12, 13, 14, 15, 255, 255, 255, 255, 255, 255, 255, 255, 255, | ||||||
|  | 	255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, | ||||||
|  | 	255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, | ||||||
|  | 	255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, | ||||||
|  | 	255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, | ||||||
|  | 	255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, | ||||||
|  | 	255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, | ||||||
|  | 	255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, | ||||||
|  | 	255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, | ||||||
|  | 	255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // xtob converts hex characters x1 and x2 into a byte. | ||||||
|  | func xtob(x1, x2 byte) (byte, bool) { | ||||||
|  | 	b1 := xvalues[x1] | ||||||
|  | 	b2 := xvalues[x2] | ||||||
|  | 	return (b1 << 4) | b2, b1 != 255 && b2 != 255 | ||||||
|  | } | ||||||
							
								
								
									
										245
									
								
								vendor/github.com/google/uuid/uuid.go
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										245
									
								
								vendor/github.com/google/uuid/uuid.go
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							| @ -0,0 +1,245 @@ | |||||||
|  | // Copyright 2018 Google Inc.  All rights reserved. | ||||||
|  | // Use of this source code is governed by a BSD-style | ||||||
|  | // license that can be found in the LICENSE file. | ||||||
|  | 
 | ||||||
|  | package uuid | ||||||
|  | 
 | ||||||
|  | import ( | ||||||
|  | 	"bytes" | ||||||
|  | 	"crypto/rand" | ||||||
|  | 	"encoding/hex" | ||||||
|  | 	"errors" | ||||||
|  | 	"fmt" | ||||||
|  | 	"io" | ||||||
|  | 	"strings" | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | // A UUID is a 128 bit (16 byte) Universal Unique IDentifier as defined in RFC | ||||||
|  | // 4122. | ||||||
|  | type UUID [16]byte | ||||||
|  | 
 | ||||||
|  | // A Version represents a UUID's version. | ||||||
|  | type Version byte | ||||||
|  | 
 | ||||||
|  | // A Variant represents a UUID's variant. | ||||||
|  | type Variant byte | ||||||
|  | 
 | ||||||
|  | // Constants returned by Variant. | ||||||
|  | const ( | ||||||
|  | 	Invalid   = Variant(iota) // Invalid UUID | ||||||
|  | 	RFC4122                   // The variant specified in RFC4122 | ||||||
|  | 	Reserved                  // Reserved, NCS backward compatibility. | ||||||
|  | 	Microsoft                 // Reserved, Microsoft Corporation backward compatibility. | ||||||
|  | 	Future                    // Reserved for future definition. | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | var rander = rand.Reader // random function | ||||||
|  | 
 | ||||||
|  | // Parse decodes s into a UUID or returns an error.  Both the standard UUID | ||||||
|  | // forms of xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx and | ||||||
|  | // urn:uuid:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx are decoded as well as the | ||||||
|  | // Microsoft encoding {xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx} and the raw hex | ||||||
|  | // encoding: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx. | ||||||
|  | func Parse(s string) (UUID, error) { | ||||||
|  | 	var uuid UUID | ||||||
|  | 	switch len(s) { | ||||||
|  | 	// xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx | ||||||
|  | 	case 36: | ||||||
|  | 
 | ||||||
|  | 	// urn:uuid:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx | ||||||
|  | 	case 36 + 9: | ||||||
|  | 		if strings.ToLower(s[:9]) != "urn:uuid:" { | ||||||
|  | 			return uuid, fmt.Errorf("invalid urn prefix: %q", s[:9]) | ||||||
|  | 		} | ||||||
|  | 		s = s[9:] | ||||||
|  | 
 | ||||||
|  | 	// {xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx} | ||||||
|  | 	case 36 + 2: | ||||||
|  | 		s = s[1:] | ||||||
|  | 
 | ||||||
|  | 	// xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx | ||||||
|  | 	case 32: | ||||||
|  | 		var ok bool | ||||||
|  | 		for i := range uuid { | ||||||
|  | 			uuid[i], ok = xtob(s[i*2], s[i*2+1]) | ||||||
|  | 			if !ok { | ||||||
|  | 				return uuid, errors.New("invalid UUID format") | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 		return uuid, nil | ||||||
|  | 	default: | ||||||
|  | 		return uuid, fmt.Errorf("invalid UUID length: %d", len(s)) | ||||||
|  | 	} | ||||||
|  | 	// s is now at least 36 bytes long | ||||||
|  | 	// it must be of the form  xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx | ||||||
|  | 	if s[8] != '-' || s[13] != '-' || s[18] != '-' || s[23] != '-' { | ||||||
|  | 		return uuid, errors.New("invalid UUID format") | ||||||
|  | 	} | ||||||
|  | 	for i, x := range [16]int{ | ||||||
|  | 		0, 2, 4, 6, | ||||||
|  | 		9, 11, | ||||||
|  | 		14, 16, | ||||||
|  | 		19, 21, | ||||||
|  | 		24, 26, 28, 30, 32, 34} { | ||||||
|  | 		v, ok := xtob(s[x], s[x+1]) | ||||||
|  | 		if !ok { | ||||||
|  | 			return uuid, errors.New("invalid UUID format") | ||||||
|  | 		} | ||||||
|  | 		uuid[i] = v | ||||||
|  | 	} | ||||||
|  | 	return uuid, nil | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // ParseBytes is like Parse, except it parses a byte slice instead of a string. | ||||||
|  | func ParseBytes(b []byte) (UUID, error) { | ||||||
|  | 	var uuid UUID | ||||||
|  | 	switch len(b) { | ||||||
|  | 	case 36: // xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx | ||||||
|  | 	case 36 + 9: // urn:uuid:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx | ||||||
|  | 		if !bytes.Equal(bytes.ToLower(b[:9]), []byte("urn:uuid:")) { | ||||||
|  | 			return uuid, fmt.Errorf("invalid urn prefix: %q", b[:9]) | ||||||
|  | 		} | ||||||
|  | 		b = b[9:] | ||||||
|  | 	case 36 + 2: // {xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx} | ||||||
|  | 		b = b[1:] | ||||||
|  | 	case 32: // xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx | ||||||
|  | 		var ok bool | ||||||
|  | 		for i := 0; i < 32; i += 2 { | ||||||
|  | 			uuid[i/2], ok = xtob(b[i], b[i+1]) | ||||||
|  | 			if !ok { | ||||||
|  | 				return uuid, errors.New("invalid UUID format") | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 		return uuid, nil | ||||||
|  | 	default: | ||||||
|  | 		return uuid, fmt.Errorf("invalid UUID length: %d", len(b)) | ||||||
|  | 	} | ||||||
|  | 	// s is now at least 36 bytes long | ||||||
|  | 	// it must be of the form  xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx | ||||||
|  | 	if b[8] != '-' || b[13] != '-' || b[18] != '-' || b[23] != '-' { | ||||||
|  | 		return uuid, errors.New("invalid UUID format") | ||||||
|  | 	} | ||||||
|  | 	for i, x := range [16]int{ | ||||||
|  | 		0, 2, 4, 6, | ||||||
|  | 		9, 11, | ||||||
|  | 		14, 16, | ||||||
|  | 		19, 21, | ||||||
|  | 		24, 26, 28, 30, 32, 34} { | ||||||
|  | 		v, ok := xtob(b[x], b[x+1]) | ||||||
|  | 		if !ok { | ||||||
|  | 			return uuid, errors.New("invalid UUID format") | ||||||
|  | 		} | ||||||
|  | 		uuid[i] = v | ||||||
|  | 	} | ||||||
|  | 	return uuid, nil | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // MustParse is like Parse but panics if the string cannot be parsed. | ||||||
|  | // It simplifies safe initialization of global variables holding compiled UUIDs. | ||||||
|  | func MustParse(s string) UUID { | ||||||
|  | 	uuid, err := Parse(s) | ||||||
|  | 	if err != nil { | ||||||
|  | 		panic(`uuid: Parse(` + s + `): ` + err.Error()) | ||||||
|  | 	} | ||||||
|  | 	return uuid | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // FromBytes creates a new UUID from a byte slice. Returns an error if the slice | ||||||
|  | // does not have a length of 16. The bytes are copied from the slice. | ||||||
|  | func FromBytes(b []byte) (uuid UUID, err error) { | ||||||
|  | 	err = uuid.UnmarshalBinary(b) | ||||||
|  | 	return uuid, err | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // Must returns uuid if err is nil and panics otherwise. | ||||||
|  | func Must(uuid UUID, err error) UUID { | ||||||
|  | 	if err != nil { | ||||||
|  | 		panic(err) | ||||||
|  | 	} | ||||||
|  | 	return uuid | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // String returns the string form of uuid, xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx | ||||||
|  | // , or "" if uuid is invalid. | ||||||
|  | func (uuid UUID) String() string { | ||||||
|  | 	var buf [36]byte | ||||||
|  | 	encodeHex(buf[:], uuid) | ||||||
|  | 	return string(buf[:]) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // URN returns the RFC 2141 URN form of uuid, | ||||||
|  | // urn:uuid:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx,  or "" if uuid is invalid. | ||||||
|  | func (uuid UUID) URN() string { | ||||||
|  | 	var buf [36 + 9]byte | ||||||
|  | 	copy(buf[:], "urn:uuid:") | ||||||
|  | 	encodeHex(buf[9:], uuid) | ||||||
|  | 	return string(buf[:]) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func encodeHex(dst []byte, uuid UUID) { | ||||||
|  | 	hex.Encode(dst, uuid[:4]) | ||||||
|  | 	dst[8] = '-' | ||||||
|  | 	hex.Encode(dst[9:13], uuid[4:6]) | ||||||
|  | 	dst[13] = '-' | ||||||
|  | 	hex.Encode(dst[14:18], uuid[6:8]) | ||||||
|  | 	dst[18] = '-' | ||||||
|  | 	hex.Encode(dst[19:23], uuid[8:10]) | ||||||
|  | 	dst[23] = '-' | ||||||
|  | 	hex.Encode(dst[24:], uuid[10:]) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // Variant returns the variant encoded in uuid. | ||||||
|  | func (uuid UUID) Variant() Variant { | ||||||
|  | 	switch { | ||||||
|  | 	case (uuid[8] & 0xc0) == 0x80: | ||||||
|  | 		return RFC4122 | ||||||
|  | 	case (uuid[8] & 0xe0) == 0xc0: | ||||||
|  | 		return Microsoft | ||||||
|  | 	case (uuid[8] & 0xe0) == 0xe0: | ||||||
|  | 		return Future | ||||||
|  | 	default: | ||||||
|  | 		return Reserved | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // Version returns the version of uuid. | ||||||
|  | func (uuid UUID) Version() Version { | ||||||
|  | 	return Version(uuid[6] >> 4) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (v Version) String() string { | ||||||
|  | 	if v > 15 { | ||||||
|  | 		return fmt.Sprintf("BAD_VERSION_%d", v) | ||||||
|  | 	} | ||||||
|  | 	return fmt.Sprintf("VERSION_%d", v) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (v Variant) String() string { | ||||||
|  | 	switch v { | ||||||
|  | 	case RFC4122: | ||||||
|  | 		return "RFC4122" | ||||||
|  | 	case Reserved: | ||||||
|  | 		return "Reserved" | ||||||
|  | 	case Microsoft: | ||||||
|  | 		return "Microsoft" | ||||||
|  | 	case Future: | ||||||
|  | 		return "Future" | ||||||
|  | 	case Invalid: | ||||||
|  | 		return "Invalid" | ||||||
|  | 	} | ||||||
|  | 	return fmt.Sprintf("BadVariant%d", int(v)) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // SetRand sets the random number generator to r, which implements io.Reader. | ||||||
|  | // If r.Read returns an error when the package requests random data then | ||||||
|  | // a panic will be issued. | ||||||
|  | // | ||||||
|  | // Calling SetRand with nil sets the random number generator to the default | ||||||
|  | // generator. | ||||||
|  | func SetRand(r io.Reader) { | ||||||
|  | 	if r == nil { | ||||||
|  | 		rander = rand.Reader | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 	rander = r | ||||||
|  | } | ||||||
							
								
								
									
										44
									
								
								vendor/github.com/google/uuid/version1.go
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										44
									
								
								vendor/github.com/google/uuid/version1.go
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							| @ -0,0 +1,44 @@ | |||||||
|  | // Copyright 2016 Google Inc.  All rights reserved. | ||||||
|  | // Use of this source code is governed by a BSD-style | ||||||
|  | // license that can be found in the LICENSE file. | ||||||
|  | 
 | ||||||
|  | package uuid | ||||||
|  | 
 | ||||||
|  | import ( | ||||||
|  | 	"encoding/binary" | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | // NewUUID returns a Version 1 UUID based on the current NodeID and clock | ||||||
|  | // sequence, and the current time.  If the NodeID has not been set by SetNodeID | ||||||
|  | // or SetNodeInterface then it will be set automatically.  If the NodeID cannot | ||||||
|  | // be set NewUUID returns nil.  If clock sequence has not been set by | ||||||
|  | // SetClockSequence then it will be set automatically.  If GetTime fails to | ||||||
|  | // return the current NewUUID returns nil and an error. | ||||||
|  | // | ||||||
|  | // In most cases, New should be used. | ||||||
|  | func NewUUID() (UUID, error) { | ||||||
|  | 	nodeMu.Lock() | ||||||
|  | 	if nodeID == zeroID { | ||||||
|  | 		setNodeInterface("") | ||||||
|  | 	} | ||||||
|  | 	nodeMu.Unlock() | ||||||
|  | 
 | ||||||
|  | 	var uuid UUID | ||||||
|  | 	now, seq, err := GetTime() | ||||||
|  | 	if err != nil { | ||||||
|  | 		return uuid, err | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	timeLow := uint32(now & 0xffffffff) | ||||||
|  | 	timeMid := uint16((now >> 32) & 0xffff) | ||||||
|  | 	timeHi := uint16((now >> 48) & 0x0fff) | ||||||
|  | 	timeHi |= 0x1000 // Version 1 | ||||||
|  | 
 | ||||||
|  | 	binary.BigEndian.PutUint32(uuid[0:], timeLow) | ||||||
|  | 	binary.BigEndian.PutUint16(uuid[4:], timeMid) | ||||||
|  | 	binary.BigEndian.PutUint16(uuid[6:], timeHi) | ||||||
|  | 	binary.BigEndian.PutUint16(uuid[8:], seq) | ||||||
|  | 	copy(uuid[10:], nodeID[:]) | ||||||
|  | 
 | ||||||
|  | 	return uuid, nil | ||||||
|  | } | ||||||
							
								
								
									
										38
									
								
								vendor/github.com/google/uuid/version4.go
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										38
									
								
								vendor/github.com/google/uuid/version4.go
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							| @ -0,0 +1,38 @@ | |||||||
|  | // Copyright 2016 Google Inc.  All rights reserved. | ||||||
|  | // Use of this source code is governed by a BSD-style | ||||||
|  | // license that can be found in the LICENSE file. | ||||||
|  | 
 | ||||||
|  | package uuid | ||||||
|  | 
 | ||||||
|  | import "io" | ||||||
|  | 
 | ||||||
|  | // New creates a new random UUID or panics.  New is equivalent to | ||||||
|  | // the expression | ||||||
|  | // | ||||||
|  | //    uuid.Must(uuid.NewRandom()) | ||||||
|  | func New() UUID { | ||||||
|  | 	return Must(NewRandom()) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // NewRandom returns a Random (Version 4) UUID. | ||||||
|  | // | ||||||
|  | // The strength of the UUIDs is based on the strength of the crypto/rand | ||||||
|  | // package. | ||||||
|  | // | ||||||
|  | // A note about uniqueness derived from the UUID Wikipedia entry: | ||||||
|  | // | ||||||
|  | //  Randomly generated UUIDs have 122 random bits.  One's annual risk of being | ||||||
|  | //  hit by a meteorite is estimated to be one chance in 17 billion, that | ||||||
|  | //  means the probability is about 0.00000000006 (6 × 10−11), | ||||||
|  | //  equivalent to the odds of creating a few tens of trillions of UUIDs in a | ||||||
|  | //  year and having one duplicate. | ||||||
|  | func NewRandom() (UUID, error) { | ||||||
|  | 	var uuid UUID | ||||||
|  | 	_, err := io.ReadFull(rander, uuid[:]) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return Nil, err | ||||||
|  | 	} | ||||||
|  | 	uuid[6] = (uuid[6] & 0x0f) | 0x40 // Version 4 | ||||||
|  | 	uuid[8] = (uuid[8] & 0x3f) | 0x80 // Variant is 10 | ||||||
|  | 	return uuid, nil | ||||||
|  | } | ||||||
							
								
								
									
										1
									
								
								vendor/github.com/joho/godotenv/.gitignore
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								vendor/github.com/joho/godotenv/.gitignore
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							| @ -0,0 +1 @@ | |||||||
|  | .DS_Store | ||||||
							
								
								
									
										8
									
								
								vendor/github.com/joho/godotenv/.travis.yml
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										8
									
								
								vendor/github.com/joho/godotenv/.travis.yml
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							| @ -0,0 +1,8 @@ | |||||||
|  | language: go | ||||||
|  | 
 | ||||||
|  | go: | ||||||
|  |   - 1.x | ||||||
|  | 
 | ||||||
|  | os: | ||||||
|  |   - linux | ||||||
|  |   - osx | ||||||
							
								
								
									
										23
									
								
								vendor/github.com/joho/godotenv/LICENCE
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								vendor/github.com/joho/godotenv/LICENCE
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							| @ -0,0 +1,23 @@ | |||||||
|  | Copyright (c) 2013 John Barton | ||||||
|  | 
 | ||||||
|  | MIT License | ||||||
|  | 
 | ||||||
|  | Permission is hereby granted, free of charge, to any person obtaining | ||||||
|  | a copy of this software and associated documentation files (the | ||||||
|  | "Software"), to deal in the Software without restriction, including | ||||||
|  | without limitation the rights to use, copy, modify, merge, publish, | ||||||
|  | distribute, sublicense, and/or sell copies of the Software, and to | ||||||
|  | permit persons to whom the Software is furnished to do so, subject to | ||||||
|  | the following conditions: | ||||||
|  | 
 | ||||||
|  | The above copyright notice and this permission notice shall be | ||||||
|  | included in all copies or substantial portions of the Software. | ||||||
|  | 
 | ||||||
|  | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, | ||||||
|  | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF | ||||||
|  | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND | ||||||
|  | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE | ||||||
|  | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION | ||||||
|  | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION | ||||||
|  | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. | ||||||
|  | 
 | ||||||
							
								
								
									
										163
									
								
								vendor/github.com/joho/godotenv/README.md
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										163
									
								
								vendor/github.com/joho/godotenv/README.md
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							| @ -0,0 +1,163 @@ | |||||||
|  | # GoDotEnv [](https://travis-ci.org/joho/godotenv) [](https://ci.appveyor.com/project/joho/godotenv) [](https://goreportcard.com/report/github.com/joho/godotenv) | ||||||
|  | 
 | ||||||
|  | A Go (golang) port of the Ruby dotenv project (which loads env vars from a .env file) | ||||||
|  | 
 | ||||||
|  | From the original Library: | ||||||
|  | 
 | ||||||
|  | > Storing configuration in the environment is one of the tenets of a twelve-factor app. Anything that is likely to change between deployment environments–such as resource handles for databases or credentials for external services–should be extracted from the code into environment variables. | ||||||
|  | > | ||||||
|  | > But it is not always practical to set environment variables on development machines or continuous integration servers where multiple projects are run. Dotenv load variables from a .env file into ENV when the environment is bootstrapped. | ||||||
|  | 
 | ||||||
|  | It can be used as a library (for loading in env for your own daemons etc) or as a bin command. | ||||||
|  | 
 | ||||||
|  | There is test coverage and CI for both linuxish and windows environments, but I make no guarantees about the bin version working on windows. | ||||||
|  | 
 | ||||||
|  | ## Installation | ||||||
|  | 
 | ||||||
|  | As a library | ||||||
|  | 
 | ||||||
|  | ```shell | ||||||
|  | go get github.com/joho/godotenv | ||||||
|  | ``` | ||||||
|  | 
 | ||||||
|  | or if you want to use it as a bin command | ||||||
|  | ```shell | ||||||
|  | go get github.com/joho/godotenv/cmd/godotenv | ||||||
|  | ``` | ||||||
|  | 
 | ||||||
|  | ## Usage | ||||||
|  | 
 | ||||||
|  | Add your application configuration to your `.env` file in the root of your project: | ||||||
|  | 
 | ||||||
|  | ```shell | ||||||
|  | S3_BUCKET=YOURS3BUCKET | ||||||
|  | SECRET_KEY=YOURSECRETKEYGOESHERE | ||||||
|  | ``` | ||||||
|  | 
 | ||||||
|  | Then in your Go app you can do something like | ||||||
|  | 
 | ||||||
|  | ```go | ||||||
|  | package main | ||||||
|  | 
 | ||||||
|  | import ( | ||||||
|  |     "github.com/joho/godotenv" | ||||||
|  |     "log" | ||||||
|  |     "os" | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | func main() { | ||||||
|  |   err := godotenv.Load() | ||||||
|  |   if err != nil { | ||||||
|  |     log.Fatal("Error loading .env file") | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   s3Bucket := os.Getenv("S3_BUCKET") | ||||||
|  |   secretKey := os.Getenv("SECRET_KEY") | ||||||
|  | 
 | ||||||
|  |   // now do something with s3 or whatever | ||||||
|  | } | ||||||
|  | ``` | ||||||
|  | 
 | ||||||
|  | If you're even lazier than that, you can just take advantage of the autoload package which will read in `.env` on import | ||||||
|  | 
 | ||||||
|  | ```go | ||||||
|  | import _ "github.com/joho/godotenv/autoload" | ||||||
|  | ``` | ||||||
|  | 
 | ||||||
|  | While `.env` in the project root is the default, you don't have to be constrained, both examples below are 100% legit | ||||||
|  | 
 | ||||||
|  | ```go | ||||||
|  | _ = godotenv.Load("somerandomfile") | ||||||
|  | _ = godotenv.Load("filenumberone.env", "filenumbertwo.env") | ||||||
|  | ``` | ||||||
|  | 
 | ||||||
|  | If you want to be really fancy with your env file you can do comments and exports (below is a valid env file) | ||||||
|  | 
 | ||||||
|  | ```shell | ||||||
|  | # I am a comment and that is OK | ||||||
|  | SOME_VAR=someval | ||||||
|  | FOO=BAR # comments at line end are OK too | ||||||
|  | export BAR=BAZ | ||||||
|  | ``` | ||||||
|  | 
 | ||||||
|  | Or finally you can do YAML(ish) style | ||||||
|  | 
 | ||||||
|  | ```yaml | ||||||
|  | FOO: bar | ||||||
|  | BAR: baz | ||||||
|  | ``` | ||||||
|  | 
 | ||||||
|  | as a final aside, if you don't want godotenv munging your env you can just get a map back instead | ||||||
|  | 
 | ||||||
|  | ```go | ||||||
|  | var myEnv map[string]string | ||||||
|  | myEnv, err := godotenv.Read() | ||||||
|  | 
 | ||||||
|  | s3Bucket := myEnv["S3_BUCKET"] | ||||||
|  | ``` | ||||||
|  | 
 | ||||||
|  | ... or from an `io.Reader` instead of a local file | ||||||
|  | 
 | ||||||
|  | ```go | ||||||
|  | reader := getRemoteFile() | ||||||
|  | myEnv, err := godotenv.Parse(reader) | ||||||
|  | ``` | ||||||
|  | 
 | ||||||
|  | ... or from a `string` if you so desire | ||||||
|  | 
 | ||||||
|  | ```go | ||||||
|  | content := getRemoteFileContent() | ||||||
|  | myEnv, err := godotenv.Unmarshal(content) | ||||||
|  | ``` | ||||||
|  | 
 | ||||||
|  | ### Command Mode | ||||||
|  | 
 | ||||||
|  | Assuming you've installed the command as above and you've got `$GOPATH/bin` in your `$PATH` | ||||||
|  | 
 | ||||||
|  | ``` | ||||||
|  | godotenv -f /some/path/to/.env some_command with some args | ||||||
|  | ``` | ||||||
|  | 
 | ||||||
|  | If you don't specify `-f` it will fall back on the default of loading `.env` in `PWD` | ||||||
|  | 
 | ||||||
|  | ### Writing Env Files | ||||||
|  | 
 | ||||||
|  | Godotenv can also write a map representing the environment to a correctly-formatted and escaped file | ||||||
|  | 
 | ||||||
|  | ```go | ||||||
|  | env, err := godotenv.Unmarshal("KEY=value") | ||||||
|  | err := godotenv.Write(env, "./.env") | ||||||
|  | ``` | ||||||
|  | 
 | ||||||
|  | ... or to a string | ||||||
|  | 
 | ||||||
|  | ```go | ||||||
|  | env, err := godotenv.Unmarshal("KEY=value") | ||||||
|  | content, err := godotenv.Marshal(env) | ||||||
|  | ``` | ||||||
|  | 
 | ||||||
|  | ## Contributing | ||||||
|  | 
 | ||||||
|  | Contributions are most welcome! The parser itself is pretty stupidly naive and I wouldn't be surprised if it breaks with edge cases. | ||||||
|  | 
 | ||||||
|  | *code changes without tests will not be accepted* | ||||||
|  | 
 | ||||||
|  | 1. Fork it | ||||||
|  | 2. Create your feature branch (`git checkout -b my-new-feature`) | ||||||
|  | 3. Commit your changes (`git commit -am 'Added some feature'`) | ||||||
|  | 4. Push to the branch (`git push origin my-new-feature`) | ||||||
|  | 5. Create new Pull Request | ||||||
|  | 
 | ||||||
|  | ## Releases | ||||||
|  | 
 | ||||||
|  | Releases should follow [Semver](http://semver.org/) though the first couple of releases are `v1` and `v1.1`. | ||||||
|  | 
 | ||||||
|  | Use [annotated tags for all releases](https://github.com/joho/godotenv/issues/30). Example `git tag -a v1.2.1` | ||||||
|  | 
 | ||||||
|  | ## CI | ||||||
|  | 
 | ||||||
|  | Linux: [](https://travis-ci.org/joho/godotenv) Windows: [](https://ci.appveyor.com/project/joho/godotenv) | ||||||
|  | 
 | ||||||
|  | ## Who? | ||||||
|  | 
 | ||||||
|  | The original library [dotenv](https://github.com/bkeepers/dotenv) was written by [Brandon Keepers](http://opensoul.org/), and this port was done by [John Barton](https://johnbarton.co/) based off the tests/fixtures in the original library. | ||||||
							
								
								
									
										15
									
								
								vendor/github.com/joho/godotenv/autoload/autoload.go
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								vendor/github.com/joho/godotenv/autoload/autoload.go
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							| @ -0,0 +1,15 @@ | |||||||
|  | package autoload | ||||||
|  | 
 | ||||||
|  | /* | ||||||
|  | 	You can just read the .env file on import just by doing | ||||||
|  | 
 | ||||||
|  | 		import _ "github.com/joho/godotenv/autoload" | ||||||
|  | 
 | ||||||
|  | 	And bob's your mother's brother | ||||||
|  | */ | ||||||
|  | 
 | ||||||
|  | import "github.com/joho/godotenv" | ||||||
|  | 
 | ||||||
|  | func init() { | ||||||
|  | 	godotenv.Load() | ||||||
|  | } | ||||||
							
								
								
									
										346
									
								
								vendor/github.com/joho/godotenv/godotenv.go
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										346
									
								
								vendor/github.com/joho/godotenv/godotenv.go
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							| @ -0,0 +1,346 @@ | |||||||
|  | // Package godotenv is a go port of the ruby dotenv library (https://github.com/bkeepers/dotenv) | ||||||
|  | // | ||||||
|  | // Examples/readme can be found on the github page at https://github.com/joho/godotenv | ||||||
|  | // | ||||||
|  | // The TL;DR is that you make a .env file that looks something like | ||||||
|  | // | ||||||
|  | // 		SOME_ENV_VAR=somevalue | ||||||
|  | // | ||||||
|  | // and then in your go code you can call | ||||||
|  | // | ||||||
|  | // 		godotenv.Load() | ||||||
|  | // | ||||||
|  | // and all the env vars declared in .env will be available through os.Getenv("SOME_ENV_VAR") | ||||||
|  | package godotenv | ||||||
|  | 
 | ||||||
|  | import ( | ||||||
|  | 	"bufio" | ||||||
|  | 	"errors" | ||||||
|  | 	"fmt" | ||||||
|  | 	"io" | ||||||
|  | 	"os" | ||||||
|  | 	"os/exec" | ||||||
|  | 	"regexp" | ||||||
|  | 	"sort" | ||||||
|  | 	"strings" | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | const doubleQuoteSpecialChars = "\\\n\r\"!$`" | ||||||
|  | 
 | ||||||
|  | // Load will read your env file(s) and load them into ENV for this process. | ||||||
|  | // | ||||||
|  | // Call this function as close as possible to the start of your program (ideally in main) | ||||||
|  | // | ||||||
|  | // If you call Load without any args it will default to loading .env in the current path | ||||||
|  | // | ||||||
|  | // You can otherwise tell it which files to load (there can be more than one) like | ||||||
|  | // | ||||||
|  | //		godotenv.Load("fileone", "filetwo") | ||||||
|  | // | ||||||
|  | // It's important to note that it WILL NOT OVERRIDE an env variable that already exists - consider the .env file to set dev vars or sensible defaults | ||||||
|  | func Load(filenames ...string) (err error) { | ||||||
|  | 	filenames = filenamesOrDefault(filenames) | ||||||
|  | 
 | ||||||
|  | 	for _, filename := range filenames { | ||||||
|  | 		err = loadFile(filename, false) | ||||||
|  | 		if err != nil { | ||||||
|  | 			return // return early on a spazout | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 	return | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // Overload will read your env file(s) and load them into ENV for this process. | ||||||
|  | // | ||||||
|  | // Call this function as close as possible to the start of your program (ideally in main) | ||||||
|  | // | ||||||
|  | // If you call Overload without any args it will default to loading .env in the current path | ||||||
|  | // | ||||||
|  | // You can otherwise tell it which files to load (there can be more than one) like | ||||||
|  | // | ||||||
|  | //		godotenv.Overload("fileone", "filetwo") | ||||||
|  | // | ||||||
|  | // It's important to note this WILL OVERRIDE an env variable that already exists - consider the .env file to forcefilly set all vars. | ||||||
|  | func Overload(filenames ...string) (err error) { | ||||||
|  | 	filenames = filenamesOrDefault(filenames) | ||||||
|  | 
 | ||||||
|  | 	for _, filename := range filenames { | ||||||
|  | 		err = loadFile(filename, true) | ||||||
|  | 		if err != nil { | ||||||
|  | 			return // return early on a spazout | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 	return | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // Read all env (with same file loading semantics as Load) but return values as | ||||||
|  | // a map rather than automatically writing values into env | ||||||
|  | func Read(filenames ...string) (envMap map[string]string, err error) { | ||||||
|  | 	filenames = filenamesOrDefault(filenames) | ||||||
|  | 	envMap = make(map[string]string) | ||||||
|  | 
 | ||||||
|  | 	for _, filename := range filenames { | ||||||
|  | 		individualEnvMap, individualErr := readFile(filename) | ||||||
|  | 
 | ||||||
|  | 		if individualErr != nil { | ||||||
|  | 			err = individualErr | ||||||
|  | 			return // return early on a spazout | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		for key, value := range individualEnvMap { | ||||||
|  | 			envMap[key] = value | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	return | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // Parse reads an env file from io.Reader, returning a map of keys and values. | ||||||
|  | func Parse(r io.Reader) (envMap map[string]string, err error) { | ||||||
|  | 	envMap = make(map[string]string) | ||||||
|  | 
 | ||||||
|  | 	var lines []string | ||||||
|  | 	scanner := bufio.NewScanner(r) | ||||||
|  | 	for scanner.Scan() { | ||||||
|  | 		lines = append(lines, scanner.Text()) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	if err = scanner.Err(); err != nil { | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	for _, fullLine := range lines { | ||||||
|  | 		if !isIgnoredLine(fullLine) { | ||||||
|  | 			var key, value string | ||||||
|  | 			key, value, err = parseLine(fullLine, envMap) | ||||||
|  | 
 | ||||||
|  | 			if err != nil { | ||||||
|  | 				return | ||||||
|  | 			} | ||||||
|  | 			envMap[key] = value | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 	return | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | //Unmarshal reads an env file from a string, returning a map of keys and values. | ||||||
|  | func Unmarshal(str string) (envMap map[string]string, err error) { | ||||||
|  | 	return Parse(strings.NewReader(str)) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // Exec loads env vars from the specified filenames (empty map falls back to default) | ||||||
|  | // then executes the cmd specified. | ||||||
|  | // | ||||||
|  | // Simply hooks up os.Stdin/err/out to the command and calls Run() | ||||||
|  | // | ||||||
|  | // If you want more fine grained control over your command it's recommended | ||||||
|  | // that you use `Load()` or `Read()` and the `os/exec` package yourself. | ||||||
|  | func Exec(filenames []string, cmd string, cmdArgs []string) error { | ||||||
|  | 	Load(filenames...) | ||||||
|  | 
 | ||||||
|  | 	command := exec.Command(cmd, cmdArgs...) | ||||||
|  | 	command.Stdin = os.Stdin | ||||||
|  | 	command.Stdout = os.Stdout | ||||||
|  | 	command.Stderr = os.Stderr | ||||||
|  | 	return command.Run() | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // Write serializes the given environment and writes it to a file | ||||||
|  | func Write(envMap map[string]string, filename string) error { | ||||||
|  | 	content, error := Marshal(envMap) | ||||||
|  | 	if error != nil { | ||||||
|  | 		return error | ||||||
|  | 	} | ||||||
|  | 	file, error := os.Create(filename) | ||||||
|  | 	if error != nil { | ||||||
|  | 		return error | ||||||
|  | 	} | ||||||
|  | 	_, err := file.WriteString(content) | ||||||
|  | 	return err | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // Marshal outputs the given environment as a dotenv-formatted environment file. | ||||||
|  | // Each line is in the format: KEY="VALUE" where VALUE is backslash-escaped. | ||||||
|  | func Marshal(envMap map[string]string) (string, error) { | ||||||
|  | 	lines := make([]string, 0, len(envMap)) | ||||||
|  | 	for k, v := range envMap { | ||||||
|  | 		lines = append(lines, fmt.Sprintf(`%s="%s"`, k, doubleQuoteEscape(v))) | ||||||
|  | 	} | ||||||
|  | 	sort.Strings(lines) | ||||||
|  | 	return strings.Join(lines, "\n"), nil | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func filenamesOrDefault(filenames []string) []string { | ||||||
|  | 	if len(filenames) == 0 { | ||||||
|  | 		return []string{".env"} | ||||||
|  | 	} | ||||||
|  | 	return filenames | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func loadFile(filename string, overload bool) error { | ||||||
|  | 	envMap, err := readFile(filename) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	currentEnv := map[string]bool{} | ||||||
|  | 	rawEnv := os.Environ() | ||||||
|  | 	for _, rawEnvLine := range rawEnv { | ||||||
|  | 		key := strings.Split(rawEnvLine, "=")[0] | ||||||
|  | 		currentEnv[key] = true | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	for key, value := range envMap { | ||||||
|  | 		if !currentEnv[key] || overload { | ||||||
|  | 			os.Setenv(key, value) | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	return nil | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func readFile(filename string) (envMap map[string]string, err error) { | ||||||
|  | 	file, err := os.Open(filename) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 	defer file.Close() | ||||||
|  | 
 | ||||||
|  | 	return Parse(file) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func parseLine(line string, envMap map[string]string) (key string, value string, err error) { | ||||||
|  | 	if len(line) == 0 { | ||||||
|  | 		err = errors.New("zero length string") | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	// ditch the comments (but keep quoted hashes) | ||||||
|  | 	if strings.Contains(line, "#") { | ||||||
|  | 		segmentsBetweenHashes := strings.Split(line, "#") | ||||||
|  | 		quotesAreOpen := false | ||||||
|  | 		var segmentsToKeep []string | ||||||
|  | 		for _, segment := range segmentsBetweenHashes { | ||||||
|  | 			if strings.Count(segment, "\"") == 1 || strings.Count(segment, "'") == 1 { | ||||||
|  | 				if quotesAreOpen { | ||||||
|  | 					quotesAreOpen = false | ||||||
|  | 					segmentsToKeep = append(segmentsToKeep, segment) | ||||||
|  | 				} else { | ||||||
|  | 					quotesAreOpen = true | ||||||
|  | 				} | ||||||
|  | 			} | ||||||
|  | 
 | ||||||
|  | 			if len(segmentsToKeep) == 0 || quotesAreOpen { | ||||||
|  | 				segmentsToKeep = append(segmentsToKeep, segment) | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		line = strings.Join(segmentsToKeep, "#") | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	firstEquals := strings.Index(line, "=") | ||||||
|  | 	firstColon := strings.Index(line, ":") | ||||||
|  | 	splitString := strings.SplitN(line, "=", 2) | ||||||
|  | 	if firstColon != -1 && (firstColon < firstEquals || firstEquals == -1) { | ||||||
|  | 		//this is a yaml-style line | ||||||
|  | 		splitString = strings.SplitN(line, ":", 2) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	if len(splitString) != 2 { | ||||||
|  | 		err = errors.New("Can't separate key from value") | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	// Parse the key | ||||||
|  | 	key = splitString[0] | ||||||
|  | 	if strings.HasPrefix(key, "export") { | ||||||
|  | 		key = strings.TrimPrefix(key, "export") | ||||||
|  | 	} | ||||||
|  | 	key = strings.Trim(key, " ") | ||||||
|  | 
 | ||||||
|  | 	// Parse the value | ||||||
|  | 	value = parseValue(splitString[1], envMap) | ||||||
|  | 	return | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func parseValue(value string, envMap map[string]string) string { | ||||||
|  | 
 | ||||||
|  | 	// trim | ||||||
|  | 	value = strings.Trim(value, " ") | ||||||
|  | 
 | ||||||
|  | 	// check if we've got quoted values or possible escapes | ||||||
|  | 	if len(value) > 1 { | ||||||
|  | 		rs := regexp.MustCompile(`\A'(.*)'\z`) | ||||||
|  | 		singleQuotes := rs.FindStringSubmatch(value) | ||||||
|  | 
 | ||||||
|  | 		rd := regexp.MustCompile(`\A"(.*)"\z`) | ||||||
|  | 		doubleQuotes := rd.FindStringSubmatch(value) | ||||||
|  | 
 | ||||||
|  | 		if singleQuotes != nil || doubleQuotes != nil { | ||||||
|  | 			// pull the quotes off the edges | ||||||
|  | 			value = value[1 : len(value)-1] | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		if doubleQuotes != nil { | ||||||
|  | 			// expand newlines | ||||||
|  | 			escapeRegex := regexp.MustCompile(`\\.`) | ||||||
|  | 			value = escapeRegex.ReplaceAllStringFunc(value, func(match string) string { | ||||||
|  | 				c := strings.TrimPrefix(match, `\`) | ||||||
|  | 				switch c { | ||||||
|  | 				case "n": | ||||||
|  | 					return "\n" | ||||||
|  | 				case "r": | ||||||
|  | 					return "\r" | ||||||
|  | 				default: | ||||||
|  | 					return match | ||||||
|  | 				} | ||||||
|  | 			}) | ||||||
|  | 			// unescape characters | ||||||
|  | 			e := regexp.MustCompile(`\\([^$])`) | ||||||
|  | 			value = e.ReplaceAllString(value, "$1") | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		if singleQuotes == nil { | ||||||
|  | 			value = expandVariables(value, envMap) | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	return value | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func expandVariables(v string, m map[string]string) string { | ||||||
|  | 	r := regexp.MustCompile(`(\\)?(\$)(\()?\{?([A-Z0-9_]+)?\}?`) | ||||||
|  | 
 | ||||||
|  | 	return r.ReplaceAllStringFunc(v, func(s string) string { | ||||||
|  | 		submatch := r.FindStringSubmatch(s) | ||||||
|  | 
 | ||||||
|  | 		if submatch == nil { | ||||||
|  | 			return s | ||||||
|  | 		} | ||||||
|  | 		if submatch[1] == "\\" || submatch[2] == "(" { | ||||||
|  | 			return submatch[0][1:] | ||||||
|  | 		} else if submatch[4] != "" { | ||||||
|  | 			return m[submatch[4]] | ||||||
|  | 		} | ||||||
|  | 		return s | ||||||
|  | 	}) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func isIgnoredLine(line string) bool { | ||||||
|  | 	trimmedLine := strings.Trim(line, " \n\t") | ||||||
|  | 	return len(trimmedLine) == 0 || strings.HasPrefix(trimmedLine, "#") | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func doubleQuoteEscape(line string) string { | ||||||
|  | 	for _, c := range doubleQuoteSpecialChars { | ||||||
|  | 		toReplace := "\\" + string(c) | ||||||
|  | 		if c == '\n' { | ||||||
|  | 			toReplace = `\n` | ||||||
|  | 		} | ||||||
|  | 		if c == '\r' { | ||||||
|  | 			toReplace = `\r` | ||||||
|  | 		} | ||||||
|  | 		line = strings.Replace(line, string(c), toReplace, -1) | ||||||
|  | 	} | ||||||
|  | 	return line | ||||||
|  | } | ||||||
							
								
								
									
										3
									
								
								vendor/github.com/mailgun/mailgun-go/v3/.gitignore
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								vendor/github.com/mailgun/mailgun-go/v3/.gitignore
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							| @ -0,0 +1,3 @@ | |||||||
|  | .DS_Store | ||||||
|  | .idea/ | ||||||
|  | cmd/mailgun/mailgun | ||||||
							
								
								
									
										7
									
								
								vendor/github.com/mailgun/mailgun-go/v3/.travis.yml
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								vendor/github.com/mailgun/mailgun-go/v3/.travis.yml
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							| @ -0,0 +1,7 @@ | |||||||
|  | language: go | ||||||
|  | 
 | ||||||
|  | env: | ||||||
|  |   - GO111MODULE=on | ||||||
|  | 
 | ||||||
|  | go: | ||||||
|  |   - 1.11.x | ||||||
							
								
								
									
										175
									
								
								vendor/github.com/mailgun/mailgun-go/v3/CHANGELOG
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										175
									
								
								vendor/github.com/mailgun/mailgun-go/v3/CHANGELOG
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							| @ -0,0 +1,175 @@ | |||||||
|  | # Changelog | ||||||
|  | All notable changes to this project will be documented in this file. | ||||||
|  | 
 | ||||||
|  | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), | ||||||
|  | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). | ||||||
|  | 
 | ||||||
|  | ## [3.6.3] - 2019-12-03 | ||||||
|  | ### Changes | ||||||
|  | * Calls to get stats now use epoch as the time format | ||||||
|  | 
 | ||||||
|  | ## [3.6.2] - 2019-11-18 | ||||||
|  | ### Added | ||||||
|  | * Added `AddTemplateVariable()` to make adding variables to templates  | ||||||
|  |   less confusing and error prone. | ||||||
|  | 
 | ||||||
|  | ## [3.6.1] - 2019-10-24 | ||||||
|  | ### Added | ||||||
|  | * Added `VerifyWebhookSignature()` to mailgun interface | ||||||
|  | 
 | ||||||
|  | ## [3.6.1-rc.3] - 2019-07-16 | ||||||
|  | ### Added | ||||||
|  | * APIBaseEU and APIBaseUS to help customers change regions | ||||||
|  | * Documented how to change regions in the README | ||||||
|  | 
 | ||||||
|  | ## [3.6.1-rc.2] - 2019-07-01 | ||||||
|  | ### Changes | ||||||
|  | * Fix the JSON response for `GetMember()` | ||||||
|  | * Typo in format string in max number of tags error | ||||||
|  | 
 | ||||||
|  | ## [3.6.0] - 2019-06-26 | ||||||
|  | ### Added | ||||||
|  | * Added UpdateClickTracking() to modify click tracking for a domain | ||||||
|  | * Added UpdateUnsubscribeTracking() to modify unsubscribe tracking for a domain | ||||||
|  | * Added UpdateOpenTracking() to modify open tracking for a domain | ||||||
|  | 
 | ||||||
|  | ## [3.5.0] - 2019-05-21 | ||||||
|  | ### Added | ||||||
|  | * Added notice in README about go dep bug. | ||||||
|  | * Added endpoints for webhooks in mock server | ||||||
|  | ### Changes | ||||||
|  | * Change names of some parameters on public methods to make their use clearer. | ||||||
|  | * Changed signature of `GetWebhook()` now returns []string. | ||||||
|  | * Changed signature of `ListWebhooks()` now returns map[string][]string. | ||||||
|  | * Both `GetWebhooks()` and `ListWebhooks()` now handle new and legacy webhooks properly. | ||||||
|  | 
 | ||||||
|  | ## [3.4.0] - 2019-04-23 | ||||||
|  | ### Added | ||||||
|  | * Added `Message.SetTemplate()` to allow sending with the body of a template. | ||||||
|  | ### Changes | ||||||
|  | * Changed signature of `CreateDomain()` moved password into `CreateDomainOptions` | ||||||
|  | 
 | ||||||
|  | ## [3.4.0] - 2019-04-23 | ||||||
|  | ### Added | ||||||
|  | * Added `Message.SetTemplate()` to allow sending with the body of a template. | ||||||
|  | ### Changes | ||||||
|  | * Changed signature of `CreateDomain()` moved password into `CreateDomainOptions` | ||||||
|  | 
 | ||||||
|  | ## [3.3.2] - 2019-03-28 | ||||||
|  | ### Changes | ||||||
|  | * Uncommented DeliveryStatus.Code and change it to an integer (See #175) | ||||||
|  | * Added UserVariables to all Message events (See #176) | ||||||
|  | 
 | ||||||
|  | ## [3.3.1] - 2019-03-13 | ||||||
|  | ### Changes | ||||||
|  | * Updated Template calls to reflect the most recent Template API changes. | ||||||
|  | * GetStoredMessage() now accepts a URL instead of an id | ||||||
|  | * Deprecated GetStoredMessageForURL() | ||||||
|  | * Deprecated GetStoredMessageRawForURL() | ||||||
|  | * Fixed GetUnsubscribed() | ||||||
|  | 
 | ||||||
|  | ### Added | ||||||
|  | * Added `GetStoredAttachment()` | ||||||
|  | 
 | ||||||
|  | ### Removed | ||||||
|  | * Method `DeleteStoredMessage()` mailgun API no long allows this call | ||||||
|  | 
 | ||||||
|  | ## [3.3.0] - 2019-01-28 | ||||||
|  | ### Changes | ||||||
|  | * Changed signature of CreateDomain() Now returns JSON response | ||||||
|  | * Changed signature of GetDomain() Now returns a single DomainResponse | ||||||
|  | * Clarified installation notes for non golang module users | ||||||
|  | * Changed 'Public Key' to 'Public Validation Key' in readme | ||||||
|  | * Fixed issue with Next() for limit/skip based iterators | ||||||
|  | 
 | ||||||
|  | ### Added | ||||||
|  | * Added VerifyDomain() | ||||||
|  | 
 | ||||||
|  | ## [3.2.0] - 2019-01-21 | ||||||
|  | ### Changes | ||||||
|  | * Deprecated mg.VerifyWebhookRequest() | ||||||
|  | 
 | ||||||
|  | ### Added | ||||||
|  | * Added mailgun.ParseEvent() | ||||||
|  | * Added mailgun.ParseEvents() | ||||||
|  | * Added mg.VerifyWebhookSignature() | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | ## [3.1.0] - 2019-01-16 | ||||||
|  | ### Changes | ||||||
|  | * Removed context.Context from ListDomains() signature | ||||||
|  | * ListEventOptions.Begin and End are no longer pointers to time.Time | ||||||
|  | 
 | ||||||
|  | ### Added | ||||||
|  | * Added mg.ReSend() to public Mailgun interface | ||||||
|  | * Added Message.SetSkipVerification() | ||||||
|  | * Added Message.SetRequireTLS() | ||||||
|  | 
 | ||||||
|  | ## [3.0.0] - 2019-01-15 | ||||||
|  | ### Added | ||||||
|  | * Added CHANGELOG | ||||||
|  | * Added `AddDomainIP()` | ||||||
|  | * Added `ListDomainIPS()` | ||||||
|  | * Added `DeleteDomainIP()` | ||||||
|  | * Added `ListIPS()` | ||||||
|  | * Added `GetIP()` | ||||||
|  | * Added `GetDomainTracking()` | ||||||
|  | * Added `GetDomainConnection()` | ||||||
|  | * Added `UpdateDomainConnection()` | ||||||
|  | * Added `CreateExport()` | ||||||
|  | * Added `ListExports()` | ||||||
|  | * Added `GetExports()` | ||||||
|  | * Added `GetExportLink()` | ||||||
|  | * Added `CreateTemplate()` | ||||||
|  | * Added `GetTemplate()` | ||||||
|  | * Added `UpdateTemplate()` | ||||||
|  | * Added `DeleteTemplate()` | ||||||
|  | * Added `ListTemplates()` | ||||||
|  | * Added `AddTemplateVersion()` | ||||||
|  | * Added `GetTemplateVersion()` | ||||||
|  | * Added `UpdateTemplateVersion()` | ||||||
|  | * Added `DeleteTemplateVersion()` | ||||||
|  | * Added `ListTemplateVersions()` | ||||||
|  | 
 | ||||||
|  | ### Changed | ||||||
|  | * Added a `mailgun.MockServer` which duplicates part of the mailgun API; suitable for testing | ||||||
|  | * `ListMailingLists()` now uses the `/pages` API and returns an iterator | ||||||
|  | * `ListMembers()` now uses the `/pages` API and returns an iterator | ||||||
|  | * Renamed public interface methods to be consistent. IE: `GetThing(), ListThing(), CreateThing()` | ||||||
|  | * Moved event objects into the `mailgun/events` package, so names like | ||||||
|  |   `MailingList` returned by API calls and `MailingList` as an event object | ||||||
|  |   don't conflict and confuse users. | ||||||
|  | * Now using context.Context for all network operations | ||||||
|  | * Test suite will run without MG_ env vars defined | ||||||
|  | * ListRoutes() now uses the iterator interface | ||||||
|  | * Added SkipNetworkTest() | ||||||
|  | * Renamed GetStatsTotals() to GetStats() | ||||||
|  | * Renamed GetUnsubscribes to ListUnsubscribes() | ||||||
|  | * Renamed Unsubscribe() to CreateUnsubscribe() | ||||||
|  | * Renamed RemoveUnsubscribe() to DeleteUnsubscribe() | ||||||
|  | * GetStats() now takes an `*opt` argument to pass optional parameters | ||||||
|  | * Modified GetUnsubscribe() to follow the API | ||||||
|  | * Now using golang modules | ||||||
|  | * ListCredentials() now returns an iterator | ||||||
|  | * ListUnsubscribes() now returns an paging iterator | ||||||
|  | * CreateDomain now accepts CreateDomainOption{} | ||||||
|  | * CreateDomain() now supports all optional parameters not just spam_action and wildcard. | ||||||
|  | * ListComplaints() now returns a page iterator | ||||||
|  | * Renamed `TagItem` to `Tag` | ||||||
|  | * ListBounces() now returns a page iterator | ||||||
|  | * API responses with CreatedAt fields are now unmarshalled into RFC2822 | ||||||
|  | * DomainList() now returns an iterator | ||||||
|  | * Updated godoc documentation | ||||||
|  | * Renamed ApiBase to APIBase | ||||||
|  | * Updated copyright to 2019 | ||||||
|  | * `ListEvents()` now returns a list of typed events | ||||||
|  | 
 | ||||||
|  | ### Removed | ||||||
|  | * Removed more deprecated types | ||||||
|  | * Removed gobuffalo/envy dependency | ||||||
|  | * Remove mention of the CLI in the README | ||||||
|  | * Removed mailgun cli from project | ||||||
|  | * Removed GetCode() from `Bounce` struct. Verified API returns 'string' and not 'int' | ||||||
|  | * Removed deprecated methods NewMessage and NewMIMEMessage | ||||||
|  | * Removed ginkgo and gomega tests | ||||||
|  | * Removed GetStats() As the /stats endpoint is depreciated | ||||||
							
								
								
									
										27
									
								
								vendor/github.com/mailgun/mailgun-go/v3/LICENSE
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										27
									
								
								vendor/github.com/mailgun/mailgun-go/v3/LICENSE
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							| @ -0,0 +1,27 @@ | |||||||
|  | Copyright (c) 2013-2016, Michael Banzon | ||||||
|  | All rights reserved. | ||||||
|  | 
 | ||||||
|  | Redistribution and use in source and binary forms, with or without modification, | ||||||
|  | are permitted provided that the following conditions are met: | ||||||
|  | 
 | ||||||
|  | * Redistributions of source code must retain the above copyright notice, this | ||||||
|  |   list of conditions and the following disclaimer. | ||||||
|  | 
 | ||||||
|  | * Redistributions in binary form must reproduce the above copyright notice, this | ||||||
|  |   list of conditions and the following disclaimer in the documentation and/or | ||||||
|  |   other materials provided with the distribution. | ||||||
|  | 
 | ||||||
|  | * Neither the names of Mailgun, Michael Banzon, nor the names of its | ||||||
|  |   contributors may be used to endorse or promote products derived from | ||||||
|  |   this software without specific prior written permission. | ||||||
|  | 
 | ||||||
|  | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND | ||||||
|  | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED | ||||||
|  | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE | ||||||
|  | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR | ||||||
|  | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES | ||||||
|  | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; | ||||||
|  | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON | ||||||
|  | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | ||||||
|  | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS | ||||||
|  | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | ||||||
							
								
								
									
										21
									
								
								vendor/github.com/mailgun/mailgun-go/v3/Makefile
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								vendor/github.com/mailgun/mailgun-go/v3/Makefile
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							| @ -0,0 +1,21 @@ | |||||||
|  | .PHONY: all | ||||||
|  | .DEFAULT_GOAL := all | ||||||
|  | 
 | ||||||
|  | PACKAGE := github.com/mailgun/mailgun-go | ||||||
|  | 
 | ||||||
|  | gen: | ||||||
|  | 	rm events/events_easyjson.go | ||||||
|  | 	easyjson --all events/events.go | ||||||
|  | 	rm events/objects_easyjson.go | ||||||
|  | 	easyjson --all events/objects.go | ||||||
|  | 
 | ||||||
|  | all: | ||||||
|  | 	export GO111MODULE=on; go test . -v | ||||||
|  | 
 | ||||||
|  | godoc: | ||||||
|  | 	mkdir -p /tmp/tmpgoroot/doc | ||||||
|  | 	-rm -rf /tmp/tmpgopath/src/${PACKAGE} | ||||||
|  | 	mkdir -p /tmp/tmpgopath/src/${PACKAGE} | ||||||
|  | 	tar -c --exclude='.git' --exclude='tmp' . | tar -x -C /tmp/tmpgopath/src/${PACKAGE} | ||||||
|  | 	echo -e "open http://localhost:6060/pkg/${PACKAGE}\n" | ||||||
|  | 	GOROOT=/tmp/tmpgoroot/ GOPATH=/tmp/tmpgopath/ godoc -http=localhost:6060 | ||||||
Some files were not shown because too many files have changed in this diff Show More
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user