keyproofs/pkg/keyproofs/proofs.go
2020-12-03 12:32:24 -07:00

457 lines
11 KiB
Go

package keyproofs
import (
"bufio"
"bytes"
"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}
}
case strings.Contains(p.URI.Path, "/conv/"), strings.Contains(p.URI.Path, "/twt/"):
if sp := strings.SplitN(p.URI.Path, "/", 3); len(sp) == 3 {
p.Icon = "fas fa-comment-alt"
p.Service = "Twtxt"
p.Name = fmt.Sprintf("...@%s", p.URI.Host)
p.Link = fmt.Sprintf("https://%s", p.URI.Host)
url := fmt.Sprintf("https://%s/api/v1/conv", p.URI.Host)
return &twtxtResolve{p, url, sp[2], 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 (r *gitlabResolve) Proof() *Proof {
return &r.proof
}
func (p *Proof) Resolve(ctx context.Context) error {
return fmt.Errorf("Not Implemented")
}
func (p *Proof) Proof() *Proof {
return p
}
type twtxtResolve struct {
proof Proof `json:"-"`
url string `json:"-"`
Hash string `json:"hash"`
headers map[string]string `json:"-"`
}
func (t *twtxtResolve) Resolve(ctx context.Context) error {
t.proof.Status = ProofInvalid
twt := struct {
Twts []struct {
Text string `json:"text"`
Twter struct{ Nick string }
} `json:"twts"`
}{}
if err := postJSON(ctx, t.url, nil, t, &twt); err != nil {
return err
}
if len(twt.Twts) > 0 {
nick := twt.Twts[0].Twter.Nick
t.proof.Name = fmt.Sprintf("%s@%s", nick, t.proof.URI.Host)
t.proof.Link += "/user/" + nick
ck := fmt.Sprintf("[Verifying my OpenPGP key: openpgp4fpr:%s]", t.proof.Fingerprint)
if strings.Contains(twt.Twts[0].Text, ck) {
t.proof.Status = ProofVerified
return nil
}
}
return ErrNoFingerprint
}
func (t *twtxtResolve) Proof() *Proof {
return &t.proof
}
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)
}
func postJSON(ctx context.Context, uri string, hdr map[string]string, payload, dst interface{}) error {
log := log.Ctx(ctx)
log.Info().Str("URI", uri).Msg("postJSON")
body, err := json.Marshal(payload)
if err != nil {
log.Err(err).Send()
return err
}
buf := bytes.NewBuffer(body)
req, err := http.NewRequestWithContext(ctx, "POST", uri, buf)
if err != nil {
log.Err(err).Send()
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)
}