package shell import ( "context" "encoding/json" "errors" "fmt" "io" "net/http" "net/url" "strings" ) // Discover turns what someone typed on the first-run page into a homeserver // base URL, the way Matrix clients do (spec: client-server API, server // discovery). The input is a server name ("example.org") or a URL; a // /.well-known/matrix/client on that host may delegate to another base URL; // and whatever comes out has to answer /_matrix/client/versions. // // The errors are written for the person who typed the input. func Discover(ctx context.Context, c *http.Client, input string) (string, error) { input = strings.TrimSpace(input) if input == "" { return "", errors.New("Enter a server name, like example.org, or its address.") } raw := input if !strings.Contains(raw, "://") { raw = "https://" + raw } u, err := parseBase(raw) if err != nil { return "", fmt.Errorf("“%s” isn’t a server name or address.", input) } delegated, err := wellKnown(ctx, c, u) if err != nil { return "", err } if delegated == nil || *delegated == *u { if err := checkVersions(ctx, c, u); err != nil { return "", err } return u.String(), nil } err = checkVersions(ctx, c, delegated) if err == nil { return delegated.String(), nil } // A server whose well-known names an address that doesn't work (a // development server advertising its public name, say) is still // usable at the address that was typed. if checkVersions(ctx, c, u) == nil { return u.String(), nil } return "", fmt.Errorf("%s points to %s. %w", u.Host, delegated, err) } // parseBase accepts an absolute http(s) URL with no query or fragment and // returns it without a trailing slash. func parseBase(raw string) (*url.URL, error) { u, err := url.Parse(raw) if err != nil { return nil, err } if (u.Scheme != "http" && u.Scheme != "https") || u.Host == "" || u.User != nil || u.RawQuery != "" || u.Fragment != "" { return nil, errors.New("not an http(s) base URL") } u.Path = strings.TrimRight(u.Path, "/") u.RawPath = "" return u, nil } // wellKnown returns the base URL the host delegates to, or nil when it // doesn't say (no document, or one without m.homeserver). A host that can't // be reached at all is not an error here: the versions check reports it. func wellKnown(ctx context.Context, c *http.Client, u *url.URL) (*url.URL, error) { wk := url.URL{Scheme: u.Scheme, Host: u.Host, Path: "/.well-known/matrix/client"} body, status, err := get(ctx, c, wk.String()) if err != nil || status != http.StatusOK { return nil, nil } var doc struct { Homeserver *struct { BaseURL string `json:"base_url"` } `json:"m.homeserver"` } if json.Unmarshal(body, &doc) != nil || doc.Homeserver == nil { return nil, nil } base, err := parseBase(doc.Homeserver.BaseURL) if err != nil { return nil, fmt.Errorf("%s points to “%s”, which isn’t a valid homeserver address.", u.Host, doc.Homeserver.BaseURL) } return base, nil } func checkVersions(ctx context.Context, c *http.Client, base *url.URL) error { body, status, err := get(ctx, c, base.String()+"/_matrix/client/versions") if err != nil { if ctx.Err() != nil { return fmt.Errorf("%s took too long to answer.", base.Host) } return fmt.Errorf("Can’t reach %s.", base.Host) } var v struct { Versions []string `json:"versions"` } if status != http.StatusOK || json.Unmarshal(body, &v) != nil || len(v.Versions) == 0 { return fmt.Errorf("%s doesn’t look like a Matrix homeserver.", base.Host) } return nil } func get(ctx context.Context, c *http.Client, u string) ([]byte, int, error) { req, err := http.NewRequestWithContext(ctx, http.MethodGet, u, nil) if err != nil { return nil, 0, err } req.Header.Set("Accept", "application/json") res, err := c.Do(req) if err != nil { return nil, 0, err } defer res.Body.Close() body, err := io.ReadAll(io.LimitReader(res.Body, 64<<10)) return body, res.StatusCode, err }