From 819cc1ba64325bbb1fecd6c90170b71a48aa686f Mon Sep 17 00:00:00 2001 From: Jon Lundy Date: Mon, 23 Nov 2020 20:49:53 -0700 Subject: [PATCH] update styles and add home page --- .vscode/launch.json | 17 + go.mod | 7 +- go.sum | 19 +- main.go | 88 ++--- pkg/graceful/with-interrupt.go | 127 ++++++++ pkg/keyproofs/routes-dns.go | 32 ++ .../{routes.go => routes-keyproofs.go} | 165 +++++----- pkg/keyproofs/routes-vcard.go | 56 ++++ pkg/keyproofs/style.go | 2 +- pkg/keyproofs/template.go | 305 +++++++++++------- pkg/keyproofs/vcard.go | 21 +- 11 files changed, 576 insertions(+), 263 deletions(-) create mode 100644 .vscode/launch.json create mode 100644 pkg/graceful/with-interrupt.go create mode 100644 pkg/keyproofs/routes-dns.go rename pkg/keyproofs/{routes.go => routes-keyproofs.go} (69%) create mode 100644 pkg/keyproofs/routes-vcard.go diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..c23774c --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,17 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "name": "Launch", + "type": "go", + "request": "launch", + "mode": "auto", + "program": "${fileDirname}", + "env": {}, + "args": [] + } + ] +} \ No newline at end of file diff --git a/go.mod b/go.mod index b48f7ac..626e76f 100644 --- a/go.mod +++ b/go.mod @@ -4,18 +4,21 @@ go 1.15 require ( github.com/go-chi/chi v4.1.2+incompatible - github.com/google/go-cmp v0.5.3 // indirect + github.com/google/go-cmp v0.5.4 // 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/russross/blackfriday v1.5.2 github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e 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/multierr v1.6.0 go.uber.org/ratelimit v0.1.0 - golang.org/x/crypto v0.0.0-20201117144127-c1f2f97bffc9 + golang.org/x/crypto v0.0.0-20201124201722-c8d3bf9c5392 + golang.org/x/net v0.0.0-20201110031124-69a78807bb2b // indirect golang.org/x/text v0.3.4 // indirect gosrc.io/xmpp v0.5.1 ) diff --git a/go.sum b/go.sum index 7842f77..afb5ae9 100644 --- a/go.sum +++ b/go.sum @@ -28,6 +28,8 @@ github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5a 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/go-cmp v0.5.4 h1:L8R9j+yAqZuZjsqh/z+F1NCffTKKLShY6zXTItVIZ8M= +github.com/google/go-cmp v0.5.4/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= @@ -70,6 +72,8 @@ 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/russross/blackfriday v1.5.2 h1:HyvC0ARfnZBqnXwABFeSZHpKvJHJJfPz81GNueLj0oo= +github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= github.com/sirupsen/logrus v1.0.5/go.mod h1:pMByvHTf9Beacp5x1UXfOR9xyW/9antXMhjMPG0dEzc= github.com/sirupsen/logrus v1.4.2 h1:SPIRibHv4MatM3XXNO2BJeFLZwZ2LvZgfQ5+UNI2im4= github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= @@ -93,14 +97,21 @@ github.com/twitchyliquid64/golang-asm v0.0.0-20190126203739-365674df15fc/go.mod go.coder.com/go-tools v0.0.0-20190317003359-0c6a35b74a16/go.mod h1:iKV5yK9t+J5nG9O3uF6KYdPEz3dyfMyB15MN1rbQ8Qw= 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/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw= +go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= go.uber.org/multierr v1.1.0 h1:HoEmRHQPVSqub6w2z2d2EOVs2fjyFRGyofhKuyDq0QI= go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= +go.uber.org/multierr v1.6.0 h1:y6IPFStTAIT5Ytl7/XYmHvzXQ7S3g/IeZW9hyZ5thw4= +go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= 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= golang.org/x/crypto v0.0.0-20180426230345-b49d69b5da94/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-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 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/crypto v0.0.0-20201124201722-c8d3bf9c5392 h1:xYJJ3S178yv++9zXV/hnr29plCAGO9vAFG9dorqaFQc= +golang.org/x/crypto v0.0.0-20201124201722-c8d3bf9c5392/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 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= @@ -108,6 +119,10 @@ golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 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/net v0.0.0-20200202094626-16171245cfb2 h1:CCH4IOTTfewWjGOlSp+zGcjutRKlBEZQ6wTn8ozI/nI= +golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20201110031124-69a78807bb2b h1:uwuIcX0g4Yl1NC5XAz37xsr2lTtcqevgzYNVt49waME= +golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/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= @@ -123,10 +138,10 @@ golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20190927073244-c990c680b611/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/sys v0.0.0-20200930185726-fdedc70b468f/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/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.4 h1:0YWbFKbhXG/wIiuHDSKpS0Iy7FSA+u45VtBMfQcFTTc= golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4 h1:SvFZT6jyqRaOeXpc5h/JSfZenJ2O330aBsf7JfSUXmQ= diff --git a/main.go b/main.go index 9ea47de..bec0e7d 100644 --- a/main.go +++ b/main.go @@ -5,7 +5,6 @@ import ( "fmt" "net/http" "os" - "os/signal" "strings" "time" @@ -21,6 +20,7 @@ import ( "github.com/sour-is/keyproofs/pkg/cache" "github.com/sour-is/keyproofs/pkg/config" + "github.com/sour-is/keyproofs/pkg/graceful" "github.com/sour-is/keyproofs/pkg/keyproofs" ) @@ -36,11 +36,16 @@ var ( ) func main() { - log := zerolog.New(zerolog.NewConsoleWriter()).With().Timestamp().Caller().Logger() + log := zerolog.New(zerolog.NewConsoleWriter()). + With(). + Timestamp(). + Caller(). + Logger() ctx := context.Background() ctx = log.WithContext(ctx) - ctx = WithInterupt(ctx) + ctx = graceful.WithInterupt(ctx) + ctx, _ = graceful.WithWaitGroup(ctx) cfg := config.New() cfg.Set("app-name", "KeyProofs") @@ -50,13 +55,14 @@ func main() { ctx = cfg.Apply(ctx) if err := run(ctx); err != nil { - log.Fatal().Stack().Err(err).Send() + log.Error().Stack().Err(err).Msg("Application Failed") os.Exit(1) } } func run(ctx context.Context) error { log := log.Ctx(ctx) + wg := graceful.WaitGroup(ctx) // derive baseURL from listener options listen := env("HTTP_LISTEN", ":9061") @@ -66,10 +72,6 @@ func run(ctx context.Context) error { } 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)) @@ -94,6 +96,7 @@ func run(ctx context.Context) error { mux := chi.NewRouter() mux.Use( cfg.ApplyHTTP, + secHeaders, corsMiddleware, middleware.RequestID, middleware.RealIP, @@ -101,12 +104,20 @@ func run(ctx context.Context) error { middleware.Recoverer, ) - app, err := keyproofs.New(ctx, c) + // Create cache for promise engine + arc, _ := lru.NewARC(4096) + c := cache.New(arc) + + keyproofApp := keyproofs.NewKeyProofApp(ctx, c) + dnsApp := keyproofs.NewDNSApp(ctx) + vcardApp, err := keyproofs.NewVCardApp(ctx) if err != nil { return err } - app.Routes(mux) + keyproofApp.Routes(mux) + dnsApp.Routes(mux) + vcardApp.Routes(mux) log.Info(). Str("app", cfg.GetString("app-name")). @@ -122,13 +133,11 @@ func run(ctx context.Context) error { ReadTimeout: 15 * time.Second, Handler: mux, }).Run(ctx) - if err != nil { return err } - log.Info().Msg("shutdown") - return nil + return wg.Wait(5 * time.Second) } type Server struct { @@ -140,23 +149,30 @@ func New(s *http.Server) *Server { } func (s *Server) Run(ctx context.Context) error { log := log.Ctx(ctx) + wg := graceful.WaitGroup(ctx) - go func() { + wg.Go(func() error { <-ctx.Done() log.Info().Msg("Shutdown HTTP") - ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + ctx := context.Background() + ctx, cancel := context.WithTimeout(ctx, 10*time.Second) defer cancel() err := s.srv.Shutdown(ctx) - if err != nil { - log.Fatal().Err(err) - return + if err != nil && err != http.ErrServerClosed { + return err } log.Info().Msg("Stopped HTTP") - }() + return nil + }) - return s.srv.ListenAndServe() + err := s.srv.ListenAndServe() + if err != nil && err != http.ErrServerClosed { + return err + } + + return nil } func env(name, defaultValue string) string { @@ -167,30 +183,16 @@ func env(name, defaultValue string) string { return defaultValue } -func WithInterupt(ctx context.Context) context.Context { - log := log.Ctx(ctx) - ctx, cancel := context.WithCancel(ctx) +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") + w.Header().Set("Content-Security-Policy", "font-src https://pagecdn.io") - // 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 + h.ServeHTTP(w, r) + }) } type accessLog func() *zerolog.Event diff --git a/pkg/graceful/with-interrupt.go b/pkg/graceful/with-interrupt.go new file mode 100644 index 0000000..c5df5a1 --- /dev/null +++ b/pkg/graceful/with-interrupt.go @@ -0,0 +1,127 @@ +package graceful + +import ( + "context" + "errors" + "fmt" + "os" + "os/signal" + "sync" + "time" + + "github.com/rs/zerolog/log" + "go.uber.org/multierr" +) + +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) + + go func() { + defer signal.Stop(c) + + for { + select { + case <-c: + cancel() + log.Warn().Msg("Shutting down! interrupt received") + return + case <-ctx.Done(): + log.Warn().Msg("Shutting down! context cancelled") + return + } + } + }() + + return ctx +} + +type contextKey struct{ string } + +var wgKey = contextKey{"waitgroup"} + +type wgContext struct { + wg sync.WaitGroup + err error + ctx context.Context +} + +func (wg *wgContext) String() string { + return fmt.Sprintf("WaitGroup[%v %v]", wg.err, wg.ctx) +} + +type WG interface { + Wait(time.Duration) error + Go(func() error) +} + +func WithWaitGroup(ctx context.Context) (context.Context, WG) { + if wg := WaitGroup(ctx); wg != nil { + return ctx, wg + } + wg := &wgContext{ctx: ctx} + return context.WithValue(ctx, wgKey, wg), wg +} + +func WaitGroup(ctx context.Context) *wgContext { + if wg, ok := ctx.Value(wgKey).(*wgContext); ok { + return wg + } + return nil +} + +func (wg *wgContext) Go(fn func() error) { + if wg == nil { + panic("nil wait group") + } + + wg.Add(1) + go func() { + err := fn() + wg.err = multierr.Append(wg.err, err) + wg.Done() + }() +} + +func (wg *wgContext) Add(n int) { + wg.wg.Add(n) +} + +func (wg *wgContext) Done() { + wg.wg.Done() +} + +func (wg *wgContext) Wait(gracetime time.Duration) error { + if wg == nil { + return nil + } + + log := log.Ctx(wg.ctx) + + ch := make(chan struct{}) + go func() { + wg.wg.Wait() + close(ch) + }() + + <-wg.ctx.Done() + wg.err = multierr.Append(wg.err, wg.ctx.Err()) + + log.Debug().Msg("shutdown begin") + timer := time.NewTimer(gracetime) + + select { + case <-ch: + case <-timer.C: + wg.err = multierr.Append(wg.err, ErrExpiredGrace) + } + log.Debug().Msg("shutdown complete") + + return wg.err +} + +var ErrExpiredGrace = errors.New("grace time expired") diff --git a/pkg/keyproofs/routes-dns.go b/pkg/keyproofs/routes-dns.go new file mode 100644 index 0000000..0ab8a7d --- /dev/null +++ b/pkg/keyproofs/routes-dns.go @@ -0,0 +1,32 @@ +package keyproofs + +import ( + "context" + "net" + "net/http" + "strings" + + "github.com/go-chi/chi" +) + +type dnsApp struct { + resolver *net.Resolver +} + +func NewDNSApp(ctx context.Context) *dnsApp { + return &dnsApp{resolver: net.DefaultResolver} +} +func (app *dnsApp) getDNS(w http.ResponseWriter, r *http.Request) { + domain := chi.URLParam(r, "domain") + + res, err := app.resolver.LookupTXT(r.Context(), domain) + if err != nil { + writeText(w, 400, err.Error()) + return + } + + writeText(w, 200, strings.Join(res, "\n")) +} +func (app *dnsApp) Routes(r *chi.Mux) { + r.MethodFunc("GET", "/dns/{domain}", app.getDNS) +} diff --git a/pkg/keyproofs/routes.go b/pkg/keyproofs/routes-keyproofs.go similarity index 69% rename from pkg/keyproofs/routes.go rename to pkg/keyproofs/routes-keyproofs.go index 8e0af1d..a937d5a 100644 --- a/pkg/keyproofs/routes.go +++ b/pkg/keyproofs/routes-keyproofs.go @@ -4,18 +4,15 @@ import ( "context" "encoding/base64" "fmt" - "net" + "html/template" "net/http" - "net/mail" "strconv" - "strings" - "text/template" "time" "github.com/go-chi/chi" zlog "github.com/rs/zerolog/log" + "github.com/russross/blackfriday" "github.com/skip2/go-qrcode" - "gosrc.io/xmpp" "github.com/sour-is/keyproofs/pkg/cache" "github.com/sour-is/keyproofs/pkg/config" @@ -23,32 +20,7 @@ import ( ) 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 -} +var runnerTimeout = 30 * time.Second // 1x1 gif pixel var pixl = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII=" @@ -61,30 +33,32 @@ var defaultStyle = &Style{ Palette: getPalette("#93CCEA"), } -type identity struct { +type keyproofApp 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) +func NewKeyProofApp(ctx context.Context, c cache.Cacher) *keyproofApp { + return &keyproofApp{ + cache: c, + tasker: promise.NewRunner( + ctx, + promise.Timeout(runnerTimeout), + promise.WithCache(c, expireAfter), + ), + } +} +func (app *keyproofApp) Routes(r *chi.Mux) { + r.MethodFunc("GET", "/", app.getHome) + r.MethodFunc("GET", "/id/{id}", app.getProofs) + r.MethodFunc("GET", "/qr", app.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) { +func (app *keyproofApp) getProofs(w http.ResponseWriter, r *http.Request) { log := zlog.Ctx(r.Context()) cfg := config.FromContext(r.Context()) @@ -96,7 +70,7 @@ func (s *identity) get(w http.ResponseWriter, r *http.Request) { defer cancel() // Run tasks to resolve entity, style, and proofs. - task := s.tasker.Run(EntityKey(id), func(q promise.Q) { + task := app.tasker.Run(EntityKey(id), func(q promise.Q) { ctx := q.Context() log := zlog.Ctx(ctx).With().Interface(fmtKey(q), q.Key()).Logger() @@ -128,7 +102,7 @@ func (s *identity) get(w http.ResponseWriter, r *http.Request) { key := q.Key().(StyleKey) log.Debug().Msg("start task") - style, err := s.getStyle(ctx, string(key)) + style, err := getStyle(ctx, string(key)) if err != nil { q.Reject(err) return @@ -137,7 +111,6 @@ func (s *identity) get(w http.ResponseWriter, r *http.Request) { log.Debug().Msg("Resolving Style") q.Resolve(style) }) - }) task.After(func(q promise.ResultQ) { @@ -175,7 +148,6 @@ func (s *identity) get(w http.ResponseWriter, r *http.Request) { page := page{Style: defaultStyle} page.AppName = fmt.Sprintf("%s v%s", cfg.GetString("app-name"), cfg.GetString("app-version")) - // Wait for either entity to resolve or timeout select { case <-task.Await(): @@ -189,7 +161,7 @@ func (s *identity) get(w http.ResponseWriter, r *http.Request) { case <-ctx.Done(): log.Print("Deadline Timeout") - if e, ok := s.cache.Get(EntityKey(id)); ok { + if e, ok := app.cache.Get(EntityKey(id)); ok { page.Entity = e.Value().(*Entity) } } @@ -198,7 +170,7 @@ func (s *identity) get(w http.ResponseWriter, r *http.Request) { if page.Entity != nil { var gotStyle, gotProofs bool - if s, ok := s.cache.Get(StyleKey(page.Entity.Primary.Address)); ok { + if s, ok := app.cache.Get(StyleKey(page.Entity.Primary.Address)); ok { page.Style = s.Value().(*Style) gotStyle = true } @@ -210,7 +182,7 @@ func (s *identity) get(w http.ResponseWriter, r *http.Request) { for i := range page.Entity.Proofs { p := page.Entity.Proofs[i] - if s, ok := s.cache.Get(ProofKey(p)); ok { + if s, ok := app.cache.Get(ProofKey(p)); ok { log.Debug().Str("uri", p).Msg("Proof from cache") proofs[p] = s.Value().(*Proof) } else { @@ -226,31 +198,62 @@ func (s *identity) get(w http.ResponseWriter, r *http.Request) { } // Template and display. - t, err := template.New("identity").Parse(pageTPL) + var err error + t := template.New("page") + t, err = t.Parse(pageTPL) if err != nil { - WriteText(w, 500, err.Error()) + writeText(w, 500, err.Error()) return } + + t, err = t.Parse(proofTPL) + if err != nil { + writeText(w, 500, err.Error()) + return + } + err = t.Execute(w, page) if err != nil { - WriteText(w, 500, err.Error()) + writeText(w, 500, err.Error()) return } } +func (app *keyproofApp) getHome(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + cfg := config.FromContext(ctx) -func (s *identity) getDNS(w http.ResponseWriter, r *http.Request) { - domain := chi.URLParam(r, "domain") + baseURL := cfg.GetString("base-url") + if id := r.URL.Query().Get("id"); id != "" { + http.Redirect(w, r, fmt.Sprintf("%s/id/%s", baseURL, id), http.StatusFound) + return + } - res, err := net.DefaultResolver.LookupTXT(r.Context(), domain) + page := page{Style: defaultStyle, IsComplete: true, Markdown: homeMKDN} + page.AppName = fmt.Sprintf("%s v%s", cfg.GetString("app-name"), cfg.GetString("app-version")) + + // Template and display. + var err error + t := template.New("page") + t = t.Funcs(template.FuncMap{"markDown": markDowner}) + t, err = t.Parse(pageTPL) if err != nil { - WriteText(w, 400, err.Error()) + writeText(w, 500, err.Error()) return } - WriteText(w, 200, strings.Join(res, "\n")) -} + t, err = t.Parse(homeTPL) + if err != nil { + writeText(w, 500, err.Error()) + return + } -func (s *identity) getQR(w http.ResponseWriter, r *http.Request) { + err = t.Execute(w, page) + if err != nil { + writeText(w, 500, err.Error()) + return + } +} +func (app *keyproofApp) getQR(w http.ResponseWriter, r *http.Request) { log := zlog.Ctx(r.Context()) content := r.URL.Query().Get("c") @@ -280,7 +283,7 @@ func (s *identity) getQR(w http.ResponseWriter, r *http.Request) { png, err := qrcode.Encode(content, quality, size) if err != nil { - WriteText(w, 400, err.Error()) + writeText(w, 400, err.Error()) return } @@ -290,38 +293,18 @@ func (s *identity) getQR(w http.ResponseWriter, r *http.Request) { _, _ = 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) - }) +func markDowner(args ...interface{}) template.HTML { + s := blackfriday.MarkdownCommon([]byte(fmt.Sprintf("%s", args...))) + return template.HTML(s) } // WriteText writes plain text -func WriteText(w http.ResponseWriter, code int, o string) { +func writeText(w http.ResponseWriter, code int, o string) { w.Header().Set("Content-Type", "text/plain") w.WriteHeader(code) _, _ = w.Write([]byte(o)) } + +func fmtKey(key promise.Key) string { + return fmt.Sprintf("%T", key.Key()) +} diff --git a/pkg/keyproofs/routes-vcard.go b/pkg/keyproofs/routes-vcard.go new file mode 100644 index 0000000..2a196f6 --- /dev/null +++ b/pkg/keyproofs/routes-vcard.go @@ -0,0 +1,56 @@ +package keyproofs + +import ( + "context" + "fmt" + "net/http" + "net/mail" + + "github.com/go-chi/chi" + zlog "github.com/rs/zerolog/log" + "github.com/sour-is/keyproofs/pkg/config" + "gosrc.io/xmpp" +) + +type vcardApp struct { + conn *connection +} + +func NewVCardApp(ctx context.Context) (*vcardApp, 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 + } + + return &vcardApp{conn: conn}, nil +} +func (app *vcardApp) Routes(r *chi.Mux) { + r.MethodFunc("GET", "/vcard/{jid}", app.getVCard) +} +func (app *vcardApp) 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 := app.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) +} diff --git a/pkg/keyproofs/style.go b/pkg/keyproofs/style.go index e4340a8..1002b41 100644 --- a/pkg/keyproofs/style.go +++ b/pkg/keyproofs/style.go @@ -25,7 +25,7 @@ type Style struct { Palette []string } -func (s *identity) getStyle(ctx context.Context, email string) (*Style, error) { +func getStyle(ctx context.Context, email string) (*Style, error) { log := log.Ctx(ctx) avatarHost, styleHost, err := styleSRV(ctx, email) diff --git a/pkg/keyproofs/template.go b/pkg/keyproofs/template.go index 8f69cd0..cd84563 100644 --- a/pkg/keyproofs/template.go +++ b/pkg/keyproofs/template.go @@ -2,10 +2,11 @@ package keyproofs type page struct { AppName string - Entity *Entity - Style *Style - Proofs *Proofs + Entity *Entity + Style *Style + Proofs *Proofs + Markdown string HasProofs bool IsComplete bool Err error @@ -15,7 +16,6 @@ var pageTPL = ` {{if not .IsComplete}}{{end}} - @@ -25,6 +25,10 @@ var pageTPL = ` {{ with .Style }} {{end}} @@ -74,110 +77,178 @@ var pageTPL = `
-
-
-
- - {{ 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}} -
+ {{template "content" .}}
` + +var homeTPL = ` +{{define "content"}} +
+
+
+
+

Key Proofs

+

Verify social identitys using OpenPGP

+
+
+
+
+
+
+
+
+ +
+ +
+
+
+
+
+
+
{{.Markdown | markDown}}
+{{end}} +` + +var proofTPL = ` +{{define "content"}} +
+
+
+ + {{ 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}} +
+{{end}} +` + +var homeMKDN = ` +## About Keyproofs + +KeyProofs is a server side version of Keyoxide. There is no JavaScript executed on this page and resourcesKeys are looked up via [Web key directory](https://datatracker.ietf.org/doc/draft-koch-openpgp-webkey-service/) +or from . + + +### Decentralized online identity proofs + +- You decide which accounts are linked together +- You decide where this data is stored +- KeyProofs does not store your identity data on its servers +- KeyProofs merely verifies the identity proofs and displays them + +### Empowering the internet citizen + +- A verified identity proof proves ownership of an account and builds trust +- No bad actor can impersonate you as long as your accounts aren't compromised +- Your online identity data is safe from greedy internet corporations + +### User-centric platform + +- KeyProofs generates QR codes that integrate with OpenKeychain and Conversations +- KeyProofs fetches the key wherever the user decides to store it +- KeyProofs is self-hostable, meaning you could put it on any server you trust + +### Secure and privacy-friendly + +- KeyProofs doesn't want your personal data, track you or show you ads +- KeyProofs relies on OpenPGP, a widely used public-key cryptography standard (RFC-4880) +- Cryptographic operations are performed on server. +` diff --git a/pkg/keyproofs/vcard.go b/pkg/keyproofs/vcard.go index 790c331..b421c19 100644 --- a/pkg/keyproofs/vcard.go +++ b/pkg/keyproofs/vcard.go @@ -6,6 +6,7 @@ import ( "fmt" "github.com/rs/zerolog/log" + "github.com/sour-is/keyproofs/pkg/graceful" "gosrc.io/xmpp" "gosrc.io/xmpp/stanza" ) @@ -40,28 +41,34 @@ func init() { } type connection struct { - client *xmpp.Client + client xmpp.StreamClient } func NewXMPP(ctx context.Context, config *xmpp.Config) (*connection, error) { log := log.Ctx(ctx) + wg := graceful.WaitGroup(ctx) + router := xmpp.NewRouter() conn := &connection{} - var err error - conn.client, err = xmpp.NewClient(config, router, func(err error) { log.Error().Err(err).Send() }) + cl, err := xmpp.NewClient(config, router, func(err error) { log.Error().Err(err).Send() }) if err != nil { return nil, err } + sc := xmpp.NewStreamManager(cl, func(c xmpp.Sender) { log.Info().Msg("XMPP Client connected.") }) + + wg.Go(func() error { + log.Debug().Msg("starting XMPP") + return sc.Run() + }) go func() { <-ctx.Done() - err := conn.client.Disconnect() - log.Error().Err(err).Send() + sc.Stop() + log.Info().Msg("XMPP Client shutdown.") }() - err = conn.client.Connect() - + conn.client = cl return conn, err }