diff --git a/appview/config/config.go b/appview/config/config.go --- a/appview/config/config.go +++ b/appview/config/config.go @@ -3,6 +3,7 @@ import ( "context" "fmt" + "net" "net/url" "strings" "time" @@ -38,6 +39,15 @@ if c.UseTLS() { return "https://" + c.AppviewHost } return "http://" + c.AppviewHost +} + +// Hostname returns AppviewHost with any port stripped, for consumers that need +// a bare host (e.g. an ssh destination) rather than a URL authority. +func (c *CoreConfig) Hostname() string { + if host, _, err := net.SplitHostPort(c.AppviewHost); err == nil { + return host + } + return c.AppviewHost } type OAuthConfig struct { diff --git a/appview/config/config_test.go b/appview/config/config_test.go --- a/appview/config/config_test.go +++ b/appview/config/config_test.go @@ -30,3 +30,20 @@ if cfg.Knot.Default != "kt.tngl.oyster.cafe" { t.Fatalf("TANGLED_KNOT_DEFAULT override = %q, want kt.tngl.oyster.cafe", cfg.Knot.Default) } } + +func TestHostname_StripsPort(t *testing.T) { + cases := map[string]string{ + "127.0.0.1:3000": "127.0.0.1", + "localhost:3000": "localhost", + "tangled.org": "tangled.org", + "[::1]:3000": "::1", + } + for host, want := range cases { + t.Run(host, func(t *testing.T) { + c := &CoreConfig{AppviewHost: host} + if got := c.Hostname(); got != want { + t.Fatalf("Hostname() with AppviewHost=%q = %q, want %q", host, got, want) + } + }) + } +} diff --git a/appview/oauth/oauth.go b/appview/oauth/oauth.go --- a/appview/oauth/oauth.go +++ b/appview/oauth/oauth.go @@ -102,15 +102,15 @@ } func New(config *config.Config, ph posthog.Client, db *db.DB, enforcer *rbac.Enforcer, acl KnotMembership, res *idresolver.Resolver, logger *slog.Logger) (*OAuth, error) { var oauthConfig oauth.ClientConfig - var clientUri string + clientUri := config.Core.BaseUrl() + callbackUri := clientUri + "/oauth/callback" if config.Core.Dev { - clientUri = "http://127.0.0.1:3000" - callbackUri := clientUri + "/oauth/callback" + if config.Core.Hostname() == "localhost" { + logger.Warn("dev OAuth requires a loopback IP host; use 127.0.0.1 instead of 'localhost'", "host", config.Core.AppviewHost) + } oauthConfig = oauth.NewLocalhostConfig(callbackUri, TangledScopes) } else { - clientUri = "https://" + config.Core.AppviewHost clientId := fmt.Sprintf("%s/oauth/client-metadata.json", clientUri) - callbackUri := clientUri + "/oauth/callback" oauthConfig = oauth.NewPublicConfig(clientId, callbackUri, TangledScopes) } diff --git a/appview/pipelines/pipelines.go b/appview/pipelines/pipelines.go --- a/appview/pipelines/pipelines.go +++ b/appview/pipelines/pipelines.go @@ -305,7 +305,7 @@ _, port, err := net.SplitHostPort(p.config.SSH.ListenAddr) if err != nil || port == "" { return "" } - return fmt.Sprintf("ssh -t -p %s %s %s %s", port, p.config.Core.AppviewHost, repoDid, sha) + return fmt.Sprintf("ssh -t -p %s %s %s %s", port, p.config.Core.Hostname(), repoDid, sha) } var upgrader = websocket.Upgrader{ diff --git a/localinfra/readme.md b/localinfra/readme.md --- a/localinfra/readme.md +++ b/localinfra/readme.md @@ -51,3 +51,5 @@ ``` This writes the image directory under `out/localinfra-spindle-images`. 5. `docker compose up` 6. AppView will be running on `127.0.0.1:3000` with two test users: `alice.pds.tngl.boltless.dev` and `bob.pds.tngl.boltless.dev`. Both with password `password`. + +`TANGLED_APPVIEW_HOST` must be a loopback IP with the mapped port (`127.0.0.1:3000`), not `localhost`: atproto's dev OAuth client requires a loopback IP for the redirect URI. If you remap the published appview port, update `TANGLED_APPVIEW_HOST` in `docker-compose.yml` to match.