1/*
2 * Copyright © 2018-2021 A Bunch Tell LLC.
3 *
4 * This file is part of WriteFreely.
5 *
6 * WriteFreely is free software: you can redistribute it and/or modify
7 * it under the terms of the GNU Affero General Public License, included
8 * in the LICENSE file in this source code package.
9 */
10
11package writefreely
12
13import (
14	"encoding/json"
15	"io/ioutil"
16	"net/http"
17	"strings"
18
19	"github.com/writeas/go-webfinger"
20	"github.com/writeas/impart"
21	"github.com/writeas/web-core/log"
22	"github.com/writefreely/writefreely/config"
23)
24
25type wfResolver struct {
26	db  *datastore
27	cfg *config.Config
28}
29
30var wfUserNotFoundErr = impart.HTTPError{http.StatusNotFound, "User not found."}
31
32func (wfr wfResolver) FindUser(username string, host, requestHost string, r []webfinger.Rel) (*webfinger.Resource, error) {
33	var c *Collection
34	var err error
35	if username == host {
36		c = instanceColl
37	} else if wfr.cfg.App.SingleUser {
38		c, err = wfr.db.GetCollectionByID(1)
39	} else {
40		c, err = wfr.db.GetCollection(username)
41	}
42	if err != nil {
43		log.Error("Unable to get blog: %v", err)
44		return nil, err
45	}
46	c.hostName = wfr.cfg.App.Host
47
48	if !c.IsInstanceColl() {
49		silenced, err := wfr.db.IsUserSilenced(c.OwnerID)
50		if err != nil {
51			log.Error("webfinger find user: check is silenced: %v", err)
52			return nil, err
53		}
54		if silenced {
55			return nil, wfUserNotFoundErr
56		}
57	}
58	if wfr.cfg.App.SingleUser {
59		// Ensure handle matches user-chosen one on single-user blogs
60		if username != c.Alias {
61			log.Info("Username '%s' is not handle '%s'", username, c.Alias)
62			return nil, wfUserNotFoundErr
63		}
64	}
65	// Only return information if site has federation enabled.
66	// TODO: enable two levels of federation? Unlisted or Public on timelines?
67	if !wfr.cfg.App.Federation {
68		return nil, wfUserNotFoundErr
69	}
70
71	res := webfinger.Resource{
72		Subject: "acct:" + username + "@" + host,
73		Aliases: []string{
74			c.CanonicalURL(),
75			c.FederatedAccount(),
76		},
77		Links: []webfinger.Link{
78			{
79				HRef: c.CanonicalURL(),
80				Type: "text/html",
81				Rel:  "https://webfinger.net/rel/profile-page",
82			},
83			{
84				HRef: c.FederatedAccount(),
85				Type: "application/activity+json",
86				Rel:  "self",
87			},
88		},
89	}
90	return &res, nil
91}
92
93func (wfr wfResolver) DummyUser(username string, hostname string, r []webfinger.Rel) (*webfinger.Resource, error) {
94	return nil, wfUserNotFoundErr
95}
96
97func (wfr wfResolver) IsNotFoundError(err error) bool {
98	return err == wfUserNotFoundErr
99}
100
101// RemoteLookup looks up a user by handle at a remote server
102// and returns the actor URL
103func RemoteLookup(handle string) string {
104	handle = strings.TrimLeft(handle, "@")
105	// let's take the server part of the handle
106	parts := strings.Split(handle, "@")
107	resp, err := http.Get("https://" + parts[1] + "/.well-known/webfinger?resource=acct:" + handle)
108	if err != nil {
109		log.Error("Error on webfinger request: %v", err)
110		return ""
111	}
112
113	body, err := ioutil.ReadAll(resp.Body)
114	if err != nil {
115		log.Error("Error on webfinger response: %v", err)
116		return ""
117	}
118
119	var result webfinger.Resource
120	err = json.Unmarshal(body, &result)
121	if err != nil {
122		log.Error("Unable to parse webfinger response: %v", err)
123		return ""
124	}
125
126	var href string
127	// iterate over webfinger links and find the one with
128	// a self "rel"
129	for _, link := range result.Links {
130		if link.Rel == "self" {
131			href = link.HRef
132		}
133	}
134
135	// if we didn't find it with the above then
136	// try using aliases
137	if href == "" {
138		// take the last alias because mastodon has the
139		// https://instance.tld/@user first which
140		// doesn't work as an href
141		href = result.Aliases[len(result.Aliases)-1]
142	}
143
144	return href
145}
146