main: wire script:<name> transport type through CLI, factory and spec

--script-dir/--script-pubkey select a signed .js (+.sig) or .flux package
by name; --transport=script:<name> and "script:<name>:<priority>" entries
in --transports both resolve through the same expandScriptType/
resolveScriptFile path (parseTransportList now peels the priority off the
LAST colon field, so a script: reference's own colon is unambiguous).
transport_factory.go's existing "script" case (added earlier alongside the
runtime) is otherwise unchanged.

go.mod/go.sum: goja + goja_nodejs (the interpreter and its eventloop/
require support) - the only genuinely new dependencies; pion/webrtc and
pierrec/lz4 were already present via native oneme.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
p1neappleXpress
2026-09-27 08:40:19 +03:00
co-authored by Claude Sonnet 5
parent ce52985fb1
commit 70e7a2042c
9 changed files with 310 additions and 65 deletions
+7 -1
View File
@@ -14,16 +14,22 @@ require (
)
require (
github.com/dop251/goja v0.0.0-20260926152631-39ec2650adc9
github.com/dop251/goja_nodejs v0.0.0-20260918173711-b481721df8a2
github.com/klauspost/compress v1.20.0
github.com/pierrec/lz4/v4 v4.1.27
github.com/pion/webrtc/v3 v3.3.6
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e
github.com/xjasonlyu/windivert-go v0.0.0-20201010013527-4239d0afa76f
golang.org/x/crypto v0.51.0
golang.zx2c4.com/wireguard v0.0.0-20260522210424-ecfc5a8d5446
)
require (
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/dlclark/regexp2/v2 v2.5.2 // indirect
github.com/go-sourcemap/sourcemap v2.1.4+incompatible // indirect
github.com/google/pprof v0.0.0-20240727154555-813a5fbdbec8 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/pion/datachannel v1.5.8 // indirect
github.com/pion/dtls/v2 v2.2.12 // indirect
@@ -44,7 +50,7 @@ require (
github.com/stretchr/testify v1.9.0 // indirect
github.com/wlynxg/anet v0.0.5 // indirect
golang.org/x/net v0.55.0 // indirect
golang.org/x/text v0.37.0 // indirect
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 // indirect
golang.zx2c4.com/wireguard v0.0.0-20260522210424-ecfc5a8d5446 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
+18
View File
@@ -1,9 +1,23 @@
github.com/Masterminds/semver/v3 v3.5.0 h1:kQceYJfbupGfZOKZQg0kou0DgAKhzDg2NZPAwZ/2OOE=
github.com/Masterminds/semver/v3 v3.5.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dlclark/regexp2/v2 v2.5.2 h1:HAsucWRhsqcDzl6Ua9aR8JwYOTzrZyPrF0/FNxJVAI0=
github.com/dlclark/regexp2/v2 v2.5.2/go.mod h1:avUrQvPaLz2DrFNHJF0taWAFFX2C1GMSSoeiqFjcBmU=
github.com/dop251/goja v0.0.0-20260926152631-39ec2650adc9 h1:Ksd3Vil12b/ykT5NjGzd3qNc688cJB9OUXJZZChEktI=
github.com/dop251/goja v0.0.0-20260926152631-39ec2650adc9/go.mod h1:u8yZRUavu+N4EnFFy6J5fVtjE7lEcZ2YyV2GcBXY9c8=
github.com/dop251/goja_nodejs v0.0.0-20260918173711-b481721df8a2 h1:4cOnoHK48VXmDGOPp2h+u7sYZw/1Ou2JbHjTgU0DF0A=
github.com/dop251/goja_nodejs v0.0.0-20260918173711-b481721df8a2/go.mod h1:Tb7Xxye4LX7cT3i8YLvmPMGCV92IOi4CDZvm/V8ylc0=
github.com/go-sourcemap/sourcemap v2.1.4+incompatible h1:a+iTbH5auLKxaNwQFg0B+TCYl6lbukKPc7b5x0n1s6Q=
github.com/go-sourcemap/sourcemap v2.1.4+incompatible/go.mod h1:F8jJfvm2KbVjc5NqelyYJmf/v5J0dwNLS2mL4sNA1Jg=
github.com/goccy/go-yaml v1.19.2 h1:PmFC1S6h8ljIz6gMRBopkjP1TVT7xuwrButHID66PoM=
github.com/goccy/go-yaml v1.19.2/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
github.com/google/btree v1.1.2 h1:xf4v41cLI2Z6FxbKm+8Bu+m8ifhj15JuZ9sa0jZCMUU=
github.com/google/btree v1.1.2/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4=
github.com/google/pprof v0.0.0-20240727154555-813a5fbdbec8 h1:FKHo8hFI3A+7w0aUQuYXQ+6EN5stWmeY/AZqtM8xk9k=
github.com/google/pprof v0.0.0-20240727154555-813a5fbdbec8/go.mod h1:K1liHPHnj73Fdn/EKuT8nrFqBihUSKXoLYU0BuatOYo=
github.com/google/uuid v1.3.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
@@ -81,6 +95,8 @@ github.com/wlynxg/anet v0.0.5/go.mod h1:eay5PRQr7fIVAMbTbchTnO9gG65Hg/uYGdc7mguH
github.com/xjasonlyu/windivert-go v0.0.0-20201010013527-4239d0afa76f h1:glX3VZCYwW1/OmFxOjazfCtBLxXB3YNZk9LF2lYx+Lw=
github.com/xjasonlyu/windivert-go v0.0.0-20201010013527-4239d0afa76f/go.mod h1:gh//RKyt2Gesx3eOj3ulzrSQ60ySj2UA4qnOdrtarvg=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.8.0/go.mod h1:mRqEX+O9/h5TFCrQhkgjo2yKi0yYA+9ecGkdQoHrywE=
@@ -135,6 +151,8 @@ golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc=
golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38=
golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U=
golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+6 -3
View File
@@ -46,11 +46,14 @@ func TestPickSessionContext(t *testing.T) {
{"highest-priority transport URL", "", "http://#", specs, "https://docs/doc"},
{"no URL anywhere keeps the old default", "", "http://#", specs[:1], "http://#"},
{"legacy single transport", "", "http://#", []transportSpec{{Type: "yandex", Priority: 100, URL: "http://#"}}, "http://#"},
// cupsonline is script-only: a real spec for it has Type "script"
// (expandScriptType rewrites it) but keeps Name "cupsonline" - that's
// what pickSessionContext actually keys its exclusion off now.
{"cupsonline rooms are not a context", "", "http://#", []transportSpec{
{Type: "cupsonline", Priority: 100, URL: "WyIxYzE0NGQwZS1lMDQw"},
{Type: "yandex", Priority: 50, URL: "https://docs/doc"},
{Name: "cupsonline", Type: "script", Priority: 100, URL: "WyIxYzE0NGQwZS1lMDQw"},
{Name: "yandex", Type: "yandex", Priority: 50, URL: "https://docs/doc"},
}, "https://docs/doc"},
{"cupsonline alone", "", "http://#", []transportSpec{{Type: "cupsonline", Priority: 100, URL: "WyIxYzE0NGQwZS1lMDQw"}}, "http://#"},
{"cupsonline alone", "", "http://#", []transportSpec{{Name: "cupsonline", Type: "script", Priority: 100, URL: "WyIxYzE0NGQwZS1lMDQw"}}, "http://#"},
}
for _, c := range cases {
if got := pickSessionContext(c.explicit, c.url, c.specs); got != c.want {
+61 -34
View File
@@ -16,9 +16,7 @@ import (
"openflux/socks5"
"openflux/transport"
"openflux/transport/control"
"openflux/transport/cupsonline"
"openflux/transport/ipc"
"openflux/transport/mailru"
"openflux/transport/manager"
"openflux/transport/oneme"
"openflux/transport/yandex"
@@ -106,8 +104,15 @@ const (
// transportHasCookies reports whether the given transport uses HTTP cookies
// that can be refreshed via the NegotiatedTransport control channel.
func transportHasCookies(t string) bool {
if strings.HasPrefix(t, "script:") {
// Every script transport gets FetchCookies/ApplyCookies for free
// (transport/script's shared cookiejar.Jar) - assume it wants the
// store too; a script that never uses cookies just ends up with an
// empty jar file, harmless.
return true
}
switch t {
case "yandex", "vyandex", "boards", "mailru", "cupsonline":
case "yandex", "vyandex":
return true
}
return false
@@ -147,7 +152,7 @@ func isNumber(s string) bool {
// explicit --session-context, if non-empty
// --url globalURL, if set and not the placeholder
// transports URL of the highest-priority transport that has one,
// cupsonline aside
// cupsonline (native or script:cupsonline) aside
// fallback the placeholder "http://#"
//
// A cupsonline "URL" is the room list the exit creates when it starts and
@@ -167,7 +172,10 @@ func pickSessionContext(explicit, globalURL string, specs []transportSpec) strin
}
best := -1
for i, s := range specs {
if s.Type == "cupsonline" || s.URL == "" || s.URL == placeholder {
// cupsonline is script-only now (script:cupsonline expands Type to
// "script" but keeps Name == "cupsonline"), so the exclusion has to
// key off Name, not Type, to still catch it.
if s.Name == "cupsonline" || s.URL == "" || s.URL == placeholder {
continue
}
if best < 0 || s.Priority > specs[best].Priority {
@@ -206,7 +214,7 @@ func main() {
role := flag.String("role", roleClient, "client | exit | bench-send | bench-sink")
inbound := flag.String("inbound", "", "tun | socks5 (client only; default: tun on macOS, socks5 elsewhere)")
transportType := flag.String("transport", "yandex", "Transport type (yandex, vyandex, oneme, cupsonline, mailru)")
transportType := flag.String("transport", "yandex", "Transport type (yandex, vyandex, oneme, script:<name> - e.g. script:mailru, script:boards, script:cupsonline)")
mode := flag.String("mode", "", "Exit-node mode: l3 (default, Linux only) or l4 (works everywhere)")
codec := flag.String("codec", codecBatched, "batched (default, zstd+coalescing) or legacy (per-packet LZ4)")
@@ -230,15 +238,19 @@ func main() {
directListen := flag.String("direct-listen", "", "DirectTransport: local address to listen on (exit). Requires --encryption-key-file")
transportsFlag := flag.String("transports", "",
"Comma-separated list of transports with priorities, e.g. "+
"\"direct:100,yandex:50\". If empty, --transport is used as a single transport.")
"\"direct:100,yandex:50,script:mailru:30\". If empty, --transport is used as a single transport.")
yandexURL := flag.String("yandex-url", "", "URL for the yandex transport (overrides --url in --transports mode)")
vyandexURL := flag.String("vyandex-url", "", "URL for the vyandex transport")
flag.StringVar(&yandexCookiesFile, "yandex-cookies-file", "", "Netscape cookies.txt with a Yandex login for vyandex transports")
boardsURL := flag.String("boards-url", "", "URL for the boards transport")
mailruURL := flag.String("mailru-url", "", "URL (weblink) for the mailru transport")
cupsonlineURL := flag.String("cupsonline-url", "", "URL for the cupsonline transport")
onemeToken := flag.String("oneme-token", "", "MAX token for the oneme transport")
onemeUID := flag.String("oneme-uid", "", "MAX uid for the oneme transport")
scriptDir := flag.String("script-dir", "",
"Directory holding signed script transports: <name>.js + <name>.js.sig, or a "+
"<name>.flux package (script+manifest+icon signed together) - .flux wins if both exist. "+
"Select one with --transport=script:<name> or \"script:<name>:<priority>\" in --transports.")
scriptPubkey := flag.String("script-pubkey", "",
"Hex ed25519 public key every script transport's signature must verify against (the detached "+
".sig, or the .flux package's own signature). Required with --script-dir.")
configPath := flag.String("config", "",
"Path to an OpenFlux .conf file. Command-line flags override values from the file.")
shareFlag := flag.Bool("share", false,
@@ -289,9 +301,10 @@ TRANSPORT (single-transport mode)
-t, --transport=yandex Yandex.Docs over WebSocket. (default)
-t, --transport=vyandex Yandex.Volga over HTTP relay + WS.
-t, --transport=oneme MAX (VK) over WebRTC.
-t, --transport=cupsonline Cups.online interview rooms.
-t, --transport=mailru Mail.ru Docs over WebSocket.
-t, --transport=direct Plain TCP to a self-hosted exit.
-t, --transport=script:<name>
Signed JS transport (mailru, boards,
cupsonline, ...) loaded from --script-dir.
-u, --url=<URL> Document URL.
TRANSPORTS (multi-transport session; requires --encryption-key-file)
@@ -306,9 +319,6 @@ TRANSPORTS (multi-transport session; requires --encryption-key-file)
--yandex-cookies-file=<path>
Netscape cookies.txt with a Yandex login for
vyandex transports.
--boards-url=<URL> URL for the boards transport.
--mailru-url=<WEBLINK> Weblink for the mailru transport.
--cupsonline-url=<URL> URL for the cupsonline transport.
--oneme-token=<token> MAX auth token for the oneme transport.
--oneme-uid=<uid> MAX user id for the oneme transport.
--direct-dial=<addr> DirectTransport: exit host:port (client).
@@ -591,24 +601,22 @@ DEPRECATED (removed in v2)
specs = confTransports
// Per-type URL flags still override config values.
urls := map[string]string{
"yandex": *yandexURL,
"vyandex": *vyandexURL,
"boards": *boardsURL,
"mailru": *mailruURL,
"cupsonline": *cupsonlineURL,
"yandex": *yandexURL,
"vyandex": *vyandexURL,
}
var buildErr error
specs, buildErr = buildTransportSpecs(specs, urls, nil, *scriptDir, *scriptPubkey)
if buildErr != nil {
log.Fatalf("[Transport] sections: %v", buildErr)
}
specs = buildTransportSpecs(specs, urls, nil)
} else if *transportsFlag != "" {
parsed, err := parseTransportList(*transportsFlag)
if err != nil {
log.Fatalf("--transports: %v", err)
}
urls := map[string]string{
"yandex": *yandexURL,
"vyandex": *vyandexURL,
"boards": *boardsURL,
"mailru": *mailruURL,
"cupsonline": *cupsonlineURL,
"yandex": *yandexURL,
"vyandex": *vyandexURL,
}
if globalDocUrl != "" && globalDocUrl != "http://#" && urls["yandex"] == "" {
urls["yandex"] = globalDocUrl
@@ -621,7 +629,11 @@ DEPRECATED (removed in v2)
"is_exit": *role == roleExit,
},
}
specs = buildTransportSpecs(parsed, urls, extra)
var buildErr error
specs, buildErr = buildTransportSpecs(parsed, urls, extra, *scriptDir, *scriptPubkey)
if buildErr != nil {
log.Fatalf("--transports: %v", buildErr)
}
} else {
specs = []transportSpec{{
Name: "primary",
@@ -639,6 +651,13 @@ DEPRECATED (removed in v2)
"dial": *directDial, "listen": *directListen, "is_exit": *role == roleExit,
}
}
if strings.HasPrefix(*transportType, "script:") {
expanded, err := expandScriptType(specs[0], *scriptDir, *scriptPubkey)
if err != nil {
log.Fatalf("%v", err)
}
specs[0] = expanded
}
}
// Validate --codec with the multi-transport path. Session always uses
@@ -781,8 +800,6 @@ DEPRECATED (removed in v2)
// Legacy single-transport path (no negotiate, no multi).
var inner transport.Transport
switch *transportType {
case "boards":
inner = yandex.NewBoardsTransport(globalDocUrl, config)
case "vyandex":
t, err := newVolgaTransport(globalDocUrl, config)
if err != nil {
@@ -794,12 +811,22 @@ DEPRECATED (removed in v2)
case "oneme":
uidint, _ := strconv.ParseInt(maxUid, 10, 64)
inner = oneme.NewOneMeTransport(*role == roleExit, maxToken, uidint, config)
case "cupsonline":
inner = cupsonline.NewCupsonlineTransport(globalDocUrl, config, *role != roleExit)
case "mailru":
inner = mailru.NewMailruDocsTransport(globalDocUrl, config)
default:
log.Fatalf("Unknown transport type: %s", *transportType)
if strings.HasPrefix(*transportType, "script:") {
expanded, err := expandScriptType(transportSpec{Type: *transportType}, *scriptDir, *scriptPubkey)
if err != nil {
log.Fatalf("%v", err)
}
t, err := transportFactory(config)(&control.TransportConfig{
Name: expanded.Name, Type: expanded.Type, URL: globalDocUrl, Params: expanded.Params,
})
if err != nil {
log.Fatalf("script transport: %v", err)
}
inner = t
} else {
log.Fatalf("Unknown transport type: %s", *transportType)
}
}
// Persist cookie exchanger for the legacy path.
+8 -5
View File
@@ -2,18 +2,21 @@ package transport_test
import (
"openflux/transport"
"openflux/transport/cupsonline"
"openflux/transport/mailru"
"openflux/transport/script"
"openflux/transport/yandex"
)
// Compile-time guarantees that every HTTP-based transport implements
// CookieExchanger. If any of these break, main.go's type assertion will
// silently skip control wiring and cookie exchange will be dead.
//
// mailru, boards and cupsonline are script-only now (transport/script/js/
// {mailru,boards,cupsonline}.js) - their CookieExchanger guarantee is the
// generic script.ScriptTransport one below, not a type-specific assertion.
var (
_ transport.CookieExchanger = (*yandex.YandexDocsTransport)(nil)
_ transport.CookieExchanger = (*yandex.YandexVolgaTransport)(nil)
_ transport.CookieExchanger = (*yandex.BoardsTransport)(nil)
_ transport.CookieExchanger = (*mailru.MailruDocsTransport)(nil)
_ transport.CookieExchanger = (*cupsonline.CupsonlineTransport)(nil)
// Every script transport gets this for free (transport/script's shared
// cookiejar.Jar) - see ScriptTransport.FetchCookies/ApplyCookies.
_ transport.CookieExchanger = (*script.ScriptTransport)(nil)
)
+22 -8
View File
@@ -6,10 +6,9 @@ import (
"openflux/transport"
"openflux/transport/control"
"openflux/transport/cupsonline"
"openflux/transport/mailru"
"openflux/transport/manager"
"openflux/transport/oneme"
"openflux/transport/script"
"openflux/transport/yandex"
)
@@ -41,12 +40,27 @@ func transportFactory(baseCfg transport.TransportConfig) manager.Factory {
return yandex.NewYandexDocsTransport(cfg.URL, baseCfg), nil
case "vyandex":
return newVolgaTransport(cfg.URL, baseCfg)
case "boards":
return yandex.NewBoardsTransport(cfg.URL, baseCfg), nil
case "mailru":
return mailru.NewMailruDocsTransport(cfg.URL, baseCfg), nil
case "cupsonline":
return cupsonline.NewCupsonlineTransport(cfg.URL, baseCfg, false), nil
case "script":
// path: either a plain .js file (its detached path+".sig" must
// verify) or a .flux package (script+manifest+icon signed
// together - see transport/script/flux.go), picked by extension.
// pubkey: hex ed25519 public key it must be signed with.
// name: optional, defaults to the script's own info().name for
// logging - the factory doesn't know that until Start().
scriptPath, _ := cfg.Params["path"].(string)
pubkeyHex, _ := cfg.Params["pubkey"].(string)
name, _ := cfg.Params["name"].(string)
if name == "" {
name = "script"
}
if scriptPath == "" {
return nil, fmt.Errorf("factory: script transport missing \"path\" param")
}
pubKey, err := script.DecodePublicKeyHex(pubkeyHex)
if err != nil {
return nil, fmt.Errorf("factory: script transport: %w", err)
}
return script.New(name, scriptPath, pubKey, cfg.URL, cfg.Params, baseCfg)
case "oneme":
token, _ := cfg.Params["token"].(string)
uidStr, _ := cfg.Params["uid"].(string)
+85 -13
View File
@@ -2,6 +2,8 @@ package main
import (
"fmt"
"os"
"path/filepath"
"strconv"
"strings"
@@ -19,8 +21,13 @@ type transportSpec struct {
Params map[string]interface{}
}
// parseTransportList parses "direct:100,yandex:50,mailru:30".
// Priority is optional; default is 50.
// parseTransportList parses "direct:100,yandex:50,mailru:30" as well as a
// script: reference, whose OWN type carries a colon: "script:mailru:30".
// The priority, if present, is always the LAST colon-separated field - that
// stays unambiguous even for "script:mailru" (no priority: "mailru" isn't a
// number, so the whole string is kept as the type) vs "script:mailru:30"
// (30 is the last field and does parse, so it's peeled off as the
// priority). Priority is optional; default is 50.
func parseTransportList(s string) ([]transportSpec, error) {
var out []transportSpec
for _, part := range strings.Split(s, ",") {
@@ -28,27 +35,87 @@ func parseTransportList(s string) ([]transportSpec, error) {
if part == "" {
continue
}
name, prioStr, hasPrio := strings.Cut(part, ":")
typ := part
prio := 50
if hasPrio {
v, err := strconv.Atoi(prioStr)
if err != nil {
return nil, fmt.Errorf("bad priority %q: %w", prioStr, err)
if i := strings.LastIndex(part, ":"); i >= 0 {
if v, err := strconv.Atoi(part[i+1:]); err == nil {
typ = part[:i]
prio = v
}
prio = v
}
if typ == "" {
return nil, fmt.Errorf("bad transport entry %q", part)
}
out = append(out, transportSpec{
Name: name,
Type: name, // by default the name and the type match
Name: typ, // by default the name and the type match
Type: typ,
Priority: prio,
})
}
return out, nil
}
// expandScriptType rewrites a spec whose Type is a "script:<name>"
// reference into the plain "script" type transport_factory.go's factory
// case expects, filling Params from --script-dir/--script-pubkey. A spec
// whose Type isn't a script: reference is returned unchanged - this is a
// no-op for every native transport.
func expandScriptType(spec transportSpec, scriptDir, pubkeyHex string) (transportSpec, error) {
name, ok := strings.CutPrefix(spec.Type, "script:")
if !ok {
return spec, nil
}
if name == "" {
return spec, fmt.Errorf("transport %q: script: needs a name, e.g. script:mailru", spec.Type)
}
if scriptDir == "" {
return spec, fmt.Errorf("transport %q: --script-dir is required for script: transports", spec.Type)
}
if pubkeyHex == "" {
return spec, fmt.Errorf("transport %q: --script-pubkey is required for script: transports", spec.Type)
}
scriptFile, err := resolveScriptFile(scriptDir, name)
if err != nil {
return spec, err
}
params := map[string]interface{}{
"path": scriptFile,
"pubkey": pubkeyHex,
"name": name,
}
// Let any params already on the spec (from a .conf [Transport] section,
// or the oneme/direct-style --extra map) add to or override these.
for k, v := range spec.Params {
params[k] = v
}
if spec.Name == spec.Type {
spec.Name = name
}
spec.Type = "script"
spec.Params = params
return spec, nil
}
// resolveScriptFile finds a script: transport's loadable file, preferring
// the packaged <name>.flux (script + signed manifest/icon together) and
// falling back to the plain <name>.js (+ detached .sig) every script
// shipped as before .flux existed.
func resolveScriptFile(scriptDir, name string) (string, error) {
flux := filepath.Join(scriptDir, name+".flux")
if _, err := os.Stat(flux); err == nil {
return flux, nil
}
js := filepath.Join(scriptDir, name+".js")
if _, err := os.Stat(js); err == nil {
return js, nil
}
return "", fmt.Errorf("script %q: neither %s nor %s found in %s", name, name+".flux", name+".js", scriptDir)
}
// buildTransportSpecs assembles the config for each name in the list, using
// per-type URL flags (--yandex-url, --mailru-url, ...) and global settings.
func buildTransportSpecs(specs []transportSpec, urls map[string]string, extra map[string]map[string]interface{}) []transportSpec {
// per-type URL flags (--yandex-url, --mailru-url, ...), global settings,
// and (for a script: entry) --script-dir/--script-pubkey.
func buildTransportSpecs(specs []transportSpec, urls map[string]string, extra map[string]map[string]interface{}, scriptDir, scriptPubkey string) ([]transportSpec, error) {
for i := range specs {
// Only a flag that was set overrides: the unset ones are "" and
// would wipe the URL of a .conf [Transport] section.
@@ -58,8 +125,13 @@ func buildTransportSpecs(specs []transportSpec, urls map[string]string, extra ma
if p, ok := extra[specs[i].Type]; ok {
specs[i].Params = p
}
expanded, err := expandScriptType(specs[i], scriptDir, scriptPubkey)
if err != nil {
return nil, err
}
specs[i] = expanded
}
return specs
return specs, nil
}
// makeRawTransport builds a raw transport from a spec without the Manager's
+99
View File
@@ -0,0 +1,99 @@
package main
import (
"os"
"path/filepath"
"testing"
)
func TestParseTransportListScriptReference(t *testing.T) {
specs, err := parseTransportList("direct:100,script:mailru:30,script:boards")
if err != nil {
t.Fatalf("parseTransportList: %v", err)
}
if len(specs) != 3 {
t.Fatalf("got %d specs, want 3", len(specs))
}
if specs[0].Type != "direct" || specs[0].Priority != 100 {
t.Fatalf("specs[0] = %+v", specs[0])
}
if specs[1].Type != "script:mailru" || specs[1].Priority != 30 {
t.Fatalf("specs[1] = %+v, want Type=script:mailru Priority=30", specs[1])
}
// No explicit priority - the whole string is the type, default priority.
if specs[2].Type != "script:boards" || specs[2].Priority != 50 {
t.Fatalf("specs[2] = %+v, want Type=script:boards Priority=50", specs[2])
}
}
func TestExpandScriptTypeRequiresDirAndPubkey(t *testing.T) {
dir := t.TempDir()
if err := os.WriteFile(filepath.Join(dir, "mailru.js"), []byte("//"), 0o644); err != nil {
t.Fatalf("seed mailru.js: %v", err)
}
spec := transportSpec{Name: "script:mailru", Type: "script:mailru", Priority: 30}
if _, err := expandScriptType(spec, "", "somekeyhex"); err == nil {
t.Fatal("expected an error with no --script-dir")
}
if _, err := expandScriptType(spec, dir, ""); err == nil {
t.Fatal("expected an error with no --script-pubkey")
}
got, err := expandScriptType(spec, dir, "somekeyhex")
if err != nil {
t.Fatalf("expandScriptType: %v", err)
}
if got.Type != "script" {
t.Fatalf("Type = %q, want %q", got.Type, "script")
}
if got.Name != "mailru" {
t.Fatalf("Name = %q, want %q", got.Name, "mailru")
}
if want := filepath.Join(dir, "mailru.js"); got.Params["path"] != want {
t.Fatalf("Params[path] = %v, want %v", got.Params["path"], want)
}
if got.Params["pubkey"] != "somekeyhex" {
t.Fatalf("Params[pubkey] = %v, want somekeyhex", got.Params["pubkey"])
}
if got.Params["name"] != "mailru" {
t.Fatalf("Params[name] = %v, want mailru", got.Params["name"])
}
}
func TestExpandScriptTypeMissingScriptErrors(t *testing.T) {
dir := t.TempDir() // empty - neither mailru.flux nor mailru.js exists
spec := transportSpec{Name: "script:mailru", Type: "script:mailru", Priority: 30}
if _, err := expandScriptType(spec, dir, "somekeyhex"); err == nil {
t.Fatal("expected an error when neither <name>.flux nor <name>.js exists in --script-dir")
}
}
func TestExpandScriptTypePrefersFluxOverJS(t *testing.T) {
dir := t.TempDir()
if err := os.WriteFile(filepath.Join(dir, "mailru.js"), []byte("//"), 0o644); err != nil {
t.Fatalf("seed mailru.js: %v", err)
}
if err := os.WriteFile(filepath.Join(dir, "mailru.flux"), []byte("PK"), 0o644); err != nil {
t.Fatalf("seed mailru.flux: %v", err)
}
spec := transportSpec{Name: "script:mailru", Type: "script:mailru", Priority: 30}
got, err := expandScriptType(spec, dir, "somekeyhex")
if err != nil {
t.Fatalf("expandScriptType: %v", err)
}
if want := filepath.Join(dir, "mailru.flux"); got.Params["path"] != want {
t.Fatalf("Params[path] = %v, want %v (a .flux package should win over a plain .js)", got.Params["path"], want)
}
}
func TestExpandScriptTypeIgnoresNonScriptSpecs(t *testing.T) {
spec := transportSpec{Name: "mailru", Type: "mailru", Priority: 50}
got, err := expandScriptType(spec, "", "")
if err != nil {
t.Fatalf("expandScriptType: %v", err)
}
if got.Name != spec.Name || got.Type != spec.Type || got.Priority != spec.Priority || got.Params != nil {
t.Fatalf("non-script spec was modified: got %+v, want %+v", got, spec)
}
}
+4 -1
View File
@@ -11,7 +11,10 @@ func TestBuildTransportSpecsKeepsConfURL(t *testing.T) {
{Name: "yandex", Type: "yandex", URL: "https://disk.yandex.ru/i/conf"},
}
urls := map[string]string{"vyandex": "", "yandex": "https://disk.yandex.ru/i/flag"}
got := buildTransportSpecs(specs, urls, nil)
got, err := buildTransportSpecs(specs, urls, nil, "", "")
if err != nil {
t.Fatalf("buildTransportSpecs: %v", err)
}
if got[0].URL != "https://docs.yandex.ru/edit/d/conf" {
t.Fatalf("unset flag wiped the conf URL: %q", got[0].URL)
}