diff --git a/go.mod b/go.mod index eb25b76..af71e5e 100644 --- a/go.mod +++ b/go.mod @@ -12,6 +12,8 @@ require ( github.com/gomarkdown/markdown v0.0.0-20200824053859-8c8b3816f167 github.com/gorilla/mux v1.8.0 github.com/h2non/filetype v1.1.0 + github.com/hashicorp/golang-lru v0.5.1 + github.com/lucasb-eyer/go-colorful v1.0.3 github.com/remyoudompheng/go-liblzma v0.0.0-20190506200333-81bf2d431b96 github.com/sour-is/crypto v0.0.0-20201016232853-f42a24ba5a81 github.com/sour-is/go-assetfs v1.0.0 @@ -19,6 +21,8 @@ require ( github.com/tv42/zbase32 v0.0.0-20190604154422-aacc64a8f915 github.com/vektah/dataloaden v0.3.0 go.etcd.io/bbolt v1.3.5 // indirect + go.uber.org/ratelimit v0.1.0 + golang.org/x/crypto v0.0.0-20200709230013-948cd5f35899 golang.org/x/sys v0.0.0-20200831180312-196b9ba8737a sour.is/x/toolbox v0.12.17 ) diff --git a/go.sum b/go.sum index cd7c026..2609341 100644 --- a/go.sum +++ b/go.sum @@ -327,6 +327,7 @@ github.com/lann/builder v0.0.0-20180802200727-47ae307949d0/go.mod h1:dXGbAdH5GtB 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/logrusorgru/aurora v0.0.0-20200102142835-e9ef32dff381/go.mod h1:7rIyQOR62GCctdiQpZ/zOJlFyk6y+94wXzv6RNZgaR4= +github.com/lucasb-eyer/go-colorful v1.0.3/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= github.com/magiconair/properties v1.7.4-0.20170902060319-8d7837e64d3c/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= github.com/magiconair/properties v1.8.1 h1:ZC2Vc7/ZFkGmsVC9KvOjumD+G5lXy2RtTKyzRKO2BQ4= @@ -523,6 +524,8 @@ go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= 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/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= golang.org/dl v0.0.0-20190829154251-82a15e2f2ead/go.mod h1:IUMfjQLJQd4UTqG1Z90tenwKoCX93Gn3MAQJMOSBsDQ= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= diff --git a/src/pkg/cache/cache.go b/src/pkg/cache/cache.go new file mode 100644 index 0000000..ff2f822 --- /dev/null +++ b/src/pkg/cache/cache.go @@ -0,0 +1,77 @@ +package cache + +import ( + "time" + + lru "github.com/hashicorp/golang-lru" +) + +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) + Has(Key) bool + Get(Key) (Value, bool) + Remove(Key) +} + +type arcCache struct { + cache *lru.ARCCache +} + +func NewARC(size int) (Cacher, error) { + arc, err := lru.NewARC(size) + if err != nil { + return nil, err + } + + return &arcCache{cache: arc}, nil +} +func (c *arcCache) Add(key Key, value Value) { + c.cache.Add(key.Key(), value) +} +func (c *arcCache) 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 *arcCache) Has(key Key) bool { + _, ok := c.Get(key) + return ok +} +func (c *arcCache) Remove(key Key) { + c.cache.Remove(key.Key()) +} diff --git a/src/pkg/promise/promise.go b/src/pkg/promise/promise.go new file mode 100644 index 0000000..6b607b3 --- /dev/null +++ b/src/pkg/promise/promise.go @@ -0,0 +1,223 @@ +package promise + +import ( + "context" + "fmt" + "sync" + "time" + + "go.uber.org/ratelimit" + "sour.is/x/paste/src/pkg/cache" + "sour.is/x/toolbox/log" +) + +type Q interface { + Key() interface{} + Context() context.Context + Resolve(interface{}) + Reject(error) + + Tasker +} +type Fn func(Q) +type Key interface { + Key() interface{} +} + +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() } + +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 { + tr.mu.RLock() + log.Infos("task to run", fmt.Sprintf("%T", key), key.Key()) + + if task, ok := tr.queue[key.Key()]; ok { + tr.mu.RUnlock() + log.Infos("task found running", fmt.Sprintf("%T", key), key.Key()) + + 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 { + task.err = fmt.Errorf("PANIC: %v", r) + } + + if err := task.Err(); err == nil { + log.Infos("task complete", fmt.Sprintf("%T", task.Key()), task.Key()) + } else { + log.Errors("task Failed", fmt.Sprintf("%T", task.Key()), task.Key(), "err", err) + } + }() + + log.Infos("task Running", fmt.Sprintf("%T", task.Key()), task.Key()) + + task.fn(task) + + tr.mu.Lock() + delete(tr.queue, task.Key()) + tr.mu.Unlock() + }() + + 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(1), + } + + 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) +} + +func WithCache(c cache.Cacher, expireAfter time.Duration) OptionFn { + return func(task *qTask) { + innerFn := task.fn + task.fn = func(q Q) { + cacheKey, ok := q.Key().(cache.Key) + if !ok { + log.Infos("not a cache key", fmt.Sprintf("%T", q.Key()), q.Key()) + innerFn(q) + + return + } + + if v, ok := c.Get(cacheKey); ok { + log.Infos("value in cache", fmt.Sprintf("%T", cacheKey), cacheKey.Key()) + q.Resolve(v.Value()) + + return + } + + log.Infos("not in cache", fmt.Sprintf("%T", cacheKey), cacheKey.Key()) + innerFn(q) + + if err := task.Err(); err != nil { + log.Error(err) + + return + } + + result := cache.NewItem(cacheKey, task.Result(), expireAfter) + + log.Infos("result to cache", fmt.Sprintf("%T", cacheKey), cacheKey.Key()) + c.Add(cacheKey, result) + } + } +} diff --git a/src/routes/artifact.go b/src/routes/artifact.go index 8e2ca18..15af559 100644 --- a/src/routes/artifact.go +++ b/src/routes/artifact.go @@ -97,7 +97,7 @@ func (a *Artifact) get(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", mime) w.Header().Set("X-Content-Type-Options", "nosniff") - io.Copy(w, pr.Drain()) + _, _ = io.Copy(w, pr.Drain()) return } diff --git a/src/routes/identity.go b/src/routes/identity.go new file mode 100644 index 0000000..ce134fb --- /dev/null +++ b/src/routes/identity.go @@ -0,0 +1,635 @@ +package routes + +import ( + "bytes" + "context" + "crypto/md5" + "crypto/sha1" + "fmt" + "io" + "net" + "net/http" + "net/mail" + "net/url" + "strings" + "text/template" + "time" + + "github.com/gorilla/mux" + "github.com/lucasb-eyer/go-colorful" + "github.com/sour-is/crypto/openpgp" + "github.com/tv42/zbase32" + "golang.org/x/crypto/openpgp/armor" + + "sour.is/x/paste/src/pkg/cache" + "sour.is/x/paste/src/pkg/promise" + "sour.is/x/toolbox/httpsrv" + "sour.is/x/toolbox/log" +) + +var expireAfter = 20 * time.Minute + +func init() { + cache, err := cache.NewARC(2048) + if err != nil { + panic(err) + } + tasker := promise.NewRunner(context.TODO(), promise.Timeout(30*time.Second), promise.WithCache(cache, expireAfter)) + + s := &identity{ + cache: cache, + tasker: tasker, + } + + httpsrv.RegisterModule("identity", s.config) + + httpsrv.HttpRegister("identity", httpsrv.HttpRoutes{ + {Name: "get", Method: "GET", Pattern: "/id/{id}", HandlerFunc: s.get}, + }) +} + +var pixl = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII=" + +var defaultStyle = &Style{ + Avatar: pixl, + Cover: pixl, + Background: pixl, + Palette: getPalette("#93CCEA"), +} + +type identity struct { + cache cache.Cacher + tasker promise.Tasker +} + +type page struct { + Entity *Entity + Style *Style + Proofs *Proofs + + IsComplete bool + Err error +} +type Proofs map[string]*Proof + +func (s *identity) config(config map[string]string) {} + +// func (s *identity) runtoCache() + +func (s *identity) get(w http.ResponseWriter, r *http.Request) { + secHeaders(w) + + vars := mux.Vars(r) + id := vars["id"] + + ctx, cancel := context.WithTimeout(r.Context(), 2*time.Second) + defer cancel() + + task := s.tasker.Run(EntityKey(id), func(q promise.Q) { + ctx := q.Context() + key := q.Key().(EntityKey) + + log.Infos("start task", fmt.Sprintf("%T", key), key) + + entity, err := s.getOpenPGPkey(ctx, string(key)) + if err != nil { + q.Reject(err) + return + } + + q.Run(StyleKey(entity.Primary.Address), func(q promise.Q) { + ctx := q.Context() + key := q.Key().(StyleKey) + + log.Infos("start task", fmt.Sprintf("%T", key), key) + style, err := s.getStyle(ctx, string(key)) + if err != nil { + q.Reject(err) + return + } + + q.Resolve(style) + }) + + for i := range entity.Proofs { + q.Run(ProofKey(entity.Proofs[i]), func(q promise.Q) { + proof := NewProof(entity.Proofs[i]) + + q.Resolve(proof) + }) + } + + q.Resolve(entity) + }) + + page := page{Style: defaultStyle} + + select { + case <-task.Await(): + log.Info("Tasks Competed") + if err := task.Err(); err != nil { + page.Err = err + page.IsComplete = true + break + } + page.Entity = task.Result().(*Entity) + + case <-ctx.Done(): + log.Info("Deadline Timeout") + if e, ok := s.cache.Get(EntityKey(id)); ok { + page.Entity = e.Value().(*Entity) + } + } + + 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 + } + + // TODO: Proofs + gotProofs = true + if len(page.Entity.Proofs) > 0 { + proofs := make(Proofs, len(page.Entity.Proofs)) + for _, p := range page.Entity.Proofs { + proofs[p] = NewProof(p) + if s, ok := s.cache.Get(ProofKey(p)); ok { + proofs[p] = s.Value().(*Proof) + } else { + gotProofs = false + } + } + page.Proofs = &proofs + } + + page.IsComplete = gotStyle && gotProofs + } + + // e := json.NewEncoder(w) + // e.SetIndent("", " ") + // e.Encode(entity) + + t, err := template.New("identity").Parse(identityTPL) + if err != nil { + httpsrv.WriteText(w, 500, err.Error()) + return + } + err = t.Execute(w, page) + if err != nil { + httpsrv.WriteText(w, 500, err.Error()) + return + } +} + +func (s *identity) getOpenPGPkey(ctx context.Context, id string) (entity *Entity, err error) { + useArmored := false + addr := "" + + if isFingerprint(id) { + addr = "https://keys.openpgp.org/vks/v1/by-fingerprint/" + strings.ToUpper(id) + useArmored = true + } else if email, err := mail.ParseAddress(id); err == nil { + addr = getWKDPubKeyAddr(email) + useArmored = false + } else { + return entity, fmt.Errorf("Parse address: %w", err) + } + + req, err := http.NewRequestWithContext(ctx, http.MethodGet, addr, nil) + if err != nil { + return entity, err + } + cl := http.Client{} + resp, err := cl.Do(req) + if err != nil { + return entity, fmt.Errorf("Requesting key: %w\nRemote URL: %v", err, addr) + } + + if resp.StatusCode != 200 { + return entity, fmt.Errorf("bad response from remote: %s\nRemote URL: %v", resp.Status, addr) + } + + defer resp.Body.Close() + + if resp.Header.Get("Content-Type") == "application/pgp-keys" { + useArmored = true + } + log.Infos("getIdentity", "id", id, "useArmored", useArmored, "status", resp.Status, "addr", addr) + + entity, err = ReadKey(resp.Body, useArmored) + + return entity, err +} + +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 + } + + 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 + + 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 func() { aw.Close(); e.ArmorText = buf.String() }() + w = aw + } else { + defer func() { 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) +} + +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 = pixl + + 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 = "cdn.libravatar.org" + + 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) +} + +type ProofKey string + +func (k ProofKey) Key() interface{} { + return k +} + +type Proof struct { + Icon string + Service string + Name string + URI string + Link string + Checked bool + Verified bool +} + +func NewProof(uri string) *Proof { + p := &Proof{URI: uri} + + u, err := url.Parse(uri) + if err != nil { + p.Icon = "exclamation-triangle" + p.Service = "error" + p.Name = err.Error() + + return p + } + + p.Service = u.Scheme + + switch u.Scheme { + case "dns": + p.Icon = "link" + p.Name = u.Opaque + p.Link = fmt.Sprintf("https://%s", u.Hostname()) + + case "https": + p.Icon = "link" + p.Name = u.Hostname() + p.Link = fmt.Sprintf("https://%s", u.Hostname()) + + case "xmpp": + p.Icon = "comments" + p.Name = u.Hostname() + } + + return p +} + +func secHeaders(w http.ResponseWriter) { + 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("Content-Security-Policy", "default-src 'self';") + w.Header().Set("X-Content-Type-Options", "nosniff") +} + +var identityTPL = ` + +
+ {{if not .IsComplete}}{{end}} + + + + + + + + +{{ with .Style }} + +{{end}} + + + +{{.}}+
{{.Fingerprint}}
+