1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
|
// Package nodeinfo contains types and a simple client for reading the standard NodeInfo protocol described by Diaspora[1].
//
// This package supports version 2.0[2] because that is the one most commonly used.
//
// [1]: http://nodeinfo.diaspora.software/
// [2]: http://nodeinfo.diaspora.software/docson/index.html#/ns/schema/2.0#$$expand
package nodeinfo
import (
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"within.website/x/web"
"within.website/x/web/useragent"
)
const schema2point0 = "http://nodeinfo.diaspora.software/ns/schema/2.0"
const twoMebibytes = 1024 * 1024 * 2
// Node is the node information you are looking for.
type Node struct {
Version string `json:"version"`
Software Software `json:"software"`
Protocols []string `json:"protocols"`
Services Services `json:"services"`
OpenRegistration bool `json:"openRegistration"`
Usage Usage `json:"usage"`
Metadata any `json:"metadata"`
}
// Software contains metadata about the server software in use.
type Software struct {
Name string `json:"name"`
Version string `json:"version"`
}
// Services lists the third party services that this server can connect to with their application API.
type Services struct {
Inbound []string `json:"inbound"`
Outbound []string `json:"outbound"`
}
// Usage statistics for this server.
type Usage struct {
Users Users `json:"users"`
LocalPosts int64 `json:"localPosts"`
}
// Users contains statistics about the users of this server.
type Users struct {
Total int64 `json:"total"`
ActiveHalfYear int64 `json:"activeHalfYear"`
ActiveMonth int64 `json:"activeMonth"`
}
type wellKnownLinks struct {
Links []wellKnownLink `json:"links"`
}
type wellKnownLink struct {
Rel string `json:"rel"`
Href string `json:"href"`
}
// FetchWithClient uses the provided HTTP client to fetch node information.
func FetchWithClient(ctx context.Context, cli *http.Client, nodeURL string) (*Node, error) {
u, err := url.Parse(nodeURL)
if err != nil {
return nil, fmt.Errorf("nodeinfo: can't parse nodeURL: %w", err)
}
u.Path = "/.well-known/nodeinfo"
req, err := http.NewRequest(http.MethodGet, u.String(), nil)
if err != nil {
return nil, fmt.Errorf("nodeinfo: can't create HTTP request: %w", err)
}
resp, err := cli.Do(req)
if err != nil {
return nil, fmt.Errorf("nodeinfo: can't read HTTP response: %w", err)
}
if resp.StatusCode != http.StatusOK {
return nil, web.NewError(http.StatusOK, resp)
}
defer resp.Body.Close()
data, err := io.ReadAll(io.LimitReader(resp.Body, twoMebibytes))
if err != nil {
return nil, fmt.Errorf("nodeinfo: can't read from HTTP response body: %w", err)
}
var niw wellKnownLinks
err = json.Unmarshal(data, &niw)
if err != nil {
return nil, fmt.Errorf("nodeinfo: can't unmarshal discovery metadata: %w", err)
}
var targetURL string
for _, link := range niw.Links {
if link.Rel == schema2point0 {
targetURL = link.Href
}
}
if targetURL == "" {
return nil, fmt.Errorf("nodeinfo: can't find schema 2.0 nodeinfo for %s", nodeURL)
}
req, err = http.NewRequest(http.MethodGet, targetURL, nil)
if err != nil {
return nil, fmt.Errorf("nodeinfo: can't create HTTP request: %w", err)
}
resp, err = cli.Do(req)
if err != nil {
return nil, fmt.Errorf("nodeinfo: can't read HTTP response: %w", err)
}
data, err = io.ReadAll(io.LimitReader(resp.Body, twoMebibytes))
if err != nil {
return nil, fmt.Errorf("nodeinfo: can't read from HTTP response body: %w", err)
}
var result Node
err = json.Unmarshal(data, &result)
if err != nil {
return nil, fmt.Errorf("nodeinfo: can't unmarshal nodeinfo: %w", err)
}
return &result, nil
}
// Fetch uses the standard library HTTP client to fetch node information.
func Fetch(ctx context.Context, nodeURL string) (*Node, error) {
cli := &http.Client{
Transport: useragent.Transport("within.website/x/web/nodeinfo", "https://within.website/.x.botinfo", http.DefaultTransport),
}
return FetchWithClient(ctx, cli, nodeURL)
}
|