Release 2.6.0: fix AmneziaWG key parsing for awg://, JSON and vpn://.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Navis
2026-07-29 14:46:21 +03:00
co-authored by Cursor
parent ffb3ef7512
commit 99bbac20e2
25 changed files with 479 additions and 133 deletions
+3 -4
View File
@@ -725,7 +725,7 @@
<div class="brand-wrap">
<div class="brand-row">
<h1 class="brand">Navis</h1>
<span class="badge-ver">2.2.1</span>
<span class="badge-ver">2.6</span>
</div>
<p class="tagline">Быстрый клиент · Naive · Hy2 · AWG · Xray</p>
</div>
@@ -756,7 +756,7 @@
</div>
</div>
<label class="auto-best">
<input id="autoBest" type="checkbox" checked />
<input id="autoBest" type="checkbox" />
Авто: пинг + подключение к лучшему
</label>
<div class="server-list" id="serverList"></div>
@@ -923,8 +923,7 @@
try {
const saved = localStorage.getItem(AUTO_BEST_KEY);
if (saved === "0") autoBest.checked = false;
if (saved === "1") autoBest.checked = true;
autoBest.checked = saved === "1";
} catch (_) {}
autoBest.addEventListener("change", () => {
try { localStorage.setItem(AUTO_BEST_KEY, autoBest.checked ? "1" : "0"); } catch (_) {}
+4 -2
View File
@@ -15,9 +15,11 @@ func DetectProtocol(raw string) Protocol {
case strings.HasPrefix(lower, "trojan://"):
return ProtocolTrojan
case strings.HasPrefix(lower, "awg://"), strings.HasPrefix(lower, "amneziawg://"),
strings.HasPrefix(lower, "wireguard://"):
strings.HasPrefix(lower, "wireguard://"), strings.HasPrefix(lower, "vpn://"):
return ProtocolAWG
case strings.Contains(lower, "[interface]") && strings.Contains(lower, "privatekey"):
case strings.Contains(lower, "[interface]") && (strings.Contains(lower, "privatekey") || strings.Contains(lower, "private_key") || strings.Contains(lower, "private key")):
return ProtocolAWG
case strings.HasPrefix(strings.TrimSpace(raw), "{") && (strings.Contains(lower, "client_priv_key") || strings.Contains(lower, "server_pub_key")):
return ProtocolAWG
case strings.HasPrefix(lower, "naive+"), strings.HasPrefix(lower, "naive://"),
strings.HasPrefix(lower, "https://"), strings.HasPrefix(lower, "quic://"),
+345 -92
View File
@@ -3,6 +3,7 @@ package awg
import (
"encoding/base64"
"encoding/hex"
"encoding/json"
"fmt"
"net"
"net/netip"
@@ -12,22 +13,22 @@ import (
// Conf is a parsed AmneziaWG / WireGuard client configuration (AWG 2.0 fields included).
type Conf struct {
PrivateKey string
Address []string
DNS []string
MTU int
Jc int
Jmin int
Jmax int
PrivateKey string
Address []string
DNS []string
MTU int
Jc int
Jmin int
Jmax int
S1, S2, S3, S4 int
H1, H2, H3, H4 string
I1, I2, I3, I4, I5 string
PublicKey string
PublicKey string
PresharedKey string
Endpoint string
AllowedIPs []string
Keepalive int
Name string
Endpoint string
AllowedIPs []string
Keepalive int
Name string
}
// Detect reports whether raw looks like an AWG/WireGuard config or share link.
@@ -38,26 +39,43 @@ func Detect(raw string) bool {
}
lower := strings.ToLower(raw)
if strings.HasPrefix(lower, "awg://") || strings.HasPrefix(lower, "amneziawg://") ||
strings.HasPrefix(lower, "wireguard://") {
strings.HasPrefix(lower, "wireguard://") || strings.HasPrefix(lower, "vpn://") {
return true
}
if strings.Contains(lower, "[interface]") && strings.Contains(lower, "privatekey") {
if strings.Contains(lower, "[interface]") &&
(strings.Contains(lower, "privatekey") || strings.Contains(lower, "private_key") || strings.Contains(lower, "private key")) {
return true
}
if strings.HasPrefix(strings.TrimSpace(raw), "{") &&
(strings.Contains(lower, "client_priv_key") || strings.Contains(lower, "privatekey") ||
strings.Contains(lower, "server_pub_key")) {
return true
}
return false
}
// Parse accepts a WireGuard/AWG .conf body or awg:// URI and returns Conf.
// Parse accepts a WireGuard/AWG .conf body, awg:// URI, vpn:// blob, or Amnezia JSON.
func Parse(raw string) (Conf, error) {
raw = strings.TrimSpace(raw)
if raw == "" {
return Conf{}, fmt.Errorf("пустой AWG конфиг")
}
// Strip UTF-8 BOM
raw = strings.TrimPrefix(raw, "\ufeff")
lower := strings.ToLower(raw)
switch {
case strings.HasPrefix(lower, "vpn://"):
return parseVPNScheme(raw)
case strings.HasPrefix(lower, "awg://"), strings.HasPrefix(lower, "amneziawg://"),
strings.HasPrefix(lower, "wireguard://"):
return parseURI(raw)
case strings.HasPrefix(raw, "{") || strings.HasPrefix(raw, "["):
if c, err := parseAmneziaJSON(raw); err == nil {
return c, nil
}
// fall through to INI if JSON didn't work (rare)
return parseINI(raw)
default:
return parseINI(raw)
}
@@ -65,78 +83,27 @@ func Parse(raw string) (Conf, error) {
func parseINI(raw string) (Conf, error) {
var c Conf
section := ""
section := "interface" // treat leading keys before [Interface] as Interface
for _, line := range strings.Split(raw, "\n") {
line = strings.TrimSpace(line)
if line == "" || strings.HasPrefix(line, "#") || strings.HasPrefix(line, ";") {
continue
}
if strings.HasPrefix(line, "[") && strings.HasSuffix(line, "]") {
section = strings.ToLower(strings.TrimSpace(line[1 : len(line)-1]))
section = normalizeKey(line[1 : len(line)-1])
continue
}
key, val, ok := splitKV(line)
if !ok {
continue
}
k := strings.ToLower(key)
k := normalizeKey(key)
val = unwrapQuotes(val)
switch section {
case "interface":
switch k {
case "privatekey":
c.PrivateKey = val
case "address":
c.Address = appendCSV(c.Address, val)
case "dns":
c.DNS = appendCSV(c.DNS, val)
case "mtu":
c.MTU, _ = strconv.Atoi(val)
case "jc":
c.Jc, _ = strconv.Atoi(val)
case "jmin":
c.Jmin, _ = strconv.Atoi(val)
case "jmax":
c.Jmax, _ = strconv.Atoi(val)
case "s1":
c.S1, _ = strconv.Atoi(val)
case "s2":
c.S2, _ = strconv.Atoi(val)
case "s3":
c.S3, _ = strconv.Atoi(val)
case "s4":
c.S4, _ = strconv.Atoi(val)
case "h1":
c.H1 = val
case "h2":
c.H2 = val
case "h3":
c.H3 = val
case "h4":
c.H4 = val
case "i1":
c.I1 = val
case "i2":
c.I2 = val
case "i3":
c.I3 = val
case "i4":
c.I4 = val
case "i5":
c.I5 = val
}
case "interface", "":
applyInterfaceField(&c, k, val)
case "peer":
switch k {
case "publickey":
c.PublicKey = val
case "presharedkey":
c.PresharedKey = val
case "endpoint":
c.Endpoint = val
case "allowedips":
c.AllowedIPs = appendCSV(c.AllowedIPs, val)
case "persistentkeepalive":
c.Keepalive, _ = strconv.Atoi(val)
}
applyPeerField(&c, k, val)
}
}
if err := c.validate(); err != nil {
@@ -145,9 +112,77 @@ func parseINI(raw string) (Conf, error) {
return c, nil
}
func applyInterfaceField(c *Conf, k, val string) {
switch k {
case "privatekey", "clientprivkey", "clientprivatekey":
c.PrivateKey = val
case "address", "addr", "clientip":
c.Address = appendCSV(c.Address, val)
case "dns":
c.DNS = appendCSV(c.DNS, val)
case "mtu":
c.MTU, _ = strconv.Atoi(val)
case "jc":
c.Jc, _ = strconv.Atoi(val)
case "jmin":
c.Jmin, _ = strconv.Atoi(val)
case "jmax":
c.Jmax, _ = strconv.Atoi(val)
case "s1":
c.S1, _ = strconv.Atoi(val)
case "s2":
c.S2, _ = strconv.Atoi(val)
case "s3":
c.S3, _ = strconv.Atoi(val)
case "s4":
c.S4, _ = strconv.Atoi(val)
case "h1":
c.H1 = val
case "h2":
c.H2 = val
case "h3":
c.H3 = val
case "h4":
c.H4 = val
case "i1":
c.I1 = val
case "i2":
c.I2 = val
case "i3":
c.I3 = val
case "i4":
c.I4 = val
case "i5":
c.I5 = val
}
}
func applyPeerField(c *Conf, k, val string) {
switch k {
case "publickey", "serverpubkey", "serverpublickey", "peerpublickey", "peerkey":
c.PublicKey = val
case "presharedkey", "psk", "pskkey":
c.PresharedKey = val
case "endpoint", "host", "hostname":
c.Endpoint = val
case "allowedips", "allowedip":
c.AllowedIPs = appendCSV(c.AllowedIPs, val)
case "persistentkeepalive", "keepalive":
c.Keepalive, _ = strconv.Atoi(val)
}
}
func parseURI(raw string) (Conf, error) {
// awg://host:port/?public_key=...&private_key=...&address=10.8.0.2/32&jc=4&...
// Formats:
// awg://host:port/?private_key=...&public_key=...&address=10.8.0.2/32
// awg://PRIVATE_KEY@host:port/?public_key=...&address=...
// awg://host:port/#name (query keys)
u := raw
fragment := ""
if i := strings.Index(u, "#"); i >= 0 {
fragment = u[i+1:]
u = u[:i]
}
schemeIdx := strings.Index(u, "://")
if schemeIdx < 0 {
return Conf{}, fmt.Errorf("bad awg uri")
@@ -160,7 +195,22 @@ func parseURI(raw string) (Conf, error) {
query = rest[i+1:]
}
hostPort = strings.TrimSuffix(hostPort, "/")
c := Conf{Endpoint: hostPort}
c := Conf{}
if frag, err := urlDecode(fragment); err == nil && frag != "" {
c.Name = frag
}
// userinfo = private key (WireGuard share-link style)
if at := strings.LastIndex(hostPort, "@"); at >= 0 {
pk, _ := urlDecode(hostPort[:at])
c.PrivateKey = unwrapQuotes(pk)
hostPort = hostPort[at+1:]
}
if hostPort != "" {
c.Endpoint = hostPort
}
for _, part := range strings.Split(query, "&") {
if part == "" {
continue
@@ -169,21 +219,21 @@ func parseURI(raw string) (Conf, error) {
if !ok {
continue
}
k = strings.ToLower(strings.TrimSpace(k))
v = strings.TrimSpace(v)
k = normalizeKey(k)
v = unwrapQuotes(strings.TrimSpace(v))
v, _ = urlDecode(v)
switch k {
case "private_key", "privatekey":
case "privatekey", "clientprivkey", "clientprivatekey":
c.PrivateKey = v
case "public_key", "publickey":
case "publickey", "serverpubkey", "serverpublickey", "peerpublickey", "peerkey":
c.PublicKey = v
case "preshared_key", "presharedkey", "psk":
case "presharedkey", "psk", "pskkey":
c.PresharedKey = v
case "address", "addr":
case "address", "addr", "clientip":
c.Address = appendCSV(c.Address, v)
case "dns":
c.DNS = appendCSV(c.DNS, v)
case "allowed_ips", "allowedips":
case "allowedips", "allowedip":
c.AllowedIPs = appendCSV(c.AllowedIPs, v)
case "mtu":
c.MTU, _ = strconv.Atoi(v)
@@ -223,8 +273,12 @@ func parseURI(raw string) (Conf, error) {
c.I5 = v
case "name", "remark":
c.Name = v
case "endpoint":
case "endpoint", "host", "hostname":
c.Endpoint = v
case "port":
if c.Endpoint != "" && !strings.Contains(c.Endpoint, ":") {
c.Endpoint = c.Endpoint + ":" + v
}
}
}
if err := c.validate(); err != nil {
@@ -233,12 +287,137 @@ func parseURI(raw string) (Conf, error) {
return c, nil
}
func parseVPNScheme(raw string) (Conf, error) {
payload := strings.TrimSpace(raw[len("vpn://"):])
payload = strings.TrimSuffix(payload, "/")
decoded, err := decodeMaybeBase64(payload)
if err != nil {
return Conf{}, fmt.Errorf("awg vpn://: %w", err)
}
if c, err := parseAmneziaJSON(decoded); err == nil {
return c, nil
}
// nested: sometimes JSON has last_config string with INI or JSON
var wrap map[string]any
if err := json.Unmarshal([]byte(decoded), &wrap); err == nil {
for _, key := range []string{"last_config", "config", "cfg", "awg_config", "wireguard"} {
if s, ok := wrap[key].(string); ok && strings.TrimSpace(s) != "" {
if c, err := Parse(s); err == nil {
return c, nil
}
}
}
}
return Parse(decoded)
}
func parseAmneziaJSON(raw string) (Conf, error) {
raw = strings.TrimSpace(raw)
var m map[string]any
if err := json.Unmarshal([]byte(raw), &m); err != nil {
return Conf{}, err
}
// Flatten nested last_config JSON string.
if lc, ok := m["last_config"].(string); ok && strings.TrimSpace(lc) != "" {
var nested map[string]any
if json.Unmarshal([]byte(lc), &nested) == nil {
for k, v := range nested {
if _, exists := m[k]; !exists {
m[k] = v
}
}
} else if c, err := Parse(lc); err == nil {
return c, nil
}
}
str := func(keys ...string) string {
for _, k := range keys {
for mk, mv := range m {
if normalizeKey(mk) != normalizeKey(k) {
continue
}
switch t := mv.(type) {
case string:
return unwrapQuotes(strings.TrimSpace(t))
case float64:
return strconv.FormatInt(int64(t), 10)
case json.Number:
return t.String()
}
}
}
return ""
}
atoi := func(keys ...string) int {
s := str(keys...)
n, _ := strconv.Atoi(s)
return n
}
c := Conf{
PrivateKey: str("client_priv_key", "privateKey", "private_key", "PrivateKey"),
PublicKey: str("server_pub_key", "publicKey", "public_key", "PublicKey", "peer_public_key"),
PresharedKey: str("psk_key", "presharedKey", "preshared_key", "PresharedKey"),
Jc: atoi("Jc", "jc"),
Jmin: atoi("Jmin", "jmin"),
Jmax: atoi("Jmax", "jmax"),
S1: atoi("S1", "s1"),
S2: atoi("S2", "s2"),
S3: atoi("S3", "s3"),
S4: atoi("S4", "s4"),
H1: str("H1", "h1"),
H2: str("H2", "h2"),
H3: str("H3", "h3"),
H4: str("H4", "h4"),
I1: str("I1", "i1"),
I2: str("I2", "i2"),
I3: str("I3", "i3"),
I4: str("I4", "i4"),
I5: str("I5", "i5"),
MTU: atoi("mtu", "MTU"),
Keepalive: atoi("persistent_keep_alive", "PersistentKeepalive", "keepalive"),
Name: str("name", "description", "hostName"),
}
if addr := str("client_ip", "address", "Address"); addr != "" {
c.Address = appendCSV(nil, addr)
}
if dns := str("dns1", "dns", "DNS"); dns != "" {
c.DNS = appendCSV(nil, dns)
}
if dns2 := str("dns2"); dns2 != "" {
c.DNS = appendCSV(c.DNS, dns2)
}
host := str("hostName", "host", "endpoint_host", "server")
port := str("port", "endpoint_port")
endpoint := str("endpoint", "Endpoint")
switch {
case endpoint != "":
c.Endpoint = endpoint
case host != "" && port != "":
c.Endpoint = net.JoinHostPort(trimBrackets(host), port)
case host != "":
c.Endpoint = host
}
if allowed := str("allowed_ips", "AllowedIPs"); allowed != "" {
c.AllowedIPs = appendCSV(nil, allowed)
}
if err := c.validate(); err != nil {
return Conf{}, err
}
return c, nil
}
func (c *Conf) validate() error {
c.PrivateKey = unwrapQuotes(strings.TrimSpace(c.PrivateKey))
c.PublicKey = unwrapQuotes(strings.TrimSpace(c.PublicKey))
c.PresharedKey = unwrapQuotes(strings.TrimSpace(c.PresharedKey))
c.Endpoint = strings.TrimSpace(c.Endpoint)
if c.PrivateKey == "" {
return fmt.Errorf("awg: нет PrivateKey")
return fmt.Errorf("awg: нет PrivateKey (приватный ключ клиента)")
}
if c.PublicKey == "" {
return fmt.Errorf("awg: нет PublicKey")
return fmt.Errorf("awg: нет PublicKey (публичный ключ сервера)")
}
if c.Endpoint == "" {
return fmt.Errorf("awg: нет Endpoint")
@@ -260,7 +439,6 @@ func HostPort(raw string) (host, port string, err error) {
}
h, p, err := net.SplitHostPort(c.Endpoint)
if err != nil {
// bare host without port
return c.Endpoint, "51820", nil
}
return h, p, nil
@@ -368,14 +546,37 @@ func (c Conf) EffectiveMTU() int {
}
func decodeKey(s string) ([]byte, error) {
s = strings.TrimSpace(s)
if b, err := base64.StdEncoding.DecodeString(s); err == nil && len(b) == 32 {
return b, nil
s = unwrapQuotes(strings.TrimSpace(s))
if s == "" {
return nil, fmt.Errorf("пустой ключ")
}
encodings := []*base64.Encoding{
base64.StdEncoding,
base64.RawStdEncoding,
base64.URLEncoding,
base64.RawURLEncoding,
}
for _, enc := range encodings {
if b, err := enc.DecodeString(s); err == nil && len(b) == 32 {
return b, nil
}
}
// pad std / url
pad := func(in string) string {
if m := len(in) % 4; m != 0 {
return in + strings.Repeat("=", 4-m)
}
return in
}
for _, enc := range []*base64.Encoding{base64.StdEncoding, base64.URLEncoding} {
if b, err := enc.DecodeString(pad(s)); err == nil && len(b) == 32 {
return b, nil
}
}
if b, err := hex.DecodeString(s); err == nil && len(b) == 32 {
return b, nil
}
return nil, fmt.Errorf("expected 32-byte base64/hex key")
return nil, fmt.Errorf("ожидался ключ WireGuard (32 байта base64/hex)")
}
func writeInt(b *strings.Builder, key string, v int) {
@@ -417,6 +618,58 @@ func appendCSV(dst []string, raw string) []string {
return dst
}
func normalizeKey(s string) string {
s = strings.ToLower(strings.TrimSpace(s))
var b strings.Builder
for _, r := range s {
if r == '_' || r == '-' || r == ' ' || r == '\t' {
continue
}
b.WriteRune(r)
}
return b.String()
}
func unwrapQuotes(s string) string {
s = strings.TrimSpace(s)
if len(s) >= 2 {
if (s[0] == '"' && s[len(s)-1] == '"') || (s[0] == '\'' && s[len(s)-1] == '\'') {
return strings.TrimSpace(s[1 : len(s)-1])
}
}
return s
}
func trimBrackets(host string) string {
host = strings.TrimSpace(host)
if strings.HasPrefix(host, "[") && strings.HasSuffix(host, "]") {
return host[1 : len(host)-1]
}
return host
}
func decodeMaybeBase64(s string) (string, error) {
s = strings.TrimSpace(s)
encodings := []*base64.Encoding{
base64.StdEncoding,
base64.RawStdEncoding,
base64.URLEncoding,
base64.RawURLEncoding,
}
pad := func(in string) string {
if m := len(in) % 4; m != 0 {
return in + strings.Repeat("=", 4-m)
}
return in
}
for _, enc := range encodings {
if b, err := enc.DecodeString(pad(s)); err == nil && len(b) > 0 {
return string(b), nil
}
}
return "", fmt.Errorf("не base64")
}
func urlDecode(s string) (string, error) {
var b strings.Builder
for i := 0; i < len(s); {
@@ -465,10 +718,10 @@ func NormalizeShareLink(raw string) (normalized string, remark string, err error
} else {
remark = c.Endpoint
}
// Prefer keeping original INI when pasted; re-serialize only for URI imports.
lower := strings.ToLower(strings.TrimSpace(raw))
if strings.HasPrefix(lower, "awg://") || strings.HasPrefix(lower, "amneziawg://") ||
strings.HasPrefix(lower, "wireguard://") {
strings.HasPrefix(lower, "wireguard://") || strings.HasPrefix(lower, "vpn://") ||
strings.HasPrefix(strings.TrimSpace(raw), "{") {
return serializeINI(c), remark, nil
}
return strings.TrimSpace(raw), remark, nil
+90 -1
View File
@@ -1,6 +1,9 @@
package awg
import "testing"
import (
"encoding/base64"
"testing"
)
func TestParseINI_AWG20(t *testing.T) {
raw := `[Interface]
@@ -41,6 +44,80 @@ PersistentKeepalive = 25
}
}
func TestParseURI_UserInfoPrivateKey(t *testing.T) {
priv := "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA="
pub := "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEE="
raw := "awg://" + priv + "@203.0.113.10:51820/?public_key=" + pub + "&address=10.8.0.2/32&jc=4"
c, err := Parse(raw)
if err != nil {
t.Fatal(err)
}
if c.PrivateKey != priv {
t.Fatalf("private key: %q", c.PrivateKey)
}
if c.PublicKey != pub {
t.Fatalf("public key: %q", c.PublicKey)
}
if c.Endpoint != "203.0.113.10:51820" {
t.Fatalf("endpoint: %q", c.Endpoint)
}
}
func TestParseINI_SpacedKeysAndQuotes(t *testing.T) {
raw := `Private Key = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA="
Address = 10.8.0.2/32
[Peer]
Public Key = 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEE='
Endpoint = 1.2.3.4:51820
`
c, err := Parse(raw)
if err != nil {
t.Fatal(err)
}
if c.PrivateKey == "" || c.PublicKey == "" {
t.Fatalf("keys missing: %+v", c)
}
}
func TestParseAmneziaJSON(t *testing.T) {
raw := `{
"client_priv_key": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=",
"server_pub_key": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEE=",
"client_ip": "10.8.0.2/32",
"hostName": "203.0.113.10",
"port": "51820",
"Jc": 4,
"Jmin": 10,
"Jmax": 50,
"H1": "1",
"H2": "2",
"H3": "3",
"H4": "4"
}`
c, err := Parse(raw)
if err != nil {
t.Fatal(err)
}
if c.Endpoint != "203.0.113.10:51820" {
t.Fatalf("endpoint %q", c.Endpoint)
}
if c.Jc != 4 {
t.Fatalf("jc %d", c.Jc)
}
}
func TestParseVPNScheme(t *testing.T) {
inner := `{"client_priv_key":"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=","server_pub_key":"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEE=","client_ip":"10.8.0.2/32","hostName":"9.9.9.9","port":"51820"}`
raw := "vpn://" + base64.StdEncoding.EncodeToString([]byte(inner))
c, err := Parse(raw)
if err != nil {
t.Fatal(err)
}
if c.Endpoint != "9.9.9.9:51820" {
t.Fatalf("endpoint %q", c.Endpoint)
}
}
func TestDetect(t *testing.T) {
if !Detect("[Interface]\nPrivateKey = x") {
t.Fatal("expected detect")
@@ -48,6 +125,18 @@ func TestDetect(t *testing.T) {
if !Detect("awg://1.2.3.4:51820/?private_key=a&public_key=b&address=10.0.0.2/32") {
t.Fatal("expected awg uri detect")
}
if !Detect(`{"client_priv_key":"x","server_pub_key":"y"}`) {
t.Fatal("expected json detect")
}
}
func TestDecodeKeyURLSafe(t *testing.T) {
// 32 zero bytes, URL-safe without padding
raw := base64.RawURLEncoding.EncodeToString(make([]byte, 32))
b, err := decodeKey(raw)
if err != nil || len(b) != 32 {
t.Fatalf("url-safe: %v %d", err, len(b))
}
}
func containsAll(s string, parts ...string) bool {
+1 -1
View File
@@ -17,7 +17,7 @@ import (
)
// CurrentVersion is the shipped client version.
const CurrentVersion = "2.2.1"
const CurrentVersion = "2.6.0"
// DefaultManifestURL is the update feed (hosted in the project git repo).
const DefaultManifestURL = "https://git.evilfox.cc/test2/navi/raw/branch/main/dist/update.json"