/* SPDX-License-Identifier: MIT
*
* Phobos
*/
package conf
import (
"encoding/base64"
"net/url"
"strings"
"unicode/utf8"
"golang.zx2c4.com/wireguard/windows/l18n"
)
const (
PhobosLinkScheme = "phobos://"
phobosLinkNoValue = "none"
phobosFallbackName = "phobos"
)
func IsPhobosLink(link string) bool {
return strings.HasPrefix(strings.ToLower(strings.TrimSpace(link)), PhobosLinkScheme)
}
func DecodePhobosLink(link string) (config, name string, err error) {
link = strings.TrimSpace(link)
if !IsPhobosLink(link) {
return "", "", &ParseError{l18n.Sprintf("Not a phobos:// link"), link}
}
body := link[len(PhobosLinkScheme):]
payload, fragment, _ := strings.Cut(body, "#")
payload = strings.TrimSuffix(payload, "/")
if len(payload) == 0 {
return "", "", &ParseError{l18n.Sprintf("The link carries no configuration"), link}
}
decoded, err := base64.RawURLEncoding.DecodeString(payload)
if err != nil {
return "", "", &ParseError{l18n.Sprintf("The link payload is not valid base64url: %v", err), payload}
}
if !utf8.Valid(decoded) {
return "", "", &ParseError{l18n.Sprintf("The link payload is not valid UTF-8 text"), payload}
}
return stripUnsetFields(string(decoded)), phobosLinkName(fragment), nil
}
func phobosLinkName(fragment string) string {
name, err := url.QueryUnescape(fragment)
if err != nil {
name = fragment
}
name = sanitizeTunnelName(name)
if !TunnelNameIsValid(name) {
return phobosFallbackName
}
return name
}
func sanitizeTunnelName(name string) string {
if strings.EqualFold(strings.TrimSpace(name), phobosLinkNoValue) {
return ""
}
var sanitized strings.Builder
for _, r := range name {
switch {
case r >= 'a' && r <= 'z', r >= 'A' && r <= 'Z', r >= '0' && r <= '9',
r == '_', r == '=', r == '+', r == '.', r == '-':
sanitized.WriteRune(r)
default:
sanitized.WriteByte('-')
}
}
collapsed := sanitized.String()
for strings.Contains(collapsed, "--") {
collapsed = strings.ReplaceAll(collapsed, "--", "-")
}
collapsed = strings.Trim(collapsed, "-.")
if len(collapsed) > 32 {
collapsed = strings.Trim(collapsed[:32], "-.")
}
return collapsed
}
func stripUnsetFields(config string) string {
lines := strings.Split(config, "\n")
kept := lines[:0]
for _, line := range lines {
if key, value, found := strings.Cut(line, "="); found &&
strings.EqualFold(strings.TrimSpace(value), phobosLinkNoValue) &&
len(strings.TrimSpace(key)) > 0 {
continue
}
kept = append(kept, line)
}
return strings.Join(kept, "\n")
}