derefinstance.go (9632B)
1 // GoToSocial 2 // Copyright (C) GoToSocial Authors admin@gotosocial.org 3 // SPDX-License-Identifier: AGPL-3.0-or-later 4 // 5 // This program is free software: you can redistribute it and/or modify 6 // it under the terms of the GNU Affero General Public License as published by 7 // the Free Software Foundation, either version 3 of the License, or 8 // (at your option) any later version. 9 // 10 // This program is distributed in the hope that it will be useful, 11 // but WITHOUT ANY WARRANTY; without even the implied warranty of 12 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 // GNU Affero General Public License for more details. 14 // 15 // You should have received a copy of the GNU Affero General Public License 16 // along with this program. If not, see <http://www.gnu.org/licenses/>. 17 18 package transport 19 20 import ( 21 "context" 22 "encoding/json" 23 "errors" 24 "fmt" 25 "io" 26 "net/http" 27 "net/url" 28 "strings" 29 30 apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model" 31 apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util" 32 "github.com/superseriousbusiness/gotosocial/internal/gtserror" 33 "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" 34 "github.com/superseriousbusiness/gotosocial/internal/id" 35 "github.com/superseriousbusiness/gotosocial/internal/log" 36 "github.com/superseriousbusiness/gotosocial/internal/util" 37 "github.com/superseriousbusiness/gotosocial/internal/validate" 38 ) 39 40 func (t *transport) DereferenceInstance(ctx context.Context, iri *url.URL) (*gtsmodel.Instance, error) { 41 var i *gtsmodel.Instance 42 var err error 43 44 // First try to dereference using /api/v1/instance. 45 // This will provide the most complete picture of an instance, and avoid unnecessary api calls. 46 // 47 // This will only work with Mastodon-api compatible instances: Mastodon, some Pleroma instances, GoToSocial. 48 log.Debugf(ctx, "trying to dereference instance %s by /api/v1/instance", iri.Host) 49 i, err = dereferenceByAPIV1Instance(ctx, t, iri) 50 if err == nil { 51 log.Debugf(ctx, "successfully dereferenced instance using /api/v1/instance") 52 return i, nil 53 } 54 log.Debugf(ctx, "couldn't dereference instance using /api/v1/instance: %s", err) 55 56 // If that doesn't work, try to dereference using /.well-known/nodeinfo. 57 // This will involve two API calls and return less info overall, but should be more widely compatible. 58 log.Debugf(ctx, "trying to dereference instance %s by /.well-known/nodeinfo", iri.Host) 59 i, err = dereferenceByNodeInfo(ctx, t, iri) 60 if err == nil { 61 log.Debugf(ctx, "successfully dereferenced instance using /.well-known/nodeinfo") 62 return i, nil 63 } 64 log.Debugf(ctx, "couldn't dereference instance using /.well-known/nodeinfo: %s", err) 65 66 // we couldn't dereference the instance using any of the known methods, so just return a minimal representation 67 log.Debugf(ctx, "returning minimal representation of instance %s", iri.Host) 68 id, err := id.NewRandomULID() 69 if err != nil { 70 return nil, fmt.Errorf("error creating new id for instance %s: %s", iri.Host, err) 71 } 72 73 return >smodel.Instance{ 74 ID: id, 75 Domain: iri.Host, 76 URI: iri.String(), 77 }, nil 78 } 79 80 func dereferenceByAPIV1Instance(ctx context.Context, t *transport, iri *url.URL) (*gtsmodel.Instance, error) { 81 cleanIRI := &url.URL{ 82 Scheme: iri.Scheme, 83 Host: iri.Host, 84 Path: "api/v1/instance", 85 } 86 87 // Build IRI just once 88 iriStr := cleanIRI.String() 89 90 req, err := http.NewRequestWithContext(ctx, "GET", iriStr, nil) 91 if err != nil { 92 return nil, err 93 } 94 95 req.Header.Add("Accept", string(apiutil.AppJSON)) 96 req.Header.Set("Host", cleanIRI.Host) 97 98 resp, err := t.GET(req) 99 if err != nil { 100 return nil, err 101 } 102 defer resp.Body.Close() 103 104 if resp.StatusCode != http.StatusOK { 105 return nil, gtserror.NewFromResponse(resp) 106 } 107 108 b, err := io.ReadAll(resp.Body) 109 if err != nil { 110 return nil, err 111 } else if len(b) == 0 { 112 return nil, errors.New("response bytes was len 0") 113 } 114 115 // try to parse the returned bytes directly into an Instance model 116 apiResp := &apimodel.InstanceV1{} 117 if err := json.Unmarshal(b, apiResp); err != nil { 118 return nil, err 119 } 120 121 var contactUsername string 122 if apiResp.ContactAccount != nil { 123 contactUsername = apiResp.ContactAccount.Username 124 } 125 126 ulid, err := id.NewRandomULID() 127 if err != nil { 128 return nil, err 129 } 130 131 i := >smodel.Instance{ 132 ID: ulid, 133 Domain: iri.Host, 134 Title: apiResp.Title, 135 URI: iri.Scheme + "://" + iri.Host, 136 ShortDescription: apiResp.ShortDescription, 137 Description: apiResp.Description, 138 ContactEmail: apiResp.Email, 139 ContactAccountUsername: contactUsername, 140 Version: apiResp.Version, 141 } 142 143 return i, nil 144 } 145 146 func dereferenceByNodeInfo(c context.Context, t *transport, iri *url.URL) (*gtsmodel.Instance, error) { 147 niIRI, err := callNodeInfoWellKnown(c, t, iri) 148 if err != nil { 149 return nil, fmt.Errorf("dereferenceByNodeInfo: error during initial call to well-known nodeinfo: %s", err) 150 } 151 152 ni, err := callNodeInfo(c, t, niIRI) 153 if err != nil { 154 return nil, fmt.Errorf("dereferenceByNodeInfo: error doing second call to nodeinfo uri %s: %s", niIRI.String(), err) 155 } 156 157 // we got a response of some kind! take what we can from it... 158 id, err := id.NewRandomULID() 159 if err != nil { 160 return nil, fmt.Errorf("dereferenceByNodeInfo: error creating new id for instance %s: %s", iri.Host, err) 161 } 162 163 // this is the bare minimum instance we'll return, and we'll add more stuff to it if we can 164 i := >smodel.Instance{ 165 ID: id, 166 Domain: iri.Host, 167 URI: iri.String(), 168 } 169 170 var title string 171 if i, present := ni.Metadata["nodeName"]; present { 172 // it's present, check it's a string 173 if v, ok := i.(string); ok { 174 // it is a string! 175 title = v 176 } 177 } 178 i.Title = title 179 180 var shortDescription string 181 if i, present := ni.Metadata["nodeDescription"]; present { 182 // it's present, check it's a string 183 if v, ok := i.(string); ok { 184 // it is a string! 185 shortDescription = v 186 } 187 } 188 i.ShortDescription = shortDescription 189 190 var contactEmail string 191 var contactAccountUsername string 192 if i, present := ni.Metadata["maintainer"]; present { 193 // it's present, check it's a map 194 if v, ok := i.(map[string]string); ok { 195 // see if there's an email in the map 196 if email, present := v["email"]; present { 197 if err := validate.Email(email); err == nil { 198 // valid email address 199 contactEmail = email 200 } 201 } 202 // see if there's a 'name' in the map 203 if name, present := v["name"]; present { 204 // name could be just a username, or could be a mention string eg @whatever@aaaa.com 205 username, _, err := util.ExtractNamestringParts(name) 206 if err == nil { 207 // it was a mention string 208 contactAccountUsername = username 209 } else { 210 // not a mention string 211 contactAccountUsername = name 212 } 213 } 214 } 215 } 216 i.ContactEmail = contactEmail 217 i.ContactAccountUsername = contactAccountUsername 218 219 var software string 220 if ni.Software.Name != "" { 221 software = ni.Software.Name 222 } 223 if ni.Software.Version != "" { 224 software = software + " " + ni.Software.Version 225 } 226 i.Version = software 227 228 return i, nil 229 } 230 231 func callNodeInfoWellKnown(ctx context.Context, t *transport, iri *url.URL) (*url.URL, error) { 232 cleanIRI := &url.URL{ 233 Scheme: iri.Scheme, 234 Host: iri.Host, 235 Path: ".well-known/nodeinfo", 236 } 237 238 // Build IRI just once 239 iriStr := cleanIRI.String() 240 241 req, err := http.NewRequestWithContext(ctx, "GET", iriStr, nil) 242 if err != nil { 243 return nil, err 244 } 245 req.Header.Add("Accept", string(apiutil.AppJSON)) 246 req.Header.Set("Host", cleanIRI.Host) 247 248 resp, err := t.GET(req) 249 if err != nil { 250 return nil, err 251 } 252 defer resp.Body.Close() 253 254 if resp.StatusCode != http.StatusOK { 255 return nil, gtserror.NewFromResponse(resp) 256 } 257 258 b, err := io.ReadAll(resp.Body) 259 if err != nil { 260 return nil, err 261 } else if len(b) == 0 { 262 return nil, errors.New("callNodeInfoWellKnown: response bytes was len 0") 263 } 264 265 wellKnownResp := &apimodel.WellKnownResponse{} 266 if err := json.Unmarshal(b, wellKnownResp); err != nil { 267 return nil, fmt.Errorf("callNodeInfoWellKnown: could not unmarshal server response as WellKnownResponse: %s", err) 268 } 269 270 // look through the links for the first one that matches the nodeinfo schema, this is what we need 271 var nodeinfoHref *url.URL 272 for _, l := range wellKnownResp.Links { 273 if l.Href == "" || !strings.HasPrefix(l.Rel, "http://nodeinfo.diaspora.software/ns/schema/2") { 274 continue 275 } 276 nodeinfoHref, err = url.Parse(l.Href) 277 if err != nil { 278 return nil, fmt.Errorf("callNodeInfoWellKnown: couldn't parse url %s: %s", l.Href, err) 279 } 280 } 281 if nodeinfoHref == nil { 282 return nil, errors.New("callNodeInfoWellKnown: could not find nodeinfo rel in well known response") 283 } 284 285 return nodeinfoHref, nil 286 } 287 288 func callNodeInfo(ctx context.Context, t *transport, iri *url.URL) (*apimodel.Nodeinfo, error) { 289 // Build IRI just once 290 iriStr := iri.String() 291 292 req, err := http.NewRequestWithContext(ctx, "GET", iriStr, nil) 293 if err != nil { 294 return nil, err 295 } 296 req.Header.Add("Accept", string(apiutil.AppJSON)) 297 req.Header.Set("Host", iri.Host) 298 299 resp, err := t.GET(req) 300 if err != nil { 301 return nil, err 302 } 303 defer resp.Body.Close() 304 305 if resp.StatusCode != http.StatusOK { 306 return nil, gtserror.NewFromResponse(resp) 307 } 308 309 b, err := io.ReadAll(resp.Body) 310 if err != nil { 311 return nil, err 312 } else if len(b) == 0 { 313 return nil, errors.New("callNodeInfo: response bytes was len 0") 314 } 315 316 niResp := &apimodel.Nodeinfo{} 317 if err := json.Unmarshal(b, niResp); err != nil { 318 return nil, fmt.Errorf("callNodeInfo: could not unmarshal server response as Nodeinfo: %s", err) 319 } 320 321 return niResp, nil 322 }