From 618ae7cf4d05280046cc5267a26d3543a2bf6ab3 Mon Sep 17 00:00:00 2001 From: Jon Lundy Date: Mon, 23 Nov 2020 13:58:19 -0700 Subject: [PATCH] initial commit --- .gitignore | 1 + Makefile | 32 ++++ example.env | 37 ++++ go.mod | 22 +++ go.sum | 320 ++++++++++++++++++++++++++++++++ main.go | 200 ++++++++++++++++++++ pkg/cache/cache.go | 78 ++++++++ pkg/config/config.go | 103 +++++++++++ pkg/keyproofs/opengpg.go | 204 +++++++++++++++++++++ pkg/keyproofs/proofs.go | 372 ++++++++++++++++++++++++++++++++++++++ pkg/keyproofs/routes.go | 324 +++++++++++++++++++++++++++++++++ pkg/keyproofs/style.go | 127 +++++++++++++ pkg/keyproofs/template.go | 182 +++++++++++++++++++ pkg/keyproofs/vcard.go | 97 ++++++++++ pkg/promise/promise.go | 219 ++++++++++++++++++++++ pkg/promise/with-cache.go | 47 +++++ version.sh | 53 ++++++ 17 files changed, 2418 insertions(+) create mode 100644 .gitignore create mode 100644 Makefile create mode 100644 example.env create mode 100644 go.mod create mode 100644 go.sum create mode 100644 main.go create mode 100644 pkg/cache/cache.go create mode 100644 pkg/config/config.go create mode 100644 pkg/keyproofs/opengpg.go create mode 100644 pkg/keyproofs/proofs.go create mode 100644 pkg/keyproofs/routes.go create mode 100644 pkg/keyproofs/style.go create mode 100644 pkg/keyproofs/template.go create mode 100644 pkg/keyproofs/vcard.go create mode 100644 pkg/promise/promise.go create mode 100644 pkg/promise/with-cache.go create mode 100755 version.sh diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2eea525 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +.env \ No newline at end of file diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..dd5ed86 --- /dev/null +++ b/Makefile @@ -0,0 +1,32 @@ +NAME=sour.is-ipseity +BUMP?=current +DATE:=$(shell date -u +%FT%TZ) +HASH:=$(shell git rev-pars HEAD 2> /dev/null) +VERSION:=$(shell BUMP=$(BUMP) ./version.sh) + + +version: + @echo $(VERSION) + +tag: + git tag -a v$(VERSION) -m "Version: $(VERSION)" +release: + @make tag BUMP=patch + +run: + go run -v \ + -ldflags "\ + -X main.AppVersion=$(VERSION) \ + -X main.BuildHash=$(HASH) \ + -X main.BuildDate=$(DATE) \ + " \ + . + +build: + go run -v \ + -ldflags "\ + -X main.AppVersion=$(VERSION) \ + -X main.BuildHash=$(HASH) \ + -X main.BuildDate=$(DATE) \ + " \ + . diff --git a/example.env b/example.env new file mode 100644 index 0000000..d09e6fb --- /dev/null +++ b/example.env @@ -0,0 +1,37 @@ +# Rename to '.env' or pass required items to environment when running. + +# REDDIT_APIKEY [REQUIRED] +# REDDIT_SECRET [REQUIRED] +# To prevent reddits low ratelimits for non-authenticated requests +# provide api key and secret. +# aquire personal use script credentials here: https://www.reddit.com/prefs/apps + +REDDIT_APIKEY= +REDDIT_SECRET= + +# XMPP_USERNAME [REQUIRED] +# XMPP_PASSWORD [REQUIRED] +# To authenticate with xmpp for requesting VCard information. + +XMPP_USERNAME= +XMPP_PASSWORD= + +# HTTP_LISTEN [RECOMMEND] +# To set the listen address/port (default: :9061) + +HTTP_LISTEN= + +# BASE_URL [RECOMMEND] +# To set external facing url. It will try to guess hostname and port based on the HTTP_LISTEN. + +BASE_URL= + +# XMPP_URL [OPTIONAL] +# To set XMPP http url for VCard verification. (default: BASE_URL) + +XMPP_URL= + +# DNS_URL [OPTIONAL] +# To set DNS http url for DNS verification. (default: BASE_URL) + +XMPP_URL= diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..1471eb3 --- /dev/null +++ b/go.mod @@ -0,0 +1,22 @@ +module github.com/sour-is/keyproofs + +go 1.15 + +require ( + github.com/go-chi/chi v4.1.2+incompatible + github.com/google/go-cmp v0.5.3 // indirect + github.com/hashicorp/golang-lru v0.5.4 + github.com/joho/godotenv v1.3.0 + github.com/lucasb-eyer/go-colorful v1.0.3 + github.com/rs/cors v1.7.0 + github.com/rs/zerolog v1.20.0 + github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e + github.com/smartystreets/goconvey v1.6.4 // indirect + github.com/sour-is/crypto v0.0.0-20201016232853-f42a24ba5a81 + github.com/stretchr/testify v1.6.1 // indirect + github.com/tv42/zbase32 v0.0.0-20190604154422-aacc64a8f915 + go.uber.org/ratelimit v0.1.0 + golang.org/x/crypto v0.0.0-20201117144127-c1f2f97bffc9 + gosrc.io/xmpp v0.5.1 + sour.is/x/toolbox v0.12.17 +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..54fb3c5 --- /dev/null +++ b/go.sum @@ -0,0 +1,320 @@ +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +github.com/99designs/gqlgen v0.10.1/go.mod h1:IviubpnyI4gbBcj8IcxSSc/Q/+af5riwCmJmwF0uaPE= +github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/DATA-DOG/go-sqlmock v1.3.3/go.mod h1:f/Ixk793poVmq4qj/V1dPUg2JEAKC73Q5eFN3EC/SaM= +github.com/Masterminds/squirrel v0.0.0-20190511014652-b4b75d10d7bf/go.mod h1:yaPeOnPG5ZRwL9oKdTsO/prlkPbXWZlRVMQ/gGlzIuA= +github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= +github.com/agnivade/levenshtein v1.0.1/go.mod h1:CURSv5d9Uaml+FovSIICkLbAUZ9S4RqaHDIsdSBg7lM= +github.com/agnivade/wasmbrowsertest v0.3.1/go.mod h1:zQt6ZTdl338xxRaMW395qccVE2eQm0SjC/SDz0mPWQI= +github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= +github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8= +github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= +github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= +github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= +github.com/bouk/monkey v1.0.0 h1:k6z8fLlPhETfn5l9rlWVE7Q6B23DoaqosTdArvNQRdc= +github.com/bouk/monkey v1.0.0/go.mod h1:PG/63f4XEUlVyW1ttIeOJmJhhe1+t9EC/je3eTjvFhE= +github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= +github.com/cgilling/dbstats v0.0.0-20150427045024-c9db8cf218e6/go.mod h1:fsf3+k/VvGOE9sF2B9d6PBcZOzQIlDJhn2LhBqF/4VY= +github.com/chromedp/cdproto v0.0.0-20190614062957-d6d2f92b486d/go.mod h1:S8mB5wY3vV+vRIzf39xDXsw3XKYewW9X6rW2aEmkrSw= +github.com/chromedp/cdproto v0.0.0-20190621002710-8cbd498dd7a0/go.mod h1:S8mB5wY3vV+vRIzf39xDXsw3XKYewW9X6rW2aEmkrSw= +github.com/chromedp/cdproto v0.0.0-20190812224334-39ef923dcb8d/go.mod h1:0YChpVzuLJC5CPr+x3xkHN6Z8KOSXjNbL7qV8Wc4GW0= +github.com/chromedp/cdproto v0.0.0-20190926234355-1b4886c6fad6/go.mod h1:0YChpVzuLJC5CPr+x3xkHN6Z8KOSXjNbL7qV8Wc4GW0= +github.com/chromedp/chromedp v0.3.1-0.20190619195644-fd957a4d2901/go.mod h1:mJdvfrVn594N9tfiPecUidF6W5jPRKHymqHfzbobPsM= +github.com/chromedp/chromedp v0.4.0/go.mod h1:DC3QUn4mJ24dwjcaGQLoZrhm4X/uPHZ6spDbS2uFhm4= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= +github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= +github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= +github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= +github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +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/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= +github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= +github.com/edsrzf/mmap-go v1.0.0/go.mod h1:YO35OhQPt3KJa3ryjFM5Bs14WD66h8eGKpfaBNrHW5M= +github.com/fatih/color v1.6.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= +github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= +github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I= +github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/go-chi/chi v3.3.2+incompatible h1:uQNcQN3NsV1j4ANsPh42P4ew4t6rnRbJb8frvpp31qQ= +github.com/go-chi/chi v3.3.2+incompatible/go.mod h1:eB3wogJHnLi3x/kFX2A+IbTBlXxmMeXJVKy9tTv1XzQ= +github.com/go-chi/chi v4.1.2+incompatible h1:fGFk2Gmi/YKXk0OmGfBh0WgmN3XB8lVnEyNz34tQRec= +github.com/go-chi/chi v4.1.2+incompatible/go.mod h1:eB3wogJHnLi3x/kFX2A+IbTBlXxmMeXJVKy9tTv1XzQ= +github.com/go-interpreter/wagon v0.5.1-0.20190713202023-55a163980b6c/go.mod h1:5+b/MBYkclRZngKF5s6qrgWxSLgE9F5dFdO1hAueZLc= +github.com/go-interpreter/wagon v0.6.0/go.mod h1:5+b/MBYkclRZngKF5s6qrgWxSLgE9F5dFdO1hAueZLc= +github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= +github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= +github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= +github.com/go-sql-driver/mysql v1.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= +github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= +github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee/go.mod h1:L0fX3K22YWvt/FAX9NnzrNzcI4wNYi9Yku4O0LKYflo= +github.com/gobwas/pool v0.2.0/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw= +github.com/gobwas/ws v1.0.2/go.mod h1:szmBTxLgaFppYjEmNtny/v3w89xOydFnnZMcgRRu/EM= +github.com/gogo/protobuf v1.0.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= +github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= +github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= +github.com/golang/gddo v0.0.0-20190815223733-287de01127ef/go.mod h1:xEhNfoBDX1hzLm2Nf80qUvZ2sVwoMZ8d6IE2SrsQfh4= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2 h1:6nsPYzhq5kReh6QImI3k5qWzO4PEbvbIW2cwSfR/6xs= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.5.3 h1:x95R7cp+rSeeqAMI2knLtQ0DKlaBhv2NrtrOvafPHRo= +github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20190908185732-236ed259b199/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +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/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= +github.com/gopherjs/gopherjs v0.0.0-20190812055157-5d271430af9f h1:KMlcu9X58lhTA/KrfX8Bi1LQSO4pzoVjTiL3h4Jk+Zk= +github.com/gopherjs/gopherjs v0.0.0-20190812055157-5d271430af9f/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= +github.com/gorilla/context v0.0.0-20160226214623-1ea25387ff6f/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg= +github.com/gorilla/mux v1.6.1/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= +github.com/gorilla/mux v1.7.3/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= +github.com/gorilla/websocket v1.2.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= +github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= +github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= +github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= +github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= +github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/golang-lru v0.5.4 h1:YDjusn29QI/Das2iO9M0BHnIbxPeyuCHsjMW+lJfyTc= +github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= +github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= +github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= +github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= +github.com/jmoiron/sqlx v0.0.0-20150110152746-69738bd20981/go.mod h1:IiEW3SEiiErVyFdH8NTuWjSifiEQKUoyK3LNqr2kCHU= +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/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= +github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo= +github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= +github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= +github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/knq/sysutil v0.0.0-20181215143952-f05b59f0f307/go.mod h1:BjPj+aVjl9FW/cCGiF3nGh5v+9Gd3VCgBQbod/GlMaQ= +github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= +github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/lann/builder v0.0.0-20180802200727-47ae307949d0/go.mod h1:dXGbAdH5GtBTC4WfIxhKZfyBF/HBFgRZSWwZ9g/He9o= +github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0/go.mod h1:vmVJ0l/dxyfGW6FmdpVm2joNMFikkuWg0EoCKLGUMNw= +github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= +github.com/lucasb-eyer/go-colorful v1.0.3 h1:QIbQXiugsb+q10B+MI+7DI1oQLdmnep86tWFlaaUAac= +github.com/lucasb-eyer/go-colorful v1.0.3/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= +github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= +github.com/magiconair/properties v1.8.1 h1:ZC2Vc7/ZFkGmsVC9KvOjumD+G5lXy2RtTKyzRKO2BQ4= +github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= +github.com/mailru/easyjson v0.0.0-20190403194419-1ea4449da983/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/mailru/easyjson v0.0.0-20190620125010-da37f6c1e481/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/mailru/easyjson v0.7.0/go.mod h1:KAzv3t3aY1NaHWoQz1+4F1ccyAH66Jk7yos7ldAVICs= +github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= +github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= +github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= +github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= +github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ= +github.com/mattn/go-sqlite3 v1.11.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= +github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= +github.com/mitchellh/mapstructure v0.0.0-20180203102830-a4e142e9c047/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= +github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE= +github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= +github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= +github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.8.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= +github.com/opentracing/basictracer-go v1.0.0/go.mod h1:QfBfYuafItcjQuMwinw9GhYKwFXS9KnPs5lxoYwgW74= +github.com/opentracing/opentracing-go v1.0.2/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= +github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= +github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ= +github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= +github.com/pelletier/go-toml v1.5.0 h1:5BakdOZdtKJ1FFk6QdL8iSGrMWsXgchNJcrnarjbmJQ= +github.com/pelletier/go-toml v1.5.0/go.mod h1:5N711Q9dKgbdkxHL+MEfF31hpT7l0S0s/t2kKREewys= +github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= +github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso= +github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= +github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= +github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= +github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= +github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= +github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU= +github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= +github.com/rs/cors v1.6.0 h1:G9tHG9lebljV9mfp9SNPDL36nCDxmo3zTlAf1YgvzmI= +github.com/rs/cors v1.6.0/go.mod h1:gFx+x8UowdsKA9AchylcLynDq+nNFfI8FkUZdN/jGCU= +github.com/rs/cors v1.7.0 h1:+88SsELBHx5r+hZ8TCkggzSstaWNbDvThkVK8H6f9ik= +github.com/rs/cors v1.7.0/go.mod h1:gFx+x8UowdsKA9AchylcLynDq+nNFfI8FkUZdN/jGCU= +github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ= +github.com/rs/zerolog v1.20.0 h1:38k9hgtUBdxFwE34yS8rTHmHBa4eN16E4DJlv177LNs= +github.com/rs/zerolog v1.20.0/go.mod h1:IzD0RJ65iWH0w97OQQebJEvTZYvsCUm9WVLWBQrJRjo= +github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= +github.com/shurcooL/httpfs v0.0.0-20171119174359-809beceb2371/go.mod h1:ZY1cvUeJuFPAdZ/B6v7RHavJWZn2YPVFQ1OSXhCGOkg= +github.com/shurcooL/vfsgen v0.0.0-20180121065927-ffb13db8def0/go.mod h1:TrYk7fJVaAttu97ZZKrO9UbRa8izdowaMIZcxYMbVaw= +github.com/sirupsen/logrus v1.0.5/go.mod h1:pMByvHTf9Beacp5x1UXfOR9xyW/9antXMhjMPG0dEzc= +github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= +github.com/sirupsen/logrus v1.4.2 h1:SPIRibHv4MatM3XXNO2BJeFLZwZ2LvZgfQ5+UNI2im4= +github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= +github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e h1:MRM5ITcdelLK2j1vwZ3Je0FKVCfqOLp5zO6trqMLYs0= +github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e/go.mod h1:XV66xRDqSt+GTGFMVlhk3ULuV0y9ZmzeVGR4mloJI3M= +github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= +github.com/smartystreets/assertions v0.0.0-20190401211740-f487f9de1cd3 h1:hBSHahWMEgzwRyS6dRpxY0XyjZsHyQ61s084wo5PJe0= +github.com/smartystreets/assertions v0.0.0-20190401211740-f487f9de1cd3/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= +github.com/smartystreets/goconvey v0.0.0-20170602164621-9e8dc3f972df/go.mod h1:XDJAKZRPZ1CvBcN2aX5YOUTYGHki24fSF0Iv48Ibg0s= +github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s= +github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= +github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= +github.com/sour-is/crypto v0.0.0-20201016232853-f42a24ba5a81 h1:7LadZJfye3tq1Dr5c46uy1ign6mQr2bAOlCJeAXpB1A= +github.com/sour-is/crypto v0.0.0-20201016232853-f42a24ba5a81/go.mod h1:7/Of5cnNodFyJ6PH2C3STkdCRvqbhj9yA3BhQ/E62wA= +github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= +github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= +github.com/spf13/afero v1.2.2 h1:5jhuqJyZCZf2JRofRvN/nIFgIWNzPa3/Vz8mYylgbWc= +github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk= +github.com/spf13/cast v1.3.0 h1:oget//CVOEoFewqQxwr0Ej5yjygnqGkvggSE/gB35Q8= +github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= +github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= +github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk= +github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo= +github.com/spf13/pflag v1.0.1/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= +github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/viper v1.4.0/go.mod h1:PTJ7Z/lr49W6bUbkmS1V3by4uWynFiR9p7+dSq/yZzE= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.2.1/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= +github.com/tv42/zbase32 v0.0.0-20190604154422-aacc64a8f915 h1:vX9DBbEHmrebYnVthUTzMO6Zc1vvConJdD2s0uvXrfw= +github.com/tv42/zbase32 v0.0.0-20190604154422-aacc64a8f915/go.mod h1:Y5DJgF9Eou+hSWetC39Mns8E0PU7DykCLNWiYeOINrE= +github.com/twitchyliquid64/golang-asm v0.0.0-20190126203739-365674df15fc/go.mod h1:NoCfSFWosfqMqmmD7hApkirIK9ozpHjxRnRxs1l413A= +github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc= +github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA= +github.com/vektah/dataloaden v0.2.1-0.20190515034641-a19b9a6e7c9e/go.mod h1:/HUdMve7rvxZma+2ZELQeNh88+003LL7Pf/CZ089j8U= +github.com/vektah/gqlparser v1.1.2/go.mod h1:1ycwN7Ij5njmMkPPAOaRFY4rET2Enx7IkVv3vaXspKw= +github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= +github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= +github.com/yosssi/gmq v0.0.1/go.mod h1:mReykazh0U1JabvuWh1PEbzzJftqOQWsjr0Lwg5jL1Y= +go.coder.com/go-tools v0.0.0-20190317003359-0c6a35b74a16/go.mod h1:iKV5yK9t+J5nG9O3uF6KYdPEz3dyfMyB15MN1rbQ8Qw= +go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= +go.uber.org/atomic v1.4.0 h1:cxzIVoETapQEqDhQu3QfnvXAV4AlzcvUCxkVUFw3+EU= +go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= +go.uber.org/multierr v1.1.0 h1:HoEmRHQPVSqub6w2z2d2EOVs2fjyFRGyofhKuyDq0QI= +go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= +go.uber.org/ratelimit v0.1.0 h1:U2AruXqeTb4Eh9sYQSTrMhH8Cb7M0Ian2ibBOnBcnAw= +go.uber.org/ratelimit v0.1.0/go.mod h1:2X8KaoNd1J0lZV+PxJk/5+DGbO/tpwLR1m++a7FnB/Y= +go.uber.org/zap v1.10.0 h1:ORx85nbTijNz8ljznvCMR1ZBIPKFn3jQrag10X2AsuM= +go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= +golang.org/x/crypto v0.0.0-20180426230345-b49d69b5da94/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20201117144127-c1f2f97bffc9 h1:phUcVbl53swtrUN8kQEXFhUxPlIlWyBfKmidCu7P95o= +golang.org/x/crypto v0.0.0-20201117144127-c1f2f97bffc9/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181102091132-c10e9556a7bc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859 h1:R/3boaszxrf1GEUWTVDzSKVwLmSJpwZ1yqXm8j0v2QI= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190306220234-b354f8bf4d9e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190618155005-516e3c20635f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190712062909-fae7ac547cb7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190927073244-c990c680b611/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191010194322-b09406accb47/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191026070338-33540a1f6037 h1:YyJpGZS1sBuBCzLAR1VEpK193GlqGZbnPFnPV/5Rsb4= +golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/time v0.0.0-20190308202827-9d24e82272b4 h1:SvFZT6jyqRaOeXpc5h/JSfZenJ2O330aBsf7JfSUXmQ= +golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190125232054-d66bd3c5d5a6/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190515012406-7d7faa4812bd/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190828213141-aed303cbaa74/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20190920225731-5eefd052ad72/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7 h1:9zdDQZ7Thm29KFXgAX/+yaf3eVbP7djjWp/dXAppNCc= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= +gopkg.in/airbrake/gobrake.v2 v2.0.9/go.mod h1:/h5ZAUhDkGaJfjzjKLSjv6zCL6O0LLBxU4K+aSYdM/U= +gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= +gopkg.in/gemnasium/logrus-airbrake-hook.v2 v2.1.2/go.mod h1:Xk6kEKp8OKb+X14hQBKWaSkCsqBpgog8nAV2xsGOxlo= +gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= +gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= +gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.4 h1:/eiJrUcujPVeJ3xlSWaiNi3uSVmDGBK1pDHUHAnao1I= +gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gosrc.io/xmpp v0.5.1 h1:Rgrm5s2rt+npGggJH3HakQxQXR8ZZz3+QRzakRQqaq4= +gosrc.io/xmpp v0.5.1/go.mod h1:L3NFMqYOxyLz3JGmgFyWf7r9htE91zVGiK40oW4RwdY= +gotest.tools v2.1.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw= +gotest.tools/gotestsum v0.3.5/go.mod h1:Mnf3e5FUzXbkCfynWBGOwLssY7gTQgCHObK9tMpAriY= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +mvdan.cc/sh v2.6.4+incompatible/go.mod h1:IeeQbZq+x2SUGBensq/jge5lLQbS3XT2ktyp3wrt4x8= +nhooyr.io/websocket v1.6.5 h1:8TzpkldRfefda5JST+CnOH135bzVPz5uzfn/AF+gVKg= +nhooyr.io/websocket v1.6.5/go.mod h1:F259lAzPRAH0htX2y3ehpJe09ih1aSHN7udWki1defY= +sour.is/x/toolbox v0.12.17 h1:m+8fAl5dkuu2HsmkOuefb6tDFzpPq93xAAtZZvBxySk= +sour.is/x/toolbox v0.12.17/go.mod h1:DMM+aEl38izskLKuH+nDwEHH4FznK6dEiDcPR3wAq5s= +sourcegraph.com/sourcegraph/appdash v0.0.0-20180110180208-2cc67fd64755/go.mod h1:hI742Nqp5OhwiqlzhgfbWU4mW4yO10fP+LoT9WOswdU= +sourcegraph.com/sourcegraph/appdash-data v0.0.0-20151005221446-73f23eafcf67/go.mod h1:L5q+DGLGOQFpo1snNEkLOJT2d1YTW66rWNzatr3He1k= diff --git a/main.go b/main.go new file mode 100644 index 0000000..9ea47de --- /dev/null +++ b/main.go @@ -0,0 +1,200 @@ +package main + +import ( + "context" + "fmt" + "net/http" + "os" + "os/signal" + "strings" + "time" + + "github.com/go-chi/chi" + "github.com/go-chi/chi/middleware" + lru "github.com/hashicorp/golang-lru" + _ "github.com/joho/godotenv/autoload" + "github.com/rs/cors" + "github.com/rs/zerolog" + "github.com/rs/zerolog/log" + + "gosrc.io/xmpp" + + "github.com/sour-is/keyproofs/pkg/cache" + "github.com/sour-is/keyproofs/pkg/config" + "github.com/sour-is/keyproofs/pkg/keyproofs" +) + +var ( + // AppVersion Application Version Number + AppVersion string + + // AppBuild Application Build Hash + BuildHash string + + // AppDate Application Build Date + BuildDate string +) + +func main() { + log := zerolog.New(zerolog.NewConsoleWriter()).With().Timestamp().Caller().Logger() + + ctx := context.Background() + ctx = log.WithContext(ctx) + ctx = WithInterupt(ctx) + + cfg := config.New() + cfg.Set("app-name", "KeyProofs") + cfg.Set("app-version", AppVersion) + cfg.Set("build-hash", BuildHash) + cfg.Set("build-date", BuildDate) + ctx = cfg.Apply(ctx) + + if err := run(ctx); err != nil { + log.Fatal().Stack().Err(err).Send() + os.Exit(1) + } +} + +func run(ctx context.Context) error { + log := log.Ctx(ctx) + + // derive baseURL from listener options + listen := env("HTTP_LISTEN", ":9061") + host, _ := os.Hostname() + if strings.HasPrefix(listen, ":") { + host += listen + } + baseURL := fmt.Sprintf("http://%s", host) + + // Create cache for promise engine + arc, _ := lru.NewARC(4096) + c := cache.New(arc) + + // Set config values + cfg := config.FromContext(ctx) + cfg.Set("base-url", env("BASE_URL", baseURL)) + cfg.Set("dns-url", env("DNS_URL", baseURL)) + cfg.Set("xmpp-url", env("XMPP_URL", baseURL)) + + cfg.Set("reddit.api-key", os.Getenv("REDDIT_APIKEY")) + cfg.Set("reddit.secret", os.Getenv("REDDIT_SECRET")) + + cfg.Set("xmpp-config", &xmpp.Config{ + Jid: os.Getenv("XMPP_USERNAME"), + Credential: xmpp.Password(os.Getenv("XMPP_PASSWORD")), + }) + + // configure cors middleware + corsMiddleware := cors.New(cors.Options{ + AllowCredentials: true, + AllowedMethods: strings.Fields(env("CORS_METHODS", "GET")), + AllowedOrigins: strings.Fields(env("CORS_ORIGIN", "*")), + }).Handler + + mux := chi.NewRouter() + mux.Use( + cfg.ApplyHTTP, + corsMiddleware, + middleware.RequestID, + middleware.RealIP, + middleware.RequestLogger(&middleware.DefaultLogFormatter{Logger: accessLog(log.Info)}), + middleware.Recoverer, + ) + + app, err := keyproofs.New(ctx, c) + if err != nil { + return err + } + + app.Routes(mux) + + log.Info(). + Str("app", cfg.GetString("app-name")). + Str("version", cfg.GetString("app-version")). + Str("build-hash", cfg.GetString("build-hash")). + Str("build-date", cfg.GetString("build-date")). + Str("listen", listen). + Msg("startup") + + err = New(&http.Server{ + Addr: listen, + WriteTimeout: 15 * time.Second, + ReadTimeout: 15 * time.Second, + Handler: mux, + }).Run(ctx) + + if err != nil { + return err + } + + log.Info().Msg("shutdown") + return nil +} + +type Server struct { + srv *http.Server +} + +func New(s *http.Server) *Server { + return &Server{srv: s} +} +func (s *Server) Run(ctx context.Context) error { + log := log.Ctx(ctx) + + go func() { + <-ctx.Done() + log.Info().Msg("Shutdown HTTP") + + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + err := s.srv.Shutdown(ctx) + if err != nil { + log.Fatal().Err(err) + return + } + + log.Info().Msg("Stopped HTTP") + }() + + return s.srv.ListenAndServe() +} + +func env(name, defaultValue string) string { + if value := os.Getenv(name); value != "" { + return value + } + + return defaultValue +} + +func WithInterupt(ctx context.Context) context.Context { + log := log.Ctx(ctx) + ctx, cancel := context.WithCancel(ctx) + + // Listen for Interrupt signals + c := make(chan os.Signal, 1) + signal.Notify(c, os.Interrupt) + defer signal.Stop(c) + + go func() { + select { + case <-c: + cancel() + log.Warn().Msg("Shutting down! interrupt received") + return + case <-ctx.Done(): + cancel() + + log.Warn().Msg("Shutting down! context cancelled") + return + } + }() + + return ctx +} + +type accessLog func() *zerolog.Event + +func (a accessLog) Print(v ...interface{}) { + a().Msg(fmt.Sprint(v...)) +} diff --git a/pkg/cache/cache.go b/pkg/cache/cache.go new file mode 100644 index 0000000..2c8d12a --- /dev/null +++ b/pkg/cache/cache.go @@ -0,0 +1,78 @@ +package cache + +import ( + "time" +) + +type Key interface { + Key() interface{} +} +type Value interface { + Stale() bool + Value() interface{} +} +type item struct { + key interface{} + value interface{} + expireOn time.Time +} + +func NewItem(key, value interface{}, expires time.Duration) *item { + return &item{ + key: key, + value: value, + expireOn: time.Now().Add(expires), + } +} +func (e *item) Stale() bool { + if e == nil || e.value == nil { + return true + } + + return time.Now().After(e.expireOn) +} +func (s *item) Value() interface{} { + return s.value +} + +type Cacher interface { + Add(Key, Value) + Contains(Key) bool + Get(Key) (Value, bool) + Remove(Key) +} + +// InterfaceCacher external cache interface. +type InterfaceCacher interface { + Add(interface{}, interface{}) + Get(interface{}) (interface{}, bool) + Contains(interface{}) bool + Remove(interface{}) +} + +type cache struct { + cache InterfaceCacher +} + +func New(c InterfaceCacher) Cacher { + return &cache{cache: c} +} +func (c *cache) Add(key Key, value Value) { + c.cache.Add(key.Key(), value) +} +func (c *cache) Get(key Key) (Value, bool) { + if v, ok := c.cache.Get(key.Key()); ok { + if value, ok := v.(Value); ok && !value.Stale() { + return value, true + } + c.cache.Remove(key.Key()) + } + return nil, false +} +func (c *cache) Contains(key Key) bool { + _, ok := c.Get(key) + return ok +} +func (c *cache) Remove(key Key) { + c.cache.Remove(key.Key()) +} diff --git a/pkg/config/config.go b/pkg/config/config.go new file mode 100644 index 0000000..fb6cab9 --- /dev/null +++ b/pkg/config/config.go @@ -0,0 +1,103 @@ +package config + +import ( + "bytes" + "context" + "fmt" + "net/http" + "sync" +) + +type cfg struct { + sync.RWMutex + m map[string]interface{} +} + +var key struct{} + +func New() *cfg { + return &cfg{m: make(map[string]interface{})} +} + +func FromContext(ctx context.Context) *cfg { + if v, ok := ctx.Value(key).(*cfg); ok { + return v + } + + return nil +} + +func (c *cfg) Apply(ctx context.Context) context.Context { + if inctx := FromContext(ctx); inctx != nil { + inctx.setAll(c.m) + } + + return context.WithValue(ctx, key, c) +} + +func (c *cfg) setAll(m map[string]interface{}) { + if c == nil { + return + } + + c.Lock() + defer c.Unlock() + + c.m = m +} + +func (c *cfg) GetString(name string) string { + if v := c.Get(name); v != nil { + if s, ok := v.(string); ok { + return s + } else { + return fmt.Sprint(s) + } + } + + return "" +} + +func (c *cfg) Set(name string, value interface{}) { + if c == nil { + return + } + + c.Lock() + defer c.Unlock() + + c.m[name] = value +} + +func (c *cfg) Get(name string) interface{} { + if c == nil { + return nil + } + + c.RLock() + defer c.RUnlock() + + return c.m[name] +} + +func (c *cfg) String() string { + if c == nil { + return "" + } + + c.RLock() + defer c.RUnlock() + + var b bytes.Buffer + for k, v := range c.m { + fmt.Fprintf(&b, "%s = %v\n", k, v) + } + return b.String() +} + +func (c *cfg) ApplyHTTP(h http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + r = r.WithContext(c.Apply(r.Context())) + h.ServeHTTP(w, r) + }) +} diff --git a/pkg/keyproofs/opengpg.go b/pkg/keyproofs/opengpg.go new file mode 100644 index 0000000..a8e815e --- /dev/null +++ b/pkg/keyproofs/opengpg.go @@ -0,0 +1,204 @@ +package keyproofs + +import ( + "bytes" + "context" + "crypto/sha1" + "fmt" + "io" + "net/http" + "net/mail" + "net/url" + "strings" + + "github.com/rs/zerolog/log" + "github.com/sour-is/crypto/openpgp" + "github.com/tv42/zbase32" + "golang.org/x/crypto/openpgp/armor" +) + +func getOpenPGPkey(ctx context.Context, id string) (entity *Entity, err error) { + if isFingerprint(id) { + addr := "https://keys.openpgp.org/vks/v1/by-fingerprint/" + strings.ToUpper(id) + return getEntityHTTP(ctx, addr, true) + } else if email, err := mail.ParseAddress(id); err == nil { + addr := getWKDPubKeyAddr(email) + req, err := getEntityHTTP(ctx, addr, false) + if err == nil { + return req, err + } + + addr = "https://keys.openpgp.org/vks/v1/by-email/" + url.QueryEscape(id) + return getEntityHTTP(ctx, addr, true) + } else { + return entity, fmt.Errorf("Parse address: %w", err) + } +} + +func getEntityHTTP(ctx context.Context, url string, useArmored bool) (entity *Entity, err error) { + log := log.Ctx(ctx) + + req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) + if err != nil { + return entity, err + } + cl := http.Client{} + resp, err := cl.Do(req) + log.Debug(). + Bool("useArmored", useArmored). + Str("status", resp.Status). + Str("url", url). + Msg("getEntityHTTP") + + if err != nil { + return entity, fmt.Errorf("Requesting key: %w\nRemote URL: %v", err, url) + } + + if resp.StatusCode != 200 { + return entity, fmt.Errorf("bad response from remote: %s\nRemote URL: %v", resp.Status, url) + } + + defer resp.Body.Close() + + if resp.Header.Get("Content-Type") == "application/pgp-keys" { + useArmored = true + } + + return ReadKey(resp.Body, useArmored) +} + +type EntityKey string + +func (k EntityKey) Key() interface{} { + return k +} + +type Entity struct { + Primary *mail.Address + Emails []*mail.Address + Fingerprint string + Proofs []string + ArmorText string +} + +func getEntity(lis openpgp.EntityList) (*Entity, error) { + entity := &Entity{} + var err error + + for _, e := range lis { + if e == nil { + continue + } + if e.PrimaryKey == nil { + continue + } + + entity.Fingerprint = fmt.Sprintf("%X", e.PrimaryKey.Fingerprint) + + for name, ident := range e.Identities { + // Pick first identity + if entity.Primary == nil { + entity.Primary, err = mail.ParseAddress(name) + if err != nil { + return entity, err + } + } + // If one is marked primary use that + if ident.SelfSignature != nil && ident.SelfSignature.IsPrimaryId != nil && *ident.SelfSignature.IsPrimaryId { + entity.Primary, err = mail.ParseAddress(name) + if err != nil { + return entity, err + } + + } else { + var email *mail.Address + if email, err = mail.ParseAddress(name); err != nil { + return entity, err + } + if email.Address != entity.Primary.Address { + entity.Emails = append(entity.Emails, email) + } + } + + // If identity is self signed read notation data. + if ident.SelfSignature != nil && ident.SelfSignature.NotationData != nil { + // Get proofs and append to list. + if proofs, ok := ident.SelfSignature.NotationData["proof@metacode.biz"]; ok { + entity.Proofs = append(entity.Proofs, proofs...) + } + } + } + break + } + + if entity.Primary == nil { + entity.Primary, _ = mail.ParseAddress("nobody@nodomain.xyz") + } + + return entity, err +} + +func ReadKey(r io.Reader, useArmored bool) (e *Entity, err error) { + var buf bytes.Buffer + + var w io.Writer = &buf + + e = &Entity{} + + if !useArmored { + var aw io.WriteCloser + aw, err = armor.Encode(&buf, "PGP PUBLIC KEY BLOCK", nil) + if err != nil { + return e, fmt.Errorf("Read key: %w", err) + } + defer aw.Close() + + w = aw + } + defer func() { + if e != nil { + e.ArmorText = buf.String() + } + }() + + r = io.TeeReader(r, w) + + var lis openpgp.EntityList + + if useArmored { + lis, err = openpgp.ReadArmoredKeyRing(r) + } else { + lis, err = openpgp.ReadKeyRing(r) + } + if err != nil { + return e, fmt.Errorf("Read key: %w", err) + } + + e, err = getEntity(lis) + if err != nil { + return e, fmt.Errorf("Parse key: %w", err) + } + + return +} + +func isFingerprint(s string) bool { + for _, r := range s { + switch r { + case '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f', 'A', 'B', 'C', 'D', 'E', 'F': + default: + return false + } + } + + return true +} + +func getWKDPubKeyAddr(email *mail.Address) string { + parts := strings.SplitN(email.Address, "@", 2) + + hash := sha1.Sum([]byte(parts[0])) + lp := zbase32.EncodeToString(hash[:]) + + return fmt.Sprintf("https://%s/.well-known/openpgpkey/hu/%s", parts[1], lp) +} diff --git a/pkg/keyproofs/proofs.go b/pkg/keyproofs/proofs.go new file mode 100644 index 0000000..cb765ac --- /dev/null +++ b/pkg/keyproofs/proofs.go @@ -0,0 +1,372 @@ +package keyproofs + +import ( + "bufio" + "context" + "encoding/base64" + "encoding/json" + "errors" + "fmt" + "math/rand" + "net/http" + "net/url" + "strings" + + "github.com/rs/zerolog/log" + "github.com/sour-is/keyproofs/pkg/config" +) + +type Proof struct { + Fingerprint string + Icon string + Service string + Name string + Verify string + Link string + Status ProofStatus + + URI *url.URL +} +type Proofs map[string]*Proof + +type ProofKey string + +func (k ProofKey) Key() interface{} { + return k +} + +type ProofStatus int + +const ( + ProofChecking ProofStatus = iota + ProofError + ProofInvalid + ProofVerified +) + +func (p ProofStatus) String() string { + switch p { + case ProofChecking: + return "Checking" + case ProofError: + return "Error" + case ProofInvalid: + return "Invalid" + case ProofVerified: + return "Verified" + default: + return "" + } +} + +func NewProof(ctx context.Context, uri, fingerprint string) ProofResolver { + log := log.Ctx(ctx) + baseURL := config.FromContext(ctx).GetString("base-url") + + p := Proof{Verify: uri, Link: uri, Fingerprint: fingerprint} + defer log.Info(). + Interface("path", p.URI). + Str("name", p.Name). + Str("service", p.Service). + Str("link", p.Link). + Msg("Proof") + + var err error + + p.URI, err = url.Parse(uri) + if err != nil { + p.Icon = "exclamation-triangle" + p.Service = "error" + p.Name = err.Error() + + return &p + } + + p.Service = p.URI.Scheme + + switch p.URI.Scheme { + case "dns": + p.Icon = "fas fa-globe" + p.Name = p.URI.Opaque + p.Link = fmt.Sprintf("https://%s", p.URI.Opaque) + p.Verify = fmt.Sprintf("%s/dns/%s", baseURL, p.URI.Opaque) + return &httpResolve{p, p.Verify, nil} + + case "xmpp": + p.Icon = "fas fa-comments" + p.Name = p.URI.Opaque + p.Verify = fmt.Sprintf("%s/vcard/%s", baseURL, p.URI.Opaque) + return &httpResolve{p, p.Verify, nil} + + case "https": + p.Icon = "fas fa-atlas" + p.Name = p.URI.Hostname() + p.Link = fmt.Sprintf("https://%s", p.URI.Hostname()) + + switch { + case strings.HasPrefix(p.URI.Host, "twitter.com"): + // TODO: Add api authenticated code path. + if sp := strings.SplitN(p.URI.Path, "/", 3); len(sp) > 1 { + p.Icon = "fab fa-twitter" + p.Service = "Twitter" + p.Name = sp[1] + p.Link = fmt.Sprintf("https://twitter.com/%s", p.Name) + p.Verify = fmt.Sprintf("https://twitter.com%s", p.URI.Path) + url := fmt.Sprintf("https://mobile.twitter.com%s", p.URI.Path) + return &httpResolve{p, url, nil} + } + + case strings.HasPrefix(p.URI.Host, "news.ycombinator.com"): + p.Icon = "fab fa-hacker-news" + p.Service = "HackerNews" + p.Name = p.URI.Query().Get("id") + p.Link = uri + return &httpResolve{p, p.Verify, nil} + + case strings.HasPrefix(p.URI.Host, "dev.to"): + if sp := strings.SplitN(p.URI.Path, "/", 3); len(sp) > 1 { + p.Icon = "fab fa-dev" + p.Service = "dev.to" + p.Name = sp[1] + p.Link = fmt.Sprintf("https://dev.to/%s", p.Name) + url := fmt.Sprintf("https://dev.to/api/articles/%s/%s", sp[1], sp[2]) + return &httpResolve{p, url, nil} + } + + case strings.HasPrefix(p.URI.Host, "reddit.com"), strings.HasPrefix(p.URI.Host, "www.reddit.com"): + var headers map[string]string + + cfg := config.FromContext(ctx) + if apikey := cfg.GetString("reddit.api-key"); apikey != "" { + secret := cfg.GetString("reddit.secret") + + headers = map[string]string{ + "Authorization": fmt.Sprintf("basic %s", + base64.StdEncoding.EncodeToString([]byte(apikey+":"+secret))), + "User-Agent": "ipseity/0.1.0", + } + } + + if sp := strings.SplitN(p.URI.Path, "/", 6); len(sp) > 5 { + p.Icon = "fab fa-reddit" + p.Service = "Reddit" + p.Name = sp[2] + p.Link = fmt.Sprintf("https://www.reddit.com/user/%s", p.Name) + url := fmt.Sprintf("https://api.reddit.com/user/%s/comments/%s/%s", sp[2], sp[4], sp[5]) + return &httpResolve{p, url, headers} + } + + case strings.HasPrefix(p.URI.Host, "gist.github.com"): + p.Icon = "fab fa-github" + p.Service = "GitHub" + if sp := strings.SplitN(p.URI.Path, "/", 3); len(sp) > 2 { + var headers map[string]string + if secret := config.FromContext(ctx).GetString("github.secret"); secret != "" { + headers = map[string]string{ + "Authorization": fmt.Sprintf("bearer %s", secret), + "User-Agent": "keyproofs/0.1.0", + } + } + + p.Name = sp[1] + p.Link = fmt.Sprintf("https://github.com/%s", p.Name) + url := fmt.Sprintf("https://api.github.com/gists/%s", sp[2]) + return &httpResolve{p, url, headers} + } + + case strings.HasPrefix(p.URI.Host, "lobste.rs"): + if sp := strings.SplitN(p.URI.Path, "/", 3); len(sp) > 2 { + p.Icon = "fas fa-list-ul" + p.Service = "Lobsters" + p.Name = sp[2] + p.Link = uri + p.Verify += ".json" + return &httpResolve{p, p.Verify, nil} + } + + case strings.HasSuffix(p.URI.Path, "/gitlab_proof"): + if sp := strings.SplitN(p.URI.Path, "/", 3); len(sp) > 1 { + p.Icon = "fab fa-gitlab" + p.Service = "GetLab" + p.Name = sp[1] + p.Link = fmt.Sprintf("https://%s/%s", p.URI.Host, p.Name) + p.Name = fmt.Sprintf("%s@%s", p.Name, p.URI.Host) + return &gitlabResolve{p} + } + + case strings.HasSuffix(p.URI.Path, "/gitea_proof"): + if sp := strings.SplitN(p.URI.Path, "/", 3); len(sp) > 2 { + p.Icon = "fas fa-mug-hot" + p.Service = "Gitea" + p.Name = sp[1] + p.Link = fmt.Sprintf("https://%s/%s", p.URI.Host, p.Name) + p.Name = fmt.Sprintf("%s@%s", p.Name, p.URI.Host) + url := fmt.Sprintf("https://%s/api/v1/repos/%s/gitea_proof", p.URI.Host, sp[1]) + return &httpResolve{p, url, nil} + } + + default: + if sp := strings.SplitN(p.URI.Path, "/", 3); len(sp) > 1 { + p.Icon = "fas fa-project-diagram" + p.Service = "Fediverse" + if len(sp) > 2 && (sp[1] == "u" || sp[1] == "user" || sp[1] == "users") { + p.Name = sp[2] + } else { + p.Name = sp[1] + } + p.Name = fmt.Sprintf("%s@%s", p.Name, p.URI.Host) + p.Link = uri + return &httpResolve{p, p.Verify, nil} + } + } + default: + p.Icon = "exclamation-triangle" + p.Service = "unknown" + p.Name = "nobody" + } + + return &p +} + +type ProofResolver interface { + Resolve(context.Context) error + Proof() *Proof +} + +type httpResolve struct { + proof Proof + url string + headers map[string]string +} + +func (p *httpResolve) Resolve(ctx context.Context) error { + err := checkHTTP(ctx, p.url, p.proof.Fingerprint, p.headers) + if err == ErrNoFingerprint { + p.proof.Status = ProofInvalid + } else if err != nil { + p.proof.Status = ProofError + } else { + p.proof.Status = ProofVerified + } + return err +} +func (p *httpResolve) Proof() *Proof { + return &p.proof +} + +type gitlabResolve struct { + proof Proof +} + +func (r *gitlabResolve) Resolve(ctx context.Context) error { + uri := r.proof.URI + r.proof.Status = ProofInvalid + + if sp := strings.SplitN(uri.Path, "/", 3); len(sp) > 1 { + user := []struct { + Id int `json:"id"` + }{} + if err := httpJSON(ctx, fmt.Sprintf("https://%s/api/v4/users?username=%s", uri.Host, sp[1]), nil, &user); err != nil { + return err + } + if len(user) == 0 { + return ErrNoFingerprint + } + u := user[0] + url := fmt.Sprintf("https://%s/api/v4/users/%d/projects", uri.Host, u.Id) + proofs := []struct { + Description string + }{} + if err := httpJSON(ctx, url, nil, &proofs); err != nil { + return err + } + if len(proofs) == 0 { + return ErrNoFingerprint + } + ck := fmt.Sprintf("[Verifying my OpenPGP key: openpgp4fpr:%s]", strings.ToLower(r.proof.Fingerprint)) + for _, p := range proofs { + if strings.Contains(p.Description, ck) { + r.proof.Status = ProofVerified + return nil + } + } + } + + return ErrNoFingerprint +} +func (p *gitlabResolve) Proof() *Proof { + return &p.proof +} + +func (p *Proof) Resolve(ctx context.Context) error { + return fmt.Errorf("Not Implemented") +} +func (p *Proof) Proof() *Proof { + return p +} + +func checkHTTP(ctx context.Context, uri, fingerprint string, hdr map[string]string) error { + log := log.Ctx(ctx) + + log.Info(). + Str("URI", uri). + Str("fp", fingerprint). + Msg("Proof") + + req, err := http.NewRequestWithContext(ctx, "GET", uri, nil) + if err != nil { + log.Err(err) + return err + } + req.Header.Set("Accept", "application/json") + for k, v := range hdr { + req.Header.Set(k, v) + } + + res, err := http.DefaultClient.Do(req) + if err != nil { + log.Err(err) + return err + } + defer res.Body.Close() + + ts := rand.Int63() + log.Info().Str("uri", uri).Int64("ts", ts).Msg("Reading data") + defer log.Info().Str("uri", uri).Int64("ts", ts).Msg("Read data") + + scan := bufio.NewScanner(res.Body) + for scan.Scan() { + if strings.Contains(strings.ToUpper(scan.Text()), fingerprint) { + return nil + } + } + + return ErrNoFingerprint +} + +var ErrNoFingerprint = errors.New("fingerprint not found") + +func httpJSON(ctx context.Context, uri string, hdr map[string]string, dst interface{}) error { + log := log.Ctx(ctx) + + log.Info().Str("URI", uri).Msg("httpJSON") + + req, err := http.NewRequestWithContext(ctx, "GET", uri, nil) + if err != nil { + log.Err(err) + return err + } + req.Header.Set("Accept", "application/json") + for k, v := range hdr { + req.Header.Set(k, v) + } + + res, err := http.DefaultClient.Do(req) + if err != nil { + log.Err(err) + return err + } + defer res.Body.Close() + + return json.NewDecoder(res.Body).Decode(dst) +} diff --git a/pkg/keyproofs/routes.go b/pkg/keyproofs/routes.go new file mode 100644 index 0000000..54edc13 --- /dev/null +++ b/pkg/keyproofs/routes.go @@ -0,0 +1,324 @@ +package keyproofs + +import ( + "context" + "encoding/base64" + "fmt" + "net" + "net/http" + "net/mail" + "strconv" + "strings" + "text/template" + "time" + + "github.com/go-chi/chi" + zlog "github.com/rs/zerolog/log" + "github.com/skip2/go-qrcode" + "gosrc.io/xmpp" + + "github.com/sour-is/keyproofs/pkg/cache" + "github.com/sour-is/keyproofs/pkg/config" + "github.com/sour-is/keyproofs/pkg/promise" +) + +var expireAfter = 20 * time.Minute + +func New(ctx context.Context, c cache.Cacher) (*identity, error) { + log := zlog.Ctx(ctx) + + var ok bool + var xmppConfig *xmpp.Config + if xmppConfig, ok = config.FromContext(ctx).Get("xmpp-config").(*xmpp.Config); !ok { + log.Error().Msg("no xmpp-config") + + return nil, fmt.Errorf("no xmpp config") + } + + conn, err := NewXMPP(ctx, xmppConfig) + if err != nil { + return nil, err + } + + tasker := promise.NewRunner(ctx, promise.Timeout(30*time.Second), promise.WithCache(c, expireAfter)) + i := &identity{ + cache: c, + tasker: tasker, + conn: conn, + } + + return i, nil +} + +// 1x1 gif pixel +var pixl = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII=" +var keypng, _ = base64.StdEncoding.DecodeString("iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAABKUlEQVQ4jZ2SvUoDURCFUy/Y2Fv4BoKIiFgLSWbmCWw0e3cmNgGfwacQsbCxUEFEEIVkxsQulaK1kheIiFVW1mJXiZv904FbXb5zzvzUaiWlPqyYwIkyvRjjqwmeaauxUcbFMKOvTKEJRVPv05hCY9wrhHt+fckEJ79gxg9rweJN8qdSkESZjlLOkQm+Xe9szlubFkxwYoznuQIm9DgrQJEyjZXpPU5Eo6L+H7IEUmJFAnBQJmAMp5nw0IFnjFoiEGrQXJuBLx14JtgtiR5qAO2c4aFLAffGeGiMT8b0rAEe96WxnBlbGbbia/vZ+2CwjXO5g0pN/TZ1NNXgoQPPHO2aJLsViu4E+xdVnXsOOtPOMbxeDY6jw/6/nL+r6+qryjQyhqs/OSf1Bf+pJC1wKqO/AAAAAElFTkSuQmCC") + +var defaultStyle = &Style{ + Avatar: pixl, + Cover: pixl, + Background: pixl, + Palette: getPalette("#93CCEA"), +} + +type identity struct { + cache cache.Cacher + tasker promise.Tasker + conn *connection +} + +func (s *identity) Routes(r *chi.Mux) { + r.Use(secHeaders) + r.MethodFunc("GET", "/id/{id}", s.get) + r.MethodFunc("GET", "/dns/{domain}", s.getDNS) + r.MethodFunc("GET", "/vcard/{jid}", s.getVCard) + r.MethodFunc("GET", "/qr", s.getQR) + r.MethodFunc("GET", "/favicon.ico", func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "image/png") + w.WriteHeader(200) + _, _ = w.Write(keypng) + }) +} + +func fmtKey(key promise.Key) string { + return fmt.Sprintf("%T", key.Key()) +} + +func (s *identity) get(w http.ResponseWriter, r *http.Request) { + log := zlog.Ctx(r.Context()) + + id := chi.URLParam(r, "id") + log.Debug().Str("get ", id).Send() + + // Setup timeout for page refresh + ctx, cancel := context.WithTimeout(r.Context(), 2*time.Second) + defer cancel() + + // Run tasks to resolve entity, style, and proofs. + task := s.tasker.Run(EntityKey(id), func(q promise.Q) { + ctx := q.Context() + log := zlog.Ctx(ctx).With().Interface(fmtKey(q), q.Key()).Logger() + + key := q.Key().(EntityKey) + + entity, err := getOpenPGPkey(ctx, string(key)) + if err != nil { + q.Reject(err) + return + } + + log.Debug().Msg("Resolving Entity") + q.Resolve(entity) + }) + + task.After(func(q promise.ResultQ) { + entity := q.Result().(*Entity) + + zlog.Ctx(q.Context()). + Info(). + Str("email", entity.Primary.Address). + Interface(fmtKey(q), q.Key()). + Msg("Do Style ") + + q.Run(StyleKey(entity.Primary.Address), func(q promise.Q) { + ctx := q.Context() + log := zlog.Ctx(ctx).With().Interface(fmtKey(q), q.Key()).Logger() + + key := q.Key().(StyleKey) + + log.Debug().Msg("start task") + style, err := s.getStyle(ctx, string(key)) + if err != nil { + q.Reject(err) + return + } + + log.Debug().Msg("Resolving Style") + q.Resolve(style) + }) + + }) + + task.After(func(q promise.ResultQ) { + entity := q.Result().(*Entity) + log := zlog.Ctx(ctx). + With(). + Interface(fmtKey(q), q.Key()). + Logger() + + log.Info(). + Int("num", len(entity.Proofs)). + Msg("Scheduling Proofs") + + for i := range entity.Proofs { + q.Run(ProofKey(entity.Proofs[i]), func(q promise.Q) { + ctx := q.Context() + log := zlog.Ctx(ctx). + With(). + Interface(fmtKey(q), q.Key()). + Logger() + + key := q.Key().(ProofKey) + proof := NewProof(ctx, string(key), entity.Fingerprint) + defer log.Debug().Interface("status", proof.Proof().Status).Msg("Resolving Proof") + + if err := proof.Resolve(ctx); err != nil && err != ErrNoFingerprint { + log.Err(err).Send() + } + + q.Resolve(proof.Proof()) + }) + } + }) + + page := page{Style: defaultStyle} + + // Wait for either entity to resolve or timeout + select { + case <-task.Await(): + log.Print("Tasks Competed") + if err := task.Err(); err != nil { + page.Err = err + page.IsComplete = true + break + } + page.Entity = task.Result().(*Entity) + + case <-ctx.Done(): + log.Print("Deadline Timeout") + if e, ok := s.cache.Get(EntityKey(id)); ok { + page.Entity = e.Value().(*Entity) + } + } + + // Build page based on available information. + if page.Entity != nil { + var gotStyle, gotProofs bool + + if s, ok := s.cache.Get(StyleKey(page.Entity.Primary.Address)); ok { + page.Style = s.Value().(*Style) + gotStyle = true + } + + gotProofs = true + if len(page.Entity.Proofs) > 0 { + page.HasProofs = true + proofs := make(Proofs, len(page.Entity.Proofs)) + for i := range page.Entity.Proofs { + p := page.Entity.Proofs[i] + + if s, ok := s.cache.Get(ProofKey(p)); ok { + log.Debug().Str("uri", p).Msg("Proof from cache") + proofs[p] = s.Value().(*Proof) + } else { + log.Debug().Str("uri", p).Msg("Missing proof") + proofs[p] = NewProof(ctx, p, page.Entity.Fingerprint).Proof() + gotProofs = false + } + } + page.Proofs = &proofs + } + + page.IsComplete = gotStyle && gotProofs + } + + // Template and display. + t, err := template.New("identity").Parse(pageTPL) + if err != nil { + WriteText(w, 500, err.Error()) + return + } + err = t.Execute(w, page) + if err != nil { + WriteText(w, 500, err.Error()) + return + } +} + +func (s *identity) getDNS(w http.ResponseWriter, r *http.Request) { + domain := chi.URLParam(r, "domain") + + res, err := net.DefaultResolver.LookupTXT(r.Context(), domain) + if err != nil { + WriteText(w, 400, err.Error()) + return + } + + WriteText(w, 200, strings.Join(res, "\n")) +} + +func (s *identity) getQR(w http.ResponseWriter, r *http.Request) { + log := zlog.Ctx(r.Context()) + + content := r.URL.Query().Get("c") + size := 64 + + sz, _ := strconv.Atoi(r.URL.Query().Get("s")) + + if sz > -10 && sz < 0 { + size = sz + } else if sz > 64 && sz < 4096 { + size = sz + } else if sz > 4096 { + size = 4096 + } + + quality := qrcode.Medium + switch r.URL.Query().Get("r") { + case "L": + quality = qrcode.Low + case "Q": + quality = qrcode.High + case "H": + quality = qrcode.Highest + } + + log.Debug().Str("content", content).Int("size", size).Interface("quality", quality).Int("s", sz).Msg("QRCode") + + png, err := qrcode.Encode(content, quality, size) + if err != nil { + WriteText(w, 400, err.Error()) + return + } + + w.Header().Add("Content-Type", "image/png") + w.WriteHeader(200) + + _, _ = w.Write(png) +} + +func (s *identity) getVCard(w http.ResponseWriter, r *http.Request) { + jid := chi.URLParam(r, "jid") + if _, err := mail.ParseAddress(jid); err != nil { + fmt.Fprint(w, err) + w.WriteHeader(400) + } + + vcard, err := s.conn.GetXMPPVCard(r.Context(), jid) + if err != nil { + fmt.Fprint(w, err) + w.WriteHeader(500) + } + + w.Header().Set("Content-Type", "text/xml") + w.WriteHeader(200) + fmt.Fprint(w, vcard) +} + +func secHeaders(h http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("X-XSS-Protection", "1; mode=block") + w.Header().Set("X-Frame-Options", "DENY") + w.Header().Set("X-Content-Type-Options", "nosniff") + w.Header().Set("X-Content-Type-Options", "nosniff") + + h.ServeHTTP(w, r) + }) +} + +// WriteText writes plain text +func WriteText(w http.ResponseWriter, code int, o string) { + w.Header().Set("Content-Type", "text/plain") + w.WriteHeader(code) + _, _ = w.Write([]byte(o)) +} diff --git a/pkg/keyproofs/style.go b/pkg/keyproofs/style.go new file mode 100644 index 0000000..af0c84d --- /dev/null +++ b/pkg/keyproofs/style.go @@ -0,0 +1,127 @@ +package keyproofs + +import ( + "context" + "crypto/md5" + "fmt" + "net" + "strings" + + "github.com/lucasb-eyer/go-colorful" + "sour.is/x/toolbox/log" +) + +type StyleKey string + +func (s StyleKey) Key() interface{} { + return s +} + +type Style struct { + Avatar, + Cover, + Background string + + Palette []string +} + +func (s *identity) getStyle(ctx context.Context, email string) (*Style, error) { + avatarHost, styleHost, err := styleSRV(ctx, email) + if err != nil { + return nil, err + } + log.Infos("getStyle", "avatar", avatarHost, "style", styleHost) + + hash := md5.New() + email = strings.TrimSpace(strings.ToLower(email)) + _, _ = hash.Write([]byte(email)) + + id := hash.Sum(nil) + + style := &Style{} + + style.Palette = getPalette(fmt.Sprintf("#%x", id[:3])) + style.Avatar = fmt.Sprintf("https://%s/avatar/%x", avatarHost, id) + style.Cover = pixl + style.Background = "https://lavana.sour.is/bg/52548b3dcb032882675afe1e4bcba0e9" + + if styleHost != "" { + style.Cover = fmt.Sprintf("https://%s/cover/%x", styleHost, id) + style.Background = fmt.Sprintf("https://%s/bg/%x", styleHost, id) + } + + return style, err +} + +func styleSRV(ctx context.Context, email string) (avatar string, style string, err error) { + + // Defaults + style = "" + avatar = "www.gravatar.com" + + parts := strings.SplitN(email, "@", 2) + if _, srv, err := net.DefaultResolver.LookupSRV(ctx, "style-sec", "tcp", parts[1]); err == nil { + if len(srv) > 0 { + style = strings.TrimSuffix(srv[0].Target, ".") + avatar = strings.TrimSuffix(srv[0].Target, ".") + + return avatar, style, err + } + } + + if _, srv, err := net.DefaultResolver.LookupSRV(ctx, "avatars-sec", "tcp", parts[1]); err == nil { + if len(srv) > 0 { + avatar = strings.TrimSuffix(srv[0].Target, ".") + + return avatar, style, err + } + } + + return +} + +// getPalette maes a complementary color palette. https://play.golang.org/p/nBXLUocGsU5 +func getPalette(hex string) []string { + reference, _ := colorful.Hex(hex) + reference = sat(lum(reference, 0, .5), 0, .5) + + white := colorful.Color{R: 1, G: 1, B: 1} + black := colorful.Color{R: 0, G: 0, B: 0} + accentA := hue(reference, 60) + accentB := hue(reference, -60) + accentC := hue(reference, -180) + + return append( + []string{}, + + white.Hex(), + lum(reference, .4, .6).Hex(), + reference.Hex(), + lum(reference, .4, 0).Hex(), + black.Hex(), + + lum(accentA, .4, .6).Hex(), + accentA.Hex(), + lum(accentA, .4, 0).Hex(), + + lum(accentB, .4, .6).Hex(), + accentB.Hex(), + lum(accentB, .4, 0).Hex(), + + lum(accentC, .4, .6).Hex(), + accentC.Hex(), + lum(accentC, .4, 0).Hex(), + ) +} +func hue(in colorful.Color, H float64) colorful.Color { + h, s, l := in.Hsl() + return colorful.Hsl(h+H, s, l) +} +func sat(in colorful.Color, S, V float64) colorful.Color { + h, s, l := in.Hsl() + return colorful.Hsl(h, V+s*S, l) +} +func lum(in colorful.Color, L, V float64) colorful.Color { + h, s, l := in.Hsl() + return colorful.Hsl(h, s, V+l*L) +} diff --git a/pkg/keyproofs/template.go b/pkg/keyproofs/template.go new file mode 100644 index 0000000..171531d --- /dev/null +++ b/pkg/keyproofs/template.go @@ -0,0 +1,182 @@ +package keyproofs + +type page struct { + Entity *Entity + Style *Style + Proofs *Proofs + + HasProofs bool + IsComplete bool + Err error +} + +var pageTPL = ` + + + {{if not .IsComplete}}{{end}} + + + + + + + + +{{ with .Style }} + +{{end}} + + + +
+
+
+
+
+ + {{ with .Err }} +
+ +
+ +
+

