315 lines
7.8 KiB
Go
315 lines
7.8 KiB
Go
package app_keyproofs
|
|
|
|
import (
|
|
"context"
|
|
"encoding/base64"
|
|
"fmt"
|
|
"html/template"
|
|
"net/http"
|
|
"strconv"
|
|
"time"
|
|
|
|
"github.com/go-chi/chi"
|
|
zlog "github.com/rs/zerolog/log"
|
|
"github.com/russross/blackfriday"
|
|
"github.com/skip2/go-qrcode"
|
|
|
|
"github.com/sour-is/keyproofs/pkg/cache"
|
|
"github.com/sour-is/keyproofs/pkg/config"
|
|
"github.com/sour-is/keyproofs/pkg/opgp"
|
|
"github.com/sour-is/keyproofs/pkg/opgp/entity"
|
|
"github.com/sour-is/keyproofs/pkg/promise"
|
|
"github.com/sour-is/keyproofs/pkg/style"
|
|
)
|
|
|
|
var expireAfter = 20 * time.Minute
|
|
var runnerTimeout = 30 * time.Second
|
|
|
|
// 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.Style{
|
|
Avatar: pixl,
|
|
Cover: pixl,
|
|
Background: pixl,
|
|
Palette: style.GetPalette("#93CCEA"),
|
|
}
|
|
|
|
type keyproofApp struct {
|
|
cache cache.Cacher
|
|
tasker promise.Tasker
|
|
}
|
|
|
|
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 (app *keyproofApp) getProofs(w http.ResponseWriter, r *http.Request) {
|
|
log := zlog.Ctx(r.Context())
|
|
cfg := config.FromContext(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 := app.tasker.Run(entity.Key(id), func(q promise.Q) {
|
|
ctx := q.Context()
|
|
log := zlog.Ctx(ctx).With().Interface(fmtKey(q), q.Key()).Logger()
|
|
|
|
key := q.Key().(entity.Key)
|
|
|
|
e, err := opgp.GetKey(ctx, string(key))
|
|
if err != nil {
|
|
q.Reject(err)
|
|
return
|
|
}
|
|
|
|
log.Debug().Msg("Resolving Entity")
|
|
q.Resolve(e)
|
|
})
|
|
|
|
task.After(func(q promise.ResultQ) {
|
|
entity := q.Result().(*entity.Entity)
|
|
|
|
zlog.Ctx(q.Context()).
|
|
Info().
|
|
Str("email", entity.Primary.Address).
|
|
Interface(fmtKey(q), q.Key()).
|
|
Msg("Do Style ")
|
|
|
|
q.Run(style.Key(entity.Primary.Address), func(q promise.Q) {
|
|
ctx := q.Context()
|
|
log := zlog.Ctx(ctx).With().Interface(fmtKey(q), q.Key()).Logger()
|
|
|
|
key := q.Key().(style.Key)
|
|
|
|
log.Debug().Msg("start task")
|
|
style, err := style.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.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}
|
|
page.AppName = fmt.Sprintf("%s v%s", cfg.GetString("app-name"), cfg.GetString("app-version"))
|
|
page.AppBuild = fmt.Sprintf("%s %s", cfg.GetString("build-date"), cfg.GetString("build-hash"))
|
|
|
|
// 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.Entity)
|
|
|
|
case <-ctx.Done():
|
|
log.Print("Deadline Timeout")
|
|
if e, ok := app.cache.Get(entity.Key(id)); ok {
|
|
page.Entity = e.Value().(*entity.Entity)
|
|
}
|
|
}
|
|
|
|
// Build page based on available information.
|
|
if page.Entity != nil {
|
|
var gotStyle, gotProofs bool
|
|
|
|
if s, ok := app.cache.Get(style.Key(page.Entity.Primary.Address)); ok {
|
|
page.Style = s.Value().(*style.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 := app.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.
|
|
var err error
|
|
t := template.New("page")
|
|
t, err = t.Parse(pageTPL)
|
|
if err != nil {
|
|
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())
|
|
return
|
|
}
|
|
}
|
|
func (app *keyproofApp) getHome(w http.ResponseWriter, r *http.Request) {
|
|
ctx := r.Context()
|
|
cfg := config.FromContext(ctx)
|
|
|
|
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
|
|
}
|
|
|
|
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, 500, err.Error())
|
|
return
|
|
}
|
|
|
|
t, err = t.Parse(homeTPL)
|
|
if err != nil {
|
|
writeText(w, 500, err.Error())
|
|
return
|
|
}
|
|
|
|
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")
|
|
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 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) {
|
|
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())
|
|
}
|