Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 53 additions & 0 deletions images/wordpress/workspace-init/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ package main
import (
"context"
"fmt"
"net"
"net/url"
"os"
"strings"
"time"
Expand All @@ -27,8 +29,54 @@ import (
const (
envAPIURL = "PORTAL_API_URL"
envAPIKey = "PORTAL_API_KEY"

// dashboardAPISubdomain is the host subdomain behind which the dashboard
// API's key-exchange routes (POST /api/auth/key, GET /api/account) are
// routed. It mirrors the value pinned in portal-plugin-ipfs; the dashboard
// core package does not export it.
dashboardAPISubdomain = "account"
)

// deriveDashboardAPIURL normalizes the injected PORTAL_API_URL so requests
// always target the dashboard API host. Older portal deployments inject the
// bare core domain (e.g. https://pinner.xyz) or this plugin's subdomain
// (e.g. https://ipfs.pinner.xyz), but the key-exchange routes only exist
// behind the dashboard's host router (account.<core domain>); hitting any
// other host yields 405 and skips automatic install. Loopback/IP hosts and
// hosts already rooted at the dashboard subdomain are returned unchanged.
// An unparseable URL is returned as-is; the failure then surfaces from the
// client with full context.
func deriveDashboardAPIURL(rawURL string) string {
u, err := url.Parse(rawURL)
if err != nil || u.Host == "" {
return rawURL
}

host := strings.ToLower(u.Hostname())
// IPs and dotless hosts (e.g. "localhost") have no derivable core domain;
// treat them as already-final dev/test targets.
if net.ParseIP(host) != nil || !strings.Contains(host, ".") {
return rawURL
}

labels := strings.Split(host, ".")
if labels[0] == dashboardAPISubdomain {
return rawURL
}

// Drop any leftmost plugin subdomain so an apex URL and a subdomain URL
// both reduce to the core domain, then prepend the dashboard subdomain:
// pinner.xyz / ipfs.pinner.xyz -> account.pinner.xyz.
core := strings.Join(labels[len(labels)-2:], ".")

schemeHost := dashboardAPISubdomain + "." + core
if port := u.Port(); port != "" {
schemeHost = net.JoinHostPort(schemeHost, port)
}
u.Host = schemeHost
return u.String()
}

func main() {
email, err := fetchEmail(context.Background())
if err != nil {
Expand All @@ -53,6 +101,11 @@ func fetchEmail(ctx context.Context) (string, error) {
return "", fmt.Errorf("%s is required", envAPIKey)
}

// Older portal deployments inject the bare core domain or the plugin's own
// subdomain as PORTAL_API_URL; the key-exchange routes only exist behind
// the dashboard API's host, so point the client there.
apiURL = deriveDashboardAPIURL(apiURL)

// Bound the whole portal round-trip so a hung/unreachable portal cannot
// stall the container's init forever; wp-init treats a failure here as
// "email unavailable" and safely skips automatic install.
Expand Down
30 changes: 28 additions & 2 deletions images/wordpress/workspace-init/main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -141,8 +141,34 @@ func TestFetchEmailBadCredentials(t *testing.T) {
}
}

// writeJSON sets the JSON content type the oapi-codegen client expects when it
// decodes a JSON200 body, then encodes v.
// TestDeriveDashboardAPIURL pins the normalization of the injected
// PORTAL_API_URL: apex and plugin-subdomain hosts must be rewritten onto the
// dashboard API host, hosts already at the dashboard subdomain and dev/test
// loopback targets pass through, and scheme/port are preserved.
func TestDeriveDashboardAPIURL(t *testing.T) {
cases := []struct {
name string
in string
want string
}{
{"apex core domain", "https://pinner.xyz", "https://account.pinner.xyz"},
{"plugin subdomain", "https://ipfs.pinner.xyz", "https://account.pinner.xyz"},
{"already dashboard host", "https://account.pinner.xyz", "https://account.pinner.xyz"},
{"port preserved", "https://ipfs.pinner.xyz:8443", "https://account.pinner.xyz:8443"},
{"uppercase host", "https://Pinner.XYZ", "https://account.pinner.xyz"},
{"ipv4 passthrough", "http://127.0.0.1:8080", "http://127.0.0.1:8080"},
{"localhost passthrough", "http://localhost:3000", "http://localhost:3000"},
{"trailing path preserved", "https://pinner.xyz/api", "https://account.pinner.xyz/api"},
{"unparseable passthrough", "::::", "::::"},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
if got := deriveDashboardAPIURL(tc.in); got != tc.want {
t.Fatalf("deriveDashboardAPIURL(%q) = %q, want %q", tc.in, got, tc.want)
}
})
}
}
func writeJSON(w http.ResponseWriter, v any) {
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(v)
Expand Down
Loading