Something went wrong...

+
{{.}}
+
+ {{else}} + {{ with .Style }} +
+ avatar +
+ {{end}} + + + {{with .Entity}} +
+

{{.Primary.Name}}

+

{{.Fingerprint}}

+
+
+ qrcode +
+ {{else}} +
+

Loading...

+

Reading key from remote service.

+
+ {{end}} + + + {{end}} +
+
+
+ +
+ {{ with .Entity }} +
+
Contact
+
+ {{with .Primary}} {{.Name}} <{{.Address}}> Primary{{end}} + {{range .Emails}} {{.Name}} <{{.Address}}>{{end}} +
+
+
+ {{end}} + + {{if .HasProofs}} + {{with .Proofs}} +
+
Proofs
+
    + {{range .}} +
  • +
    +
    + + + {{.Name}} + + + {{if eq .Status 0}} + Checking + {{else if eq .Status 1}} + Error + {{else if eq .Status 2}} + Invalid + {{else if eq .Status 3}} + Verified + {{end}} +
    +
    + {{if eq .Service "xmpp"}} + qrcode + {{end}} +
    +
    +
  • + {{end}} +
+
+
+ {{else}} +
+
Proofs
+
Loading...
+
+
+ {{end}} + {{end}} +
+ + +
+
+ + +` diff --git a/pkg/keyproofs/vcard.go b/pkg/keyproofs/vcard.go new file mode 100644 index 0000000..790c331 --- /dev/null +++ b/pkg/keyproofs/vcard.go @@ -0,0 +1,97 @@ +package keyproofs + +import ( + "context" + "encoding/xml" + "fmt" + + "github.com/rs/zerolog/log" + "gosrc.io/xmpp" + "gosrc.io/xmpp/stanza" +) + +type VCard struct { + XMLName xml.Name `xml:"vcard-temp vCard"` + FullName string `xml:"FN"` + NickName string `xml:"NICKNAME"` + Description string `xml:"DESC"` + URL string `xml:"URL"` +} + +func NewVCard() *VCard { + return &VCard{} +} + +func (c *VCard) Namespace() string { + return c.XMLName.Space +} + +func (c *VCard) GetSet() *stanza.ResultSet { + return nil +} + +func (c *VCard) String() string { + b, _ := xml.MarshalIndent(c, "", " ") + return string(b) +} + +func init() { + stanza.TypeRegistry.MapExtension(stanza.PKTIQ, xml.Name{Space: "vcard-temp", Local: "vCard"}, VCard{}) +} + +type connection struct { + client *xmpp.Client +} + +func NewXMPP(ctx context.Context, config *xmpp.Config) (*connection, error) { + log := log.Ctx(ctx) + router := xmpp.NewRouter() + conn := &connection{} + + var err error + conn.client, err = xmpp.NewClient(config, router, func(err error) { log.Error().Err(err).Send() }) + if err != nil { + return nil, err + } + + go func() { + <-ctx.Done() + err := conn.client.Disconnect() + log.Error().Err(err).Send() + }() + + err = conn.client.Connect() + + return conn, err +} + +func (conn *connection) GetXMPPVCard(ctx context.Context, jid string) (vc *VCard, err error) { + log := log.Ctx(ctx) + + var iq *stanza.IQ + iq, err = stanza.NewIQ(stanza.Attrs{To: jid, Type: "get"}) + if err != nil { + return nil, err + } + iq.Payload = NewVCard() + + var ch chan stanza.IQ + ch, err = conn.client.SendIQ(ctx, iq) + if err != nil { + return nil, err + } + + select { + case result := <-ch: + b, _ := xml.MarshalIndent(result, "", " ") + log.Debug().Msgf("%s", b) + if vcard, ok := result.Payload.(*VCard); ok { + return vcard, nil + } + return nil, fmt.Errorf("bad response: %s", result.Payload) + + case <-ctx.Done(): + } + + return nil, fmt.Errorf("timeout requesting vcard for %s", jid) +} diff --git a/pkg/promise/promise.go b/pkg/promise/promise.go new file mode 100644 index 0000000..29a479e --- /dev/null +++ b/pkg/promise/promise.go @@ -0,0 +1,219 @@ +package promise + +import ( + "context" + "fmt" + "sync" + "time" + + "github.com/rs/zerolog/log" + + "go.uber.org/ratelimit" +) + +type Q interface { + Key() interface{} + Context() context.Context + Resolve(interface{}) + Reject(error) + + Tasker +} +type ResultQ interface { + Key() interface{} + Context() context.Context + Result() interface{} + + Tasker +} +type Fn func(Q) +type AfterFn func(ResultQ) +type Key interface { + Key() interface{} +} + +func typ(v interface{}) string { + return fmt.Sprintf("%T", v) +} + +type qTask struct { + key Key + + fn Fn + ctx context.Context + + cancel func() + done chan struct{} + + result interface{} + err error + + Tasker +} + +func (t *qTask) Key() interface{} { return t.key } +func (t *qTask) Context() context.Context { return t.ctx } +func (t *qTask) Resolve(r interface{}) { t.result = r; t.finish() } +func (t *qTask) Reject(err error) { t.err = err; t.finish() } + +// After runs on successful completion of the task. +func (t *qTask) After(fn AfterFn) { + log := log.Ctx(t.Context()) + + go func() { + defer func() { + if r := recover(); r != nil { + log.Panic().Msgf("%v", r) + } + }() + + <-t.Await() + if err := t.Err(); err != nil { + return + } + fn(t) + }() +} +func (t *qTask) Await() <-chan struct{} { return t.done } +func (t *qTask) Cancel() { t.err = fmt.Errorf("task cancelled"); t.finish() } + +func (t *qTask) Result() interface{} { return t.result } +func (t *qTask) Err() error { return t.err } + +func (t *qTask) finish() { + if t.done == nil { + return + } + + t.cancel() + close(t.done) + t.done = nil +} + +type Option interface { + Apply(*qTask) +} +type OptionFn func(*qTask) + +func (fn OptionFn) Apply(t *qTask) { fn(t) } + +type Tasker interface { + Run(Key, Fn, ...Option) *qTask +} + +type Runner struct { + defaultOpts []Option + queue map[interface{}]*qTask + mu sync.RWMutex + ctx context.Context + cancel func() + pause chan struct{} + limiter ratelimit.Limiter +} + +type Timeout time.Duration + +func (d Timeout) Apply(task *qTask) { + task.ctx, task.cancel = context.WithTimeout(task.ctx, time.Duration(d)) +} + +func (tr *Runner) Run(key Key, fn Fn, opts ...Option) *qTask { + log := log.Ctx(tr.ctx) + + tr.mu.RLock() + log.Trace().Interface(typ(key), key.Key()).Msg("task to run") + + if task, ok := tr.queue[key.Key()]; ok { + tr.mu.RUnlock() + log.Trace().Interface(typ(key), key.Key()).Msg("task found running") + + return task + } + tr.mu.RUnlock() + + task := &qTask{ + key: key, + fn: fn, + cancel: func() {}, + ctx: tr.ctx, + done: make(chan struct{}), + Tasker: tr, + } + + for _, opt := range tr.defaultOpts { + opt.Apply(task) + } + + for _, opt := range opts { + opt.Apply(task) + } + + tr.mu.Lock() + tr.queue[key.Key()] = task + tr.mu.Unlock() + + tr.limiter.Take() + + go func() { + defer func() { + if r := recover(); r != nil { + log.Panic().Msgf("%v", r) + } + + if err := task.Err(); err == nil { + log.Trace().Interface(typ(key), key.Key()).Msg("task complete") + } else { + log.Debug().Interface(typ(key), key.Key()).Err(err).Msg("task Failed") + } + + tr.mu.Lock() + delete(tr.queue, task.Key()) + tr.mu.Unlock() + }() + + log.Trace().Interface(typ(key), key.Key()).Msg("task Running") + + task.fn(task) + }() + + return task +} + +func NewRunner(ctx context.Context, defaultOpts ...Option) *Runner { + ctx, cancel := context.WithCancel(ctx) + + tr := &Runner{ + defaultOpts: defaultOpts, + queue: make(map[interface{}]*qTask), + ctx: ctx, + cancel: cancel, + pause: make(chan struct{}), + limiter: ratelimit.New(10), + } + + return tr +} + +func (tr *Runner) List() []*qTask { + tr.mu.RLock() + defer tr.mu.RUnlock() + + lis := make([]*qTask, 0, len(tr.queue)) + + for _, task := range tr.queue { + lis = append(lis, task) + } + + return lis +} + +func (tr *Runner) Stop() { + tr.cancel() +} + +func (tr *Runner) Len() int { + tr.mu.RLock() + defer tr.mu.RUnlock() + + return len(tr.queue) +} diff --git a/pkg/promise/with-cache.go b/pkg/promise/with-cache.go new file mode 100644 index 0000000..0220cf7 --- /dev/null +++ b/pkg/promise/with-cache.go @@ -0,0 +1,47 @@ +package promise + +import ( + "time" + + "github.com/rs/zerolog/log" + "github.com/sour-is/keyproofs/pkg/cache" +) + +func WithCache(c cache.Cacher, expireAfter time.Duration) OptionFn { + return func(task *qTask) { + innerFn := task.fn + task.fn = func(q Q) { + log := log.Ctx(q.Context()) + + cacheKey, ok := q.Key().(cache.Key) + if !ok { + log.Trace().Interface(typ(q), q.Key()).Msg("not a cache key") + innerFn(q) + + return + } + + if v, ok := c.Get(cacheKey); ok { + log.Trace().Interface(typ(cacheKey), cacheKey.Key()).Msg("task result in cache") + q.Resolve(v.Value()) + + return + } + + log.Trace().Interface(typ(cacheKey), cacheKey.Key()).Msg("task not in cache") + innerFn(q) + + if err := task.Err(); err != nil { + log.Err(err) + + return + } + + // expireAfter = time.Duration(rand.Int63() % int64(5*time.Second)) + result := cache.NewItem(cacheKey, task.Result(), expireAfter) + + log.Trace().Interface(typ(cacheKey), cacheKey.Key()).Msgf("task result to cache") + c.Add(cacheKey, result) + } + } +} diff --git a/version.sh b/version.sh new file mode 100755 index 0000000..b7ccbc1 --- /dev/null +++ b/version.sh @@ -0,0 +1,53 @@ +#!/bin/bash + +# Increment a version string using Semantic Versioning (SemVer) terminology. + +# Parse command line options. + +case $BUMP in + current ) ;; + major ) major=true;; + minor ) minor=true;; + patch ) patch=true;; +esac + +version=$(git describe --tags `git rev-list --tags --max-count=1 2> /dev/null` 2> /dev/null|cut -b2-) + +# Build array from version string. + +a=( ${version//./ } ) + +# If version string is missing or has the wrong number of members, show usage message. + +if [ ${#a[@]} -ne 3 ] +then + version=0.0.0 + a=( ${version//./ } ) +fi + +# Increment version numbers as requested. + +if [ ! -z $major ] +then + ((a[0]++)) + a[1]=0 + a[2]=0 +fi + +if [ ! -z $minor ] +then + ((a[1]++)) + a[2]=0 +fi + +if [ ! -z $patch ] +then + ((a[2]++)) +fi + +if git status --porcelain >/dev/null +then + echo "${a[0]}.${a[1]}.${a[2]}" +else + echo "${a[0]}.${a[1]}.${a[2]}-dirty" +fi \ No newline at end of file