CI / Test (ubuntu-latest) (push) Canceled after 0s
CI / Test (windows-latest) (push) Canceled after 0s
CI / Storage tests on PostgreSQL (push) Canceled after 0s
CI / Complement (federation off) (push) Canceled after 0s
CI / golangci-lint (push) Canceled after 0s
CI / Smoke test the binary (push) Canceled after 0s
CI / Admin panel (push) Canceled after 0s
CI / Web client (push) Canceled after 0s
apps/desktop is the web client in a native window. It embeds the same build warp-web does and serves it through the same Go package, so every screen is shared. Unlike warp-web it proxies nothing: the client calls the homeserver directly, and the CSP allows the homeserver plus any https: and wss: origin for the call backend. - First run asks for the homeserver: a server name or address, with .well-known discovery and a /versions check; File > Change homeserver asks again. Signed-in sessions keep their own base URL, so a change never sends one server's token to another. - Links to other sites open in the system browser (/_desktop/links.js, added to index.html); only http, https and mailto are opened. - One instance at a time, since sessions and keys live in the webview's storage. - Dev builds open the DevTools Protocol on Windows when WARP_DESKTOP_REMOTE_DEBUGGING_PORT is set, for scripted testing. - Packaging: icon, version info and manifest in the Windows exe, a macOS .app with camera and microphone descriptions, Linux packages via nfpm. Make targets: desktop, desktop-run, desktop-test, desktop-resources, desktop-macos, desktop-linux. Web client: host and client move out of internal/ so the desktop app can import them, and host gains AnyCallOrigin. The Dockerfile, .dockerignore and the vite output path follow the move. Pinned to Wails v3.0.0-beta.27. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
129 lines
3.9 KiB
Go
129 lines
3.9 KiB
Go
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
|
||
}
|