mirror of
https://github.com/p1neappleXpress/OpenFlux.git
synced 2026-10-14 05:10:49 +08:00
Node wizard: no Yandex sign-in for the node
The apps no longer hand a node a Yandex sign-in (the wizard takes a link to your own document), so the whole path goes: - node-install.sh: cookies= and set-cookies (write_cookies); a node's own cookie store (CookieStore in node.conf, the transports' state) stays. - provision: Channel.Cookies, CookieStore, ChannelCookies, SetCookies; Plan takes the channel only. - --node-wizard: withCookies, cookies, setCookies, signedIn. - mobile: NodePlan/NodeApply lose withCookies/cookieHeader; NodeSetCookies and NodeSignedIn are removed. Apps already out pin their own script and bundle their own core, so they are not affected. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
a691a5fe54
commit
b6b0e3c838
+4
-49
@@ -31,19 +31,14 @@
|
||||
# script installed)
|
||||
# node-install.sh upgrade (run as root: move every
|
||||
# channel to this core)
|
||||
# node-install.sh set-cookies CONFIG_FILE (run as root: replace a
|
||||
# channel's Yandex login)
|
||||
# node-install.sh update (run as root, by the timer:
|
||||
# move every channel to the
|
||||
# newest node-v* release)
|
||||
# node-install.sh autoupdate on|off (run as root: the updater)
|
||||
# The config is "key=value" lines: channel, key, port, the transports
|
||||
# (vyandex= or its old name url=: a Yandex document; mailru=: a Mail.ru
|
||||
# public document; cupsonline=: the packed room list), autoupdate=yes|no,
|
||||
# and optionally cookies: the channel's Yandex sign-in as the core's cookie store JSON,
|
||||
# base64-encoded. It goes to /var/lib/openflux-node/<channel>/cookies.json
|
||||
# (0600, owned by the node user) and is never printed. apply and remove
|
||||
# take it from a 0600 temp file, which they delete after reading, so that
|
||||
# public document; cupsonline=: the packed room list) and autoupdate=yes|no.
|
||||
# apply and remove take it from a 0600 temp file, which they delete after reading, so that
|
||||
# stdin stays free for `sudo -S` (a wrong sudo password would otherwise make
|
||||
# sudo read the config as further password attempts). Secrets never appear
|
||||
# in arguments, so they stay out of ps and shell history.
|
||||
@@ -261,7 +256,7 @@ pick_port() {
|
||||
|
||||
# ---- input ------------------------------------------------------------------
|
||||
|
||||
CHANNEL=""; URL=""; MAILRU=""; CUPS=""; KEY=""; PORT=""; COOKIES=""; AUTOUPDATE=""
|
||||
CHANNEL=""; URL=""; MAILRU=""; CUPS=""; KEY=""; PORT=""; AUTOUPDATE=""
|
||||
|
||||
# read_config [FILE]: reads stdin, or FILE and then deletes it.
|
||||
read_config() {
|
||||
@@ -280,7 +275,6 @@ read_config() {
|
||||
cupsonline=*) CUPS=${line#cupsonline=} ;;
|
||||
key=*) KEY=${line#key=} ;;
|
||||
port=*) PORT=${line#port=} ;;
|
||||
cookies=*) COOKIES=${line#cookies=} ;;
|
||||
autoupdate=*) AUTOUPDATE=${line#autoupdate=} ;;
|
||||
"") ;;
|
||||
*) fail input "неизвестная строка конфигурации" ;;
|
||||
@@ -307,7 +301,6 @@ check_transports() {
|
||||
[ -z "$URL" ] || valid_url "$URL" || fail input "адрес документа должен быть вида https://docs.yandex.ru/edit/d/..."
|
||||
[ -z "$MAILRU" ] || valid_mailru "$MAILRU" || fail input "ссылка Mail.ru должна быть вида https://cloud.mail.ru/public/..."
|
||||
[ -z "$CUPS" ] || valid_rooms "$CUPS" || fail input "неверный список комнат cups.online"
|
||||
[ -z "$COOKIES" ] || [ -n "$URL" ] || fail input "вход в Яндекс нужен только каналу с документом Яндекса"
|
||||
case "$AUTOUPDATE" in ""|yes|no) ;; *) fail input "autoupdate: yes или no" ;; esac
|
||||
}
|
||||
|
||||
@@ -327,26 +320,6 @@ session_context() {
|
||||
if [ -n "$URL" ]; then printf '%s' "$URL"; elif [ -n "$MAILRU" ]; then printf '%s' "$MAILRU"; fi
|
||||
}
|
||||
|
||||
# write_cookies: decodes COOKIES into the channel's cookie store, which the
|
||||
# node loads at start. Needs the node user to exist.
|
||||
write_cookies() {
|
||||
printf '%s' "$COOKIES" | grep -Eq '^[A-Za-z0-9+/]+={0,2}$' || return 1
|
||||
[ ${#COOKIES} -le 65536 ] || return 1
|
||||
dir="$STATE_ROOT/$CHANNEL"
|
||||
# umask 077 would make the parent 0700 and lock the node user out of
|
||||
# its own state directory (systemd only creates it when missing).
|
||||
mkdir -p "$STATE_ROOT" && chmod 0755 "$STATE_ROOT" || return 1
|
||||
mkdir -p "$dir" || return 1
|
||||
tmp="$dir/.cookies.json.new"
|
||||
if have base64; then
|
||||
printf '%s' "$COOKIES" | base64 -d > "$tmp" 2>/dev/null || { rm -f "$tmp"; return 1; }
|
||||
else
|
||||
printf '%s' "$COOKIES" | openssl base64 -d -A > "$tmp" 2>/dev/null || { rm -f "$tmp"; return 1; }
|
||||
fi
|
||||
head -c 1 "$tmp" | grep -q '{' || { rm -f "$tmp"; return 1; }
|
||||
chown "$NODE_USER:$NODE_USER" "$dir" "$tmp" && chmod 0600 "$tmp" && mv -f "$tmp" "$dir/cookies.json"
|
||||
}
|
||||
|
||||
check_channel() {
|
||||
[ -n "$CHANNEL" ] || fail input "не указан канал"
|
||||
valid_channel "$CHANNEL" || fail input "имя канала: только a-z, 0-9 и дефис, до 31 символа"
|
||||
@@ -400,7 +373,6 @@ cmd_plan() {
|
||||
set -- "$@" "Скачать ядро OpenFlux $core (linux-$arch) из релизов $RELEASE_REPO на GitHub и сверить SHA-256 в $BIN_DIR"
|
||||
fi
|
||||
set -- "$@" "Создать $CONF_ROOT/$CHANNEL: node.conf и ключ шифрования канала (права 0640)"
|
||||
[ -n "$COOKIES" ] && set -- "$@" "Сохранить вход в Яндекс для этого канала в $STATE_ROOT/$CHANNEL/cookies.json (права 0600, только для ноды)"
|
||||
[ -f "$UNIT_FILE" ] || set -- "$@" "Установить шаблон systemd $UNIT_FILE"
|
||||
names=$(transport_names)
|
||||
if [ -n "$names" ]; then
|
||||
@@ -610,7 +582,6 @@ cmd_apply() {
|
||||
valid_key "$KEY" || fail input "ключ канала должен быть 64 hex-символа"
|
||||
check_transports
|
||||
valid_port "$PORT" || fail input "не указан порт из плана"
|
||||
[ -z "$COOKIES" ] || printf '%s' "$COOKIES" | grep -Eq '^[A-Za-z0-9+/]+={0,2}$' || fail input "cookies должны быть в base64"
|
||||
arch=$(detect_arch)
|
||||
[ -n "$arch" ] || fail apply "архитектура $(uname -m) не поддерживается"
|
||||
[ -d "$CONF_ROOT/$CHANNEL" ] && fail apply "канал $CHANNEL уже существует на сервере"
|
||||
@@ -649,9 +620,6 @@ cmd_apply() {
|
||||
# The node's state directory (systemd before 235 does not make it).
|
||||
mkdir -p "$STATE_ROOT/$CHANNEL" && chmod 0755 "$STATE_ROOT" && chown "$NODE_USER:$NODE_USER" "$STATE_ROOT/$CHANNEL" \
|
||||
&& chmod 0750 "$STATE_ROOT/$CHANNEL" || apply_fail config "не удалось создать $STATE_ROOT/$CHANNEL"
|
||||
if [ -n "$COOKIES" ]; then
|
||||
write_cookies || apply_fail cookies "не удалось сохранить вход в Яндекс на сервере"
|
||||
fi
|
||||
|
||||
if [ ! -f "$UNIT_FILE" ]; then
|
||||
write_unit
|
||||
@@ -901,18 +869,6 @@ cmd_update() {
|
||||
"$latest" "$(json_escape "${prev#openflux-}")" "$(json_list "$@")"
|
||||
}
|
||||
|
||||
# set-cookies: replaces a channel's Yandex sign-in and restarts it.
|
||||
cmd_set_cookies() {
|
||||
[ "$(id -u)" = 0 ] || fail set-cookies "нужны права root (sudo)"
|
||||
read_config "$@"
|
||||
check_channel
|
||||
[ -d "$CONF_ROOT/$CHANNEL" ] || fail set-cookies "канала $CHANNEL нет на сервере"
|
||||
[ -n "$COOKIES" ] || fail set-cookies "нет cookies"
|
||||
write_cookies || fail set-cookies "не удалось сохранить вход в Яндекс на сервере"
|
||||
systemctl restart "openflux-node@$CHANNEL" || fail set-cookies "не удалось перезапустить openflux-node@$CHANNEL"
|
||||
printf '{"ok":true,"channel":"%s"}\n' "$CHANNEL"
|
||||
}
|
||||
|
||||
# autoupdate on|off: turns the core updater on or off for the whole server,
|
||||
# e.g. on channels installed before the wizard offered it.
|
||||
cmd_autoupdate() {
|
||||
@@ -947,8 +903,7 @@ case "${1:-}" in
|
||||
list) cmd_list ;;
|
||||
status) cmd_status ;;
|
||||
upgrade) cmd_upgrade ;;
|
||||
set-cookies) shift; cmd_set_cookies "$@" ;;
|
||||
update) cmd_update ;;
|
||||
autoupdate) shift; cmd_autoupdate "$@" ;;
|
||||
*) fail usage "usage: node-install.sh probe|plan|apply|remove|list|uninstall|status|upgrade|set-cookies|update|autoupdate" ;;
|
||||
*) fail usage "usage: node-install.sh probe|plan|apply|remove|list|uninstall|status|upgrade|update|autoupdate" ;;
|
||||
esac
|
||||
|
||||
+5
-37
@@ -144,9 +144,9 @@ func nodeTransports(transportsJSON string) ([]provision.ChannelTransport, error)
|
||||
}
|
||||
|
||||
// NodePlan asks the VDS what installing the channel would change: {"plan"}.
|
||||
// transportsJSON: the carriers (see nodeTransports); withCookies: the Yandex
|
||||
// sign-in goes to the node too; autoUpdate: the server's core updater.
|
||||
func NodePlan(channel string, port int, transportsJSON string, withCookies, autoUpdate bool) string {
|
||||
// transportsJSON: the carriers (see nodeTransports); autoUpdate: the
|
||||
// server's core updater.
|
||||
func NodePlan(channel string, port int, transportsJSON string, autoUpdate bool) string {
|
||||
conn, err := nodeConn()
|
||||
if err != nil {
|
||||
return failure(err, nil)
|
||||
@@ -155,7 +155,7 @@ func NodePlan(channel string, port int, transportsJSON string, withCookies, auto
|
||||
if err != nil {
|
||||
return failure(err, nil)
|
||||
}
|
||||
plan, err := conn.Plan(provision.Channel{ID: channel, Port: port, Transports: ts, AutoUpdate: autoUpdate}, withCookies)
|
||||
plan, err := conn.Plan(provision.Channel{ID: channel, Port: port, Transports: ts, AutoUpdate: autoUpdate})
|
||||
if err != nil {
|
||||
return failure(err, nil)
|
||||
}
|
||||
@@ -164,10 +164,7 @@ func NodePlan(channel string, port int, transportsJSON string, withCookies, auto
|
||||
|
||||
// NodeApply installs and starts the channel. sudoPassword is only used when
|
||||
// the account needs one; a wrong one comes back with "sudo": true.
|
||||
// cookieHeader is the Yandex sign-in for the node ("" for none): the node
|
||||
// then opens the Yandex document as that account, which gets it past the
|
||||
// checks Yandex shows a server's address.
|
||||
func NodeApply(channel, transportsJSON, key string, port int, autoUpdate bool, sudoPassword, cookieHeader string) string {
|
||||
func NodeApply(channel, transportsJSON, key string, port int, autoUpdate bool, sudoPassword string) string {
|
||||
conn, err := nodeConn()
|
||||
if err != nil {
|
||||
return failure(err, nil)
|
||||
@@ -177,11 +174,6 @@ func NodeApply(channel, transportsJSON, key string, port int, autoUpdate bool, s
|
||||
return failure(err, nil)
|
||||
}
|
||||
ch := provision.Channel{ID: channel, Transports: ts, Key: key, Port: port, AutoUpdate: autoUpdate}
|
||||
if cookieHeader != "" {
|
||||
if ch.Cookies, err = provision.ChannelCookies(ts, cookieHeader); err != nil {
|
||||
return failure(err, nil)
|
||||
}
|
||||
}
|
||||
appendLog("[NODE] Установка канала " + channel)
|
||||
if err := conn.Apply(ch, sudoPassword); err != nil {
|
||||
appendLog("[NODE] Установка не удалась")
|
||||
@@ -191,30 +183,6 @@ func NodeApply(channel, transportsJSON, key string, port int, autoUpdate bool, s
|
||||
return result(nil)
|
||||
}
|
||||
|
||||
// NodeSetCookies gives an installed channel's node a new Yandex sign-in
|
||||
// and restarts it.
|
||||
func NodeSetCookies(channel, documentURL, cookieHeader, sudoPassword string) string {
|
||||
conn, err := nodeConn()
|
||||
if err != nil {
|
||||
return failure(err, nil)
|
||||
}
|
||||
cookies, _, err := provision.CookieStore(documentURL, cookieHeader)
|
||||
if err != nil {
|
||||
return failure(err, nil)
|
||||
}
|
||||
if err := conn.SetCookies(channel, cookies, sudoPassword); err != nil {
|
||||
return failure(err, map[string]interface{}{"sudo": errors.Is(err, provision.ErrSudoPassword)})
|
||||
}
|
||||
appendLog("[NODE] Вход в Яндекс передан каналу " + channel)
|
||||
return result(nil)
|
||||
}
|
||||
|
||||
// NodeSignedIn reports whether a WebView Cookie header holds a Yandex login.
|
||||
func NodeSignedIn(cookieHeader string) bool {
|
||||
_, signedIn, err := provision.CookieStore("x", cookieHeader)
|
||||
return err == nil && signedIn
|
||||
}
|
||||
|
||||
// NodeRemove deletes the channel from the VDS (a failed verification's
|
||||
// cleanup).
|
||||
func NodeRemove(channel, sudoPassword string) string {
|
||||
|
||||
+4
-27
@@ -11,8 +11,8 @@ package main
|
||||
// Request: {"id": 1, "method": "connect", "params": {...}}
|
||||
// Response: {"id": 1, "ok": true, ...} or {"id": 1, "ok": false, "error": "..."}
|
||||
//
|
||||
// Secrets (SSH and sudo passwords, private key, channel key, Yandex
|
||||
// cookies) arrive only on stdin and never go to the log or the command line.
|
||||
// Secrets (SSH and sudo passwords, private key, channel key) arrive only
|
||||
// on stdin and never go to the log or the command line.
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
@@ -45,11 +45,9 @@ type wizardParams struct {
|
||||
HostKey string `json:"hostKey"`
|
||||
Channel string `json:"channel"`
|
||||
ChannelPort int `json:"channelPort"`
|
||||
WithCookies bool `json:"withCookies"`
|
||||
DocumentURL string `json:"documentUrl"`
|
||||
Key string `json:"key"`
|
||||
SudoPassword string `json:"sudoPassword"`
|
||||
Cookies string `json:"cookies"`
|
||||
Name string `json:"name"`
|
||||
// Transports are the channel's carriers besides direct. Without them,
|
||||
// DocumentURL alone means a Yandex document (older apps).
|
||||
@@ -65,7 +63,7 @@ func (p wizardParams) transports() []provision.ChannelTransport {
|
||||
return p.Transports
|
||||
}
|
||||
|
||||
// channel is the channel the request describes, key and cookies aside.
|
||||
// channel is the channel the request describes, its key aside.
|
||||
func (p wizardParams) channel() provision.Channel {
|
||||
return provision.Channel{ID: p.Channel, Transports: p.transports(), Port: p.ChannelPort, AutoUpdate: p.AutoUpdate}
|
||||
}
|
||||
@@ -160,7 +158,7 @@ func (w *nodeWizard) handle(req wizardRequest) map[string]interface{} {
|
||||
if err != nil {
|
||||
return wizardFailure(err, nil)
|
||||
}
|
||||
plan, err := conn.Plan(p.channel(), p.WithCookies)
|
||||
plan, err := conn.Plan(p.channel())
|
||||
if err != nil {
|
||||
return wizardFailure(err, nil)
|
||||
}
|
||||
@@ -172,28 +170,10 @@ func (w *nodeWizard) handle(req wizardRequest) map[string]interface{} {
|
||||
}
|
||||
ch := p.channel()
|
||||
ch.Key = p.Key
|
||||
if p.Cookies != "" {
|
||||
if ch.Cookies, err = provision.ChannelCookies(ch.Transports, p.Cookies); err != nil {
|
||||
return wizardFailure(err, nil)
|
||||
}
|
||||
}
|
||||
if err := conn.Apply(ch, p.SudoPassword); err != nil {
|
||||
return wizardFailure(err, map[string]interface{}{"sudo": errors.Is(err, provision.ErrSudoPassword)})
|
||||
}
|
||||
return wizardOK(nil)
|
||||
case "setCookies":
|
||||
conn, err := w.connected()
|
||||
if err != nil {
|
||||
return wizardFailure(err, nil)
|
||||
}
|
||||
cookies, _, err := provision.CookieStore(p.DocumentURL, p.Cookies)
|
||||
if err != nil {
|
||||
return wizardFailure(err, nil)
|
||||
}
|
||||
if err := conn.SetCookies(p.Channel, cookies, p.SudoPassword); err != nil {
|
||||
return wizardFailure(err, map[string]interface{}{"sudo": errors.Is(err, provision.ErrSudoPassword)})
|
||||
}
|
||||
return wizardOK(nil)
|
||||
case "remove":
|
||||
conn, err := w.connected()
|
||||
if err != nil {
|
||||
@@ -203,9 +183,6 @@ func (w *nodeWizard) handle(req wizardRequest) map[string]interface{} {
|
||||
return wizardFailure(err, map[string]interface{}{"sudo": errors.Is(err, provision.ErrSudoPassword)})
|
||||
}
|
||||
return wizardOK(nil)
|
||||
case "signedIn":
|
||||
_, signedIn, err := provision.CookieStore("x", p.Cookies)
|
||||
return wizardOK(map[string]interface{}{"signedIn": err == nil && signedIn})
|
||||
case "checkDocument":
|
||||
return w.checkDocument(p.DocumentURL)
|
||||
case "createRooms":
|
||||
|
||||
+1
-7
@@ -55,7 +55,7 @@ func TestNodeWizardProtocolLines(t *testing.T) {
|
||||
|
||||
func TestNodeWizardNeedsConnection(t *testing.T) {
|
||||
w := newNodeWizard()
|
||||
for _, m := range []string{"plan", "apply", "setCookies", "remove"} {
|
||||
for _, m := range []string{"plan", "apply", "remove"} {
|
||||
r := wizardCall(t, w, m, map[string]string{"channel": "c1"})
|
||||
if r["ok"] != false || r["error"] != "нет подключения к серверу" {
|
||||
t.Fatalf("%s: %v", m, r)
|
||||
@@ -137,12 +137,6 @@ func TestNodeWizardShareLinkAndSignIn(t *testing.T) {
|
||||
if r := wizardCall(t, w, "shareLink", wizardParams{DocumentURL: doc, Key: key}); r["ok"] != false {
|
||||
t.Fatalf("no host: %v", r)
|
||||
}
|
||||
if r := wizardCall(t, w, "signedIn", wizardParams{Cookies: "yandexuid=1; Session_id=s"}); r["signedIn"] != true {
|
||||
t.Fatalf("signed in: %v", r)
|
||||
}
|
||||
if r := wizardCall(t, w, "signedIn", wizardParams{Cookies: "yandexuid=1"}); r["signedIn"] != false {
|
||||
t.Fatalf("anonymous: %v", r)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNodeWizardShareLinkWithChosenTransports(t *testing.T) {
|
||||
|
||||
@@ -134,7 +134,7 @@ func TestInstallOnVDS(t *testing.T) {
|
||||
{Type: "mailru", URL: "https://cloud.mail.ru/public/AbCd/EfGhIjKlM"},
|
||||
{Type: "cupsonline", URL: "WyJyb29tLTEiLCJyb29tLTIiXQ"},
|
||||
}
|
||||
plan, err := c.Plan(Channel{ID: id, Transports: volga, AutoUpdate: true}, true)
|
||||
plan, err := c.Plan(Channel{ID: id, Transports: volga, AutoUpdate: true})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -147,11 +147,7 @@ func TestInstallOnVDS(t *testing.T) {
|
||||
t.Fatalf("plan actions: %s", actions)
|
||||
}
|
||||
key, _ := NewKey()
|
||||
cookies, signedIn, err := CookieStore(os.Getenv("OPENFLUX_TEST_DOC"), "Session_id=test-login-value; spravka=pass")
|
||||
if err != nil || !signedIn {
|
||||
t.Fatalf("CookieStore: %v %v", signedIn, err)
|
||||
}
|
||||
ch := Channel{ID: id, Transports: volga, Key: key, Port: plan.Port, Cookies: cookies, AutoUpdate: true}
|
||||
ch := Channel{ID: id, Transports: volga, Key: key, Port: plan.Port, AutoUpdate: true}
|
||||
|
||||
if err := c.Apply(ch, "wrong-password"); !errors.Is(err, ErrSudoPassword) {
|
||||
t.Fatalf("wrong sudo password: got %v", err)
|
||||
@@ -174,30 +170,14 @@ func TestInstallOnVDS(t *testing.T) {
|
||||
if out, _, _ := c.run("ps -eo args", nil); strings.Contains(string(out), key) {
|
||||
t.Fatal("channel key visible in the process list")
|
||||
}
|
||||
if out, _, _ := c.run("stat -c '%a %U' /var/lib/openflux-node/"+id+"/cookies.json", nil); strings.TrimSpace(string(out)) != "600 openflux-node" {
|
||||
t.Fatalf("cookies.json: %q", out)
|
||||
}
|
||||
if out, _, _ := c.run("sudo -S -p '' cat /var/lib/openflux-node/"+id+"/cookies.json", []byte(userPass+"\n")); !strings.Contains(string(out), "test-login-value") {
|
||||
t.Fatalf("cookies.json content: %q", out)
|
||||
}
|
||||
if out, _, _ := c.run("ps -eo args; sudo -S -p '' journalctl -u openflux-node@"+id+" --no-pager", []byte(userPass+"\n")); strings.Contains(string(out), "test-login-value") {
|
||||
t.Fatal("the Yandex login leaked into ps or the node's log")
|
||||
}
|
||||
fresh, _, _ := CookieStore(os.Getenv("OPENFLUX_TEST_DOC"), "Session_id=renewed-login")
|
||||
if err := c.SetCookies(id, fresh, userPass); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if out, _, _ := c.run("sudo -S -p '' cat /var/lib/openflux-node/"+id+"/cookies.json", []byte(userPass+"\n")); !strings.Contains(string(out), "renewed-login") {
|
||||
t.Fatalf("set-cookies did not replace the login: %q", out)
|
||||
}
|
||||
if _, err := c.Plan(Channel{ID: id, Transports: volga}, true); err == nil {
|
||||
if _, err := c.Plan(Channel{ID: id, Transports: volga}); err == nil {
|
||||
t.Fatal("planning an existing channel must fail")
|
||||
}
|
||||
again, err := c.Plan(Channel{ID: "other", Port: plan.Port}, false)
|
||||
again, err := c.Plan(Channel{ID: "other", Port: plan.Port})
|
||||
if err == nil {
|
||||
t.Fatalf("the channel's port must count as taken: %+v", again)
|
||||
}
|
||||
next, err := c.Plan(Channel{ID: "other"}, false)
|
||||
next, err := c.Plan(Channel{ID: "other"})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -265,7 +245,7 @@ func TestInstallOnVDS(t *testing.T) {
|
||||
t.Fatalf("autoupdate on: %s", out)
|
||||
}
|
||||
// An app with an older pinned script must not take the server back.
|
||||
older, err := c.Plan(Channel{ID: "other"}, false)
|
||||
older, err := c.Plan(Channel{ID: "other"})
|
||||
if err != nil || older.Core != "node-v1.1.0" {
|
||||
t.Fatalf("plan after an update: %+v %v", older, err)
|
||||
}
|
||||
@@ -275,7 +255,7 @@ func TestInstallOnVDS(t *testing.T) {
|
||||
if err := c.FetchScript(Script{URL: base + "/other-install.sh", SHA256: ScriptHash(otherRepo)}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
switched, err := c.Plan(Channel{ID: "other"}, false)
|
||||
switched, err := c.Plan(Channel{ID: "other"})
|
||||
if err != nil || switched.Core != "node-v1.0.1" || !strings.Contains(strings.Join(switched.Actions, "\n"), "someone/OpenFlux") {
|
||||
t.Fatalf("plan from another repository: %+v %v", switched, err)
|
||||
}
|
||||
@@ -296,7 +276,7 @@ func TestInstallOnVDS(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, name := range []string{"first", "second"} {
|
||||
p, err := c.Plan(Channel{ID: name, AutoUpdate: true}, false)
|
||||
p, err := c.Plan(Channel{ID: name, AutoUpdate: true})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
+2
-42
@@ -14,7 +14,6 @@ import (
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"crypto/sha256"
|
||||
"encoding/base64"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
@@ -106,9 +105,6 @@ type Channel struct {
|
||||
Key string
|
||||
// Port is direct's port; 0 in a plan lets the script pick one.
|
||||
Port int
|
||||
// Cookies is the channel's Yandex sign-in for the node: the core's
|
||||
// cookie store JSON, base64-encoded (see CookieStore). Empty: none.
|
||||
Cookies string
|
||||
// AutoUpdate turns the server's core updater on, or off: it is one
|
||||
// timer for every channel on the server.
|
||||
AutoUpdate bool
|
||||
@@ -299,15 +295,12 @@ func (c *Conn) Probe() (*Probe, error) {
|
||||
}
|
||||
|
||||
// Plan asks what apply would change: ch without its key, Port 0 lets the
|
||||
// script pick one. withCookies adds the Yandex sign-in step.
|
||||
func (c *Conn) Plan(ch Channel, withCookies bool) (*Plan, error) {
|
||||
// script pick one.
|
||||
func (c *Conn) Plan(ch Channel) (*Plan, error) {
|
||||
cfg, err := ch.config()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if withCookies {
|
||||
cfg += "cookies=yes\n"
|
||||
}
|
||||
var p Plan
|
||||
if err := c.script_("plan", []byte(cfg), &p); err != nil {
|
||||
return nil, err
|
||||
@@ -326,42 +319,9 @@ func (c *Conn) Apply(ch Channel, sudoPassword string) error {
|
||||
return err
|
||||
}
|
||||
cfg += "key=" + ch.Key + "\n"
|
||||
if ch.Cookies != "" {
|
||||
cfg += "cookies=" + ch.Cookies + "\n"
|
||||
}
|
||||
return c.asRoot("apply", cfg, sudoPassword)
|
||||
}
|
||||
|
||||
// SetCookies replaces an installed channel's Yandex sign-in (base64 cookie
|
||||
// store JSON, see CookieStore) and restarts its node.
|
||||
func (c *Conn) SetCookies(channel, cookies, sudoPassword string) error {
|
||||
return c.asRoot("set-cookies", "channel="+channel+"\ncookies="+cookies+"\n", sudoPassword)
|
||||
}
|
||||
|
||||
// CookieStore turns a Cookie header ("a=1; b=2", as the WebView keeps it
|
||||
// for the document) into what Channel.Cookies takes: the core's cookie
|
||||
// store JSON, keyed by the document URL like the node looks it up, then
|
||||
// base64. signedIn reports whether the header holds a Yandex login.
|
||||
func CookieStore(documentURL, header string) (cookies string, signedIn bool, err error) {
|
||||
jar := map[string]string{}
|
||||
for _, part := range strings.Split(header, ";") {
|
||||
name, value, ok := strings.Cut(strings.TrimSpace(part), "=")
|
||||
if !ok || name == "" || strings.ContainsAny(name+value, "\r\n") {
|
||||
continue
|
||||
}
|
||||
jar[name] = value
|
||||
}
|
||||
if len(jar) == 0 {
|
||||
return "", false, errors.New("нет cookies Яндекса")
|
||||
}
|
||||
raw, err := json.Marshal(map[string]map[string]string{documentURL: jar})
|
||||
if err != nil {
|
||||
return "", false, err
|
||||
}
|
||||
_, signedIn = jar["Session_id"]
|
||||
return base64.StdEncoding.EncodeToString(raw), signedIn, nil
|
||||
}
|
||||
|
||||
// Remove stops and deletes the channel; the last one also removes the
|
||||
// binaries, unit and system user the script installed.
|
||||
func (c *Conn) Remove(channel, sudoPassword string) error {
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
package provision
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"os"
|
||||
"regexp"
|
||||
"testing"
|
||||
@@ -66,22 +64,3 @@ func TestSudoRefusal(t *testing.T) {
|
||||
t.Fatal("unexpected refusal")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCookieStoreFormat(t *testing.T) {
|
||||
b64, signedIn, err := CookieStore("https://docs.yandex.ru/edit/d/x", " Session_id=a=b ; yandexuid=1;bad;\nx=y")
|
||||
if err != nil || !signedIn {
|
||||
t.Fatalf("%v %v", signedIn, err)
|
||||
}
|
||||
raw, _ := base64.StdEncoding.DecodeString(b64)
|
||||
var store map[string]map[string]string
|
||||
if err := json.Unmarshal(raw, &store); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
jar := store["https://docs.yandex.ru/edit/d/x"]
|
||||
if jar["Session_id"] != "a=b" || jar["yandexuid"] != "1" || len(jar) != 3 {
|
||||
t.Fatalf("jar = %v", jar)
|
||||
}
|
||||
if _, signedIn, _ := CookieStore("u", "yandexuid=1"); signedIn {
|
||||
t.Fatal("no Session_id means not signed in")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -133,19 +133,3 @@ func transportLines(ts []ChannelTransport) string {
|
||||
}
|
||||
return b.String()
|
||||
}
|
||||
|
||||
// ChannelCookies turns the app's Cookie header for the channel's Yandex
|
||||
// document into what Channel.Cookies takes (see CookieStore), keyed by the
|
||||
// document as the node's config names it.
|
||||
func ChannelCookies(ts []ChannelTransport, header string) (string, error) {
|
||||
ts, err := CheckTransports(ts)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
doc := TransportURL(ts, "vyandex")
|
||||
if doc == "" {
|
||||
return "", errors.New("вход в Яндекс нужен только каналу с документом Яндекса")
|
||||
}
|
||||
cookies, _, err := CookieStore(doc, header)
|
||||
return cookies, err
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user