wireguard-ui/handler/android_assetlinks.go

106 lines
3.5 KiB
Go

package handler
import (
"encoding/json"
"fmt"
"net/http"
"strconv"
"strings"
"github.com/labstack/echo/v4"
"github.com/ngoduykhanh/wireguard-ui/util"
)
// Digital Asset Links payload for linking the Android WireGuard UI client passkeys ↔ this HTTPS host.
//
// Android Credential Manager ONLY requests https://<rpId>/.well-known/assetlinks.json at the
// host root (not under wireguard-ui BASE_PATH). If your reverse proxy routes only /wg to this
// process, GET / returns 404 and passkeys fail with "RP ID cannot be validated". Fix by:
//
// - ProxyPass (or equivalent) from host "/.well-known/assetlinks.json" to this app's same path,
// or mirror the JSON to a static file at that URL; or
// - Curl the duplicate endpoint https://<host><BASE_PATH>/.well-known/assetlinks.json (see
// main.go) and install that file under the vhost DocumentRoot /.well-known/.
//
// Ref: https://developers.google.com/digital-asset-links/v1/getting-started
type assetLinkStatement struct {
Relation []string `json:"relation"`
Target assetLinkTargetAndroid `json:"target"`
}
type assetLinkTargetAndroid struct {
Namespace string `json:"namespace"`
PackageName string `json:"package_name"`
Sha256CertFingerprints []string `json:"sha256_cert_fingerprints"`
}
func AndroidDigitalAssetLinks() echo.HandlerFunc {
return func(c echo.Context) error {
shaRaw := strings.TrimSpace(util.AndroidPasskeySHA256FingerprintsCSV())
if shaRaw == "" {
return echo.NewHTTPError(http.StatusNotFound, "digital asset links not configured (set WGUI_ANDROID_PASSKEY_SHA256)")
}
pkg := strings.TrimSpace(util.AndroidPasskeyPackageName())
if pkg == "" {
pkg = "com.wireguardui.wireguard_ui_client"
}
fpStrings := strings.Split(shaRaw, ",")
fingerprints := make([]string, 0, len(fpStrings))
for _, frag := range fpStrings {
norm, err := normalizeAndroidCertFingerprint(frag)
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("invalid WGUI_ANDROID_PASSKEY_SHA256: %v", err))
}
fingerprints = append(fingerprints, norm)
}
target := assetLinkTargetAndroid{
Namespace: "android_app",
PackageName: pkg,
Sha256CertFingerprints: fingerprints,
}
payload := []assetLinkStatement{
{Relation: []string{"delegate_permission/common.get_login_creds"}, Target: target},
{Relation: []string{"delegate_permission/common.handle_all_urls"}, Target: target},
}
b, err := json.Marshal(payload)
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, err.Error())
}
// curl -I and some proxies use HEAD; Echo only binds GET unless we reply explicitly.
if c.Request().Method == http.MethodHead {
h := c.Response().Header()
h.Set(echo.HeaderContentType, "application/json")
h.Set(echo.HeaderContentLength, strconv.Itoa(len(b)))
c.Response().WriteHeader(http.StatusOK)
return nil
}
return c.Blob(http.StatusOK, "application/json", b)
}
}
func normalizeAndroidCertFingerprint(raw string) (string, error) {
s := strings.ToLower(strings.TrimSpace(strings.ReplaceAll(raw, " ", "")))
s = strings.ReplaceAll(s, ":", "")
if len(s) != 64 {
return "", fmt.Errorf("want 64 hex chars (SHA-256), got length %d", len(s))
}
for _, r := range s {
if !(r >= '0' && r <= '9' || r >= 'a' && r <= 'f') {
return "", fmt.Errorf("non-hex fingerprint")
}
}
var b strings.Builder
for i := 0; i < 64; i += 2 {
if i > 0 {
b.WriteByte(':')
}
b.WriteString(strings.ToUpper(s[i : i+2]))
}
return b.String(), nil
}