gtsocial-umbx

Unnamed repository; edit this file 'description' to name the repository.
Log | Files | Refs | README | LICENSE

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 &gtsmodel.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 := &gtsmodel.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 := &gtsmodel.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 }