mirror of
https://github.com/p1neappleXpress/OpenFlux.git
synced 2026-10-02 05:04:39 +08:00
phphost: resend a file the host cuts short; node panel address; chosen-token check (#146)
* phphost: a file the host cuts short is sent again; 'auto' leaves a failing TLS data channel for plain FTP InfinityFree's Pure-FTPd answered '451 Transfer aborted' about 640 KB into the 2 MB link parser (assets/share.wasm.gz) over a TLS data channel, and that one drop failed the whole install; the same files went up whole in plain FTP with curl. - each file is tried up to 4 times, connecting again before each try (after a drop the server often closes the control connection too) - with TLS 'auto', two failed sends over a TLS data channel go on in plain FTP, as the host would have got with no TLS at all; the install then reports the security as 'none' (the password crossed in the clear). 'explicit'/'implicit' never get weaker - the TLS config has a session cache: servers that require data connections to resume the control connection's TLS session refused them before - the page's link parser is optional: a host that will not take it gets a working node, and Installed.skipped says so - progress has 'retry' and 'skipped' phases with the server's own words (note) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * phphost: 'page' gives the node's control panel address (auto=0: opening it only looks) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * phphost: a chosen token must be 8-64 of A-Z a-z 0-9 - _ (it goes into config.php and every address) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * CHANGELOG: the FTP upload-retry fix and the page/token additions, under 0.3.0 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: p1neappleXpress <a@a.a> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
p1neappleXpress
parent
acac97b07d
commit
d245db7561
@@ -81,6 +81,17 @@ All notable changes to the OpenFlux core. Format loosely follows
|
||||
|
||||
### Fixed
|
||||
|
||||
- The hosting install no longer fails when the host cuts a transfer short.
|
||||
InfinityFree's Pure-FTPd aborted the 2 MB link parser part way through over a
|
||||
TLS data channel (`451 Transfer aborted`), which failed the whole install;
|
||||
the same files went up whole in plain FTP. Each file is now sent again (up to
|
||||
four tries, reconnecting between them); with TLS `auto`, two failures over a
|
||||
TLS data channel go on in plain FTP, as a host with no TLS would have, and the
|
||||
reported security becomes `none`; the link parser is optional, so a host that
|
||||
will not take it still gets a working node. `provision/phphost` also gained a
|
||||
`page` call (the node's control-panel address, `auto=0` so opening it only
|
||||
looks) and checks that a chosen token is 8–64 of `A–Z a–z 0–9 - _`.
|
||||
|
||||
- The mode-without-a-server node now survives any host's limits and keeps the
|
||||
tunnel up across generations, found on a local emulation of a free host
|
||||
(Apache + PHP-FPM, a 60 s CPU cap, the host's disabled functions, a hidden
|
||||
|
||||
@@ -46,7 +46,8 @@ func RoomURL(uuid string) string { return "https://interview.cups.online/live-co
|
||||
// check does the site answer as a node, can the host run it -> Status
|
||||
// start run the node on Target (waits until it reports) -> NodeState
|
||||
// stop end the node (the whole chain)
|
||||
// node is the node on Target running -> NodeState
|
||||
// node is the node on Target running, which generation -> NodeState
|
||||
// page the node's control panel for a browser (auto=0) -> {"url"}
|
||||
// newRoom make a cups.online room for the node and clients -> {"room","url"}
|
||||
// link the openflux:// link clients scan for this node -> share.Result
|
||||
func Call(ctx context.Context, method string, raw json.RawMessage, progress func(Progress)) Result {
|
||||
@@ -111,6 +112,12 @@ func Call(ctx context.Context, method string, raw json.RawMessage, progress func
|
||||
return Failed(err)
|
||||
}
|
||||
return Done(ns)
|
||||
case "page":
|
||||
u, err := site().PageURL(p.Target)
|
||||
if err != nil {
|
||||
return Failed(err)
|
||||
}
|
||||
return Done(map[string]string{"url": u})
|
||||
case "newRoom":
|
||||
packed, err := NewRoom(ctx)
|
||||
if err != nil {
|
||||
|
||||
@@ -153,3 +153,24 @@ func TestCallStartWaitsForTheNode(t *testing.T) {
|
||||
}
|
||||
_ = time.Second
|
||||
}
|
||||
|
||||
func TestCallPageIsTheNodesPanelThatOnlyLooks(t *testing.T) {
|
||||
for _, c := range []struct{ carrier, file, target string }{
|
||||
{"mailru", "/mailruexit.php", "https://cloud.mail.ru/public/Vuri/d5nuZ5aQp"},
|
||||
{"cupsonline", "/cupsexit.php", "https://interview.cups.online/live-coding/?room=0a1b2c3d-1111-2222-3333-444455556666"},
|
||||
} {
|
||||
raw, _ := json.Marshal(map[string]string{"url": "https://site.example.org/", "token": "KEY1", "carrier": c.carrier, "target": c.target})
|
||||
res := Call(context.Background(), "page", raw, nil)
|
||||
if !res.OK {
|
||||
t.Fatalf("%s: %+v", c.carrier, res)
|
||||
}
|
||||
u, err := url.Parse(res.Data.(map[string]string)["url"])
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
q := u.Query()
|
||||
if u.Host != "site.example.org" || u.Path != c.file || q.Get("k") != "KEY1" || q.Get("url") != c.target || q.Get("auto") != "0" {
|
||||
t.Errorf("%s: page = %s", c.carrier, u)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,6 +24,8 @@ type fakeFTP struct {
|
||||
dirs map[string]bool // "/htdocs"
|
||||
readOnly bool // STOR answers 550
|
||||
truncate bool // STOR keeps only half the bytes
|
||||
abort map[string]int // path -> STORs left to cut short with "451 Transfer aborted" (as InfinityFree's Pure-FTPd did)
|
||||
stors map[string]int // path -> STORs begun
|
||||
}
|
||||
|
||||
func newFakeFTP(t *testing.T, user, pass string, dirs ...string) *fakeFTP {
|
||||
@@ -31,7 +33,8 @@ func newFakeFTP(t *testing.T, user, pass string, dirs ...string) *fakeFTP {
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
f := &fakeFTP{ln: ln, user: user, pass: pass, files: map[string][]byte{}, dirs: map[string]bool{"/": true}}
|
||||
f := &fakeFTP{ln: ln, user: user, pass: pass, files: map[string][]byte{}, dirs: map[string]bool{"/": true},
|
||||
abort: map[string]int{}, stors: map[string]int{}}
|
||||
for _, d := range dirs {
|
||||
f.dirs["/"+strings.Trim(d, "/")] = true
|
||||
}
|
||||
@@ -164,6 +167,23 @@ func (f *fakeFTP) session(c net.Conn) {
|
||||
}
|
||||
say("150 send it")
|
||||
conn, _ := pasv.Accept()
|
||||
f.mu.Lock()
|
||||
f.stors[p]++
|
||||
cut := f.abort[p] > 0
|
||||
if cut {
|
||||
f.abort[p]--
|
||||
}
|
||||
f.mu.Unlock()
|
||||
if cut { // take a little, keep it, and hang up on the rest
|
||||
part := make([]byte, 1024)
|
||||
n, _ := io.ReadFull(conn, part)
|
||||
conn.Close()
|
||||
f.mu.Lock()
|
||||
f.files[p] = part[:n]
|
||||
f.mu.Unlock()
|
||||
say(`451 Transfer aborted\n0.458 seconds (measured here), 1.40 Mbytes per second`)
|
||||
continue
|
||||
}
|
||||
b, _ := io.ReadAll(conn)
|
||||
conn.Close()
|
||||
f.mu.Lock()
|
||||
|
||||
+104
-17
@@ -80,8 +80,11 @@ func dial(ctx context.Context, t FTP) (*session, error) {
|
||||
sec string
|
||||
}
|
||||
host := strings.TrimSpace(t.Host)
|
||||
verified := &tls.Config{ServerName: host}
|
||||
lax := &tls.Config{ServerName: host, InsecureSkipVerify: true}
|
||||
// A session cache lets data connections resume the control connection's TLS session: servers
|
||||
// that insist on it refuse a data connection that starts a new one.
|
||||
cache := tls.NewLRUClientSessionCache(16)
|
||||
verified := &tls.Config{ServerName: host, ClientSessionCache: cache}
|
||||
lax := &tls.Config{ServerName: host, InsecureSkipVerify: true, ClientSessionCache: cache}
|
||||
base := []ftp.DialOption{ftp.DialWithContext(ctx), ftp.DialWithTimeout(20 * time.Second)}
|
||||
var attempts []attempt
|
||||
switch t.TLS {
|
||||
@@ -170,6 +173,12 @@ type Probe struct {
|
||||
|
||||
var tokenRe = regexp.MustCompile(`define\('PHPBOX_TOKEN',\s*'([0-9A-Za-z_-]+)'\)`)
|
||||
|
||||
var tokenShape = regexp.MustCompile(`^[0-9A-Za-z_-]{8,64}$`)
|
||||
|
||||
// TokenOK says whether a token someone chose can guard a node: it goes into config.php between quotes and into
|
||||
// every page address, so only letters, digits, '-' and '_', and long enough not to be guessed.
|
||||
func TokenOK(token string) bool { return tokenShape.MatchString(token) }
|
||||
|
||||
// ProbeHost logs in, finds the web root and checks it can be written to.
|
||||
func ProbeHost(ctx context.Context, t FTP) (*Probe, error) {
|
||||
s, err := dial(ctx, t)
|
||||
@@ -309,12 +318,14 @@ func join(dir, name string) string {
|
||||
|
||||
// Progress is reported while Deploy uploads.
|
||||
type Progress struct {
|
||||
Phase string `json:"phase"` // "connect" | "probe" | "upload" | "done"
|
||||
Phase string `json:"phase"` // "connect" | "probe" | "upload" | "retry" | "skipped" | "done"
|
||||
File string `json:"file,omitempty"`
|
||||
N int `json:"n"` // files finished
|
||||
Of int `json:"of"` // files in all
|
||||
BytesDone int64 `json:"bytes_done"` // over all files
|
||||
BytesTotal int64 `json:"bytes_total"`
|
||||
// Retry, skipped: what went wrong and what is tried next, for the app's log.
|
||||
Note string `json:"note,omitempty"`
|
||||
}
|
||||
|
||||
// Installed is what Deploy answers.
|
||||
@@ -325,8 +336,22 @@ type Installed struct {
|
||||
Bytes int64 `json:"bytes"`
|
||||
Security string `json:"security"`
|
||||
Reused bool `json:"token_reused"` // the node was installed before; its token was kept
|
||||
// Skipped: optional files the host would not take (the page's link parser); the node runs without them.
|
||||
Skipped []string `json:"skipped,omitempty"`
|
||||
}
|
||||
|
||||
const (
|
||||
uploadTries = 4 // times one file is sent before the install gives up on it
|
||||
tlsTriesOnFile = 2 // failed sends over a TLS data channel before "auto" goes on in plain FTP
|
||||
optionalFileWhy = "the page's link parser: the node runs without it, the page reads no links"
|
||||
)
|
||||
|
||||
// retryPause is the wait before a file is sent again (a var: tests shorten it).
|
||||
var retryPause = 2 * time.Second
|
||||
|
||||
// optional are files a node runs without: a host that will not take them gets the rest.
|
||||
var optional = map[string]bool{"assets/share.wasm.gz": true}
|
||||
|
||||
type countingReader struct {
|
||||
r io.Reader
|
||||
done *int64
|
||||
@@ -348,6 +373,9 @@ func Deploy(ctx context.Context, t FTP, token string, progress func(Progress)) (
|
||||
if progress == nil {
|
||||
progress = func(Progress) {}
|
||||
}
|
||||
if token != "" && !TokenOK(token) {
|
||||
return nil, fail(CodeBadParams, "token", "phphost: a token is 8 to 64 of A-Z a-z 0-9 - _", nil)
|
||||
}
|
||||
progress(Progress{Phase: "connect"})
|
||||
s, err := dial(ctx, t)
|
||||
if err != nil {
|
||||
@@ -383,27 +411,86 @@ func Deploy(ctx context.Context, t FTP, token string, progress func(Progress)) (
|
||||
_ = s.c.MakeDir(join(p.Dir, d)) // exists already on a reinstall
|
||||
}
|
||||
var done int64
|
||||
var skipped []string
|
||||
security := s.security
|
||||
for i, f := range files {
|
||||
if ctx.Err() != nil {
|
||||
return nil, fail(CodeUpload, f.Path, "phphost: cancelled", ctx.Err())
|
||||
}
|
||||
start := done
|
||||
cr := &countingReader{r: bytes.NewReader(f.Data), done: &done}
|
||||
cr.tick = func() {
|
||||
progress(Progress{Phase: "upload", File: f.Path, N: i, Of: len(files), BytesDone: done, BytesTotal: total})
|
||||
}
|
||||
if err := s.c.Stor(join(p.Dir, f.Path), cr); err != nil {
|
||||
return nil, fail(CodeUpload, f.Path, "phphost: upload failed", err)
|
||||
for try := 1; ; try++ {
|
||||
if ctx.Err() != nil {
|
||||
return nil, fail(CodeUpload, f.Path, "phphost: cancelled", ctx.Err())
|
||||
}
|
||||
done = start
|
||||
err := s.put(join(p.Dir, f.Path), f.Data, func() {
|
||||
progress(Progress{Phase: "upload", File: f.Path, N: i, Of: len(files), BytesDone: done, BytesTotal: total})
|
||||
}, &done)
|
||||
if err == nil {
|
||||
break
|
||||
}
|
||||
// Free hosts drop transfers (InfinityFree's Pure-FTPd: "451 Transfer aborted" part way through a 2 MB file over
|
||||
// a TLS data channel, while curl in plain FTP sent it whole). Connect again and send the file again; after
|
||||
// two failures over TLS, "auto" goes on in plain FTP, as it would have if the host had no TLS at all.
|
||||
if try >= uploadTries {
|
||||
if optional[f.Path] {
|
||||
skipped = append(skipped, f.Path)
|
||||
done = start + int64(len(f.Data))
|
||||
progress(Progress{Phase: "skipped", File: f.Path, N: i + 1, Of: len(files), BytesDone: done, BytesTotal: total,
|
||||
Note: fmt.Sprintf("%s: %v (%s)", f.Path, err, optionalFileWhy)})
|
||||
break
|
||||
}
|
||||
return nil, fail(CodeUpload, f.Path, "phphost: upload failed", err)
|
||||
}
|
||||
mode := redialMode(t.TLS, s.security, try)
|
||||
note := fmt.Sprintf("%s: %v; try %d of %d", f.Path, err, try+1, uploadTries)
|
||||
if mode == "none" && s.security != SecurityNone {
|
||||
note += ", now in plain FTP"
|
||||
}
|
||||
progress(Progress{Phase: "retry", File: f.Path, N: i, Of: len(files), BytesDone: start, BytesTotal: total, Note: note})
|
||||
s.close()
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return nil, fail(CodeUpload, f.Path, "phphost: cancelled", ctx.Err())
|
||||
case <-time.After(retryPause):
|
||||
}
|
||||
again := t
|
||||
again.TLS = mode
|
||||
if s, err = dial(ctx, again); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer s.close()
|
||||
if s.security == SecurityNone {
|
||||
security = SecurityNone // the password has crossed in the clear: say so
|
||||
}
|
||||
}
|
||||
done = start + int64(len(f.Data))
|
||||
progress(Progress{Phase: "upload", File: f.Path, N: i + 1, Of: len(files), BytesDone: done, BytesTotal: total})
|
||||
// A truncated upload is the usual silent failure on shared hosting: check the size where the server can say.
|
||||
if sz, err := s.c.FileSize(join(p.Dir, f.Path)); err == nil && sz != int64(len(f.Data)) {
|
||||
return nil, fail(CodeUpload, f.Path, fmt.Sprintf("phphost: %s arrived as %d bytes, sent %d", f.Path, sz, len(f.Data)), nil)
|
||||
}
|
||||
}
|
||||
progress(Progress{Phase: "done", N: len(files), Of: len(files), BytesDone: total, BytesTotal: total})
|
||||
return &Installed{Dir: p.Dir, Token: token, Files: len(files), Bytes: total, Security: s.security, Reused: reused}, nil
|
||||
return &Installed{Dir: p.Dir, Token: token, Files: len(files) - len(skipped), Bytes: total, Security: security, Reused: reused, Skipped: skipped}, nil
|
||||
}
|
||||
|
||||
// put sends one file whole, then checks its size where the server can say: a truncated upload is the usual
|
||||
// silent failure on shared hosting.
|
||||
func (s *session) put(remote string, data []byte, tick func(), done *int64) error {
|
||||
cr := &countingReader{r: bytes.NewReader(data), done: done, tick: tick}
|
||||
if err := s.c.Stor(remote, cr); err != nil {
|
||||
return err
|
||||
}
|
||||
if sz, err := s.c.FileSize(remote); err == nil && sz != int64(len(data)) {
|
||||
return fmt.Errorf("arrived as %d bytes, sent %d", sz, len(data))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// redialMode is the TLS mode to connect with again after a file failed on its try'th send: the same as before,
|
||||
// except that "auto" leaves a TLS data channel that failed tlsTriesOnFile times for plain FTP.
|
||||
func redialMode(asked, got string, try int) string {
|
||||
if asked == "" || asked == "auto" {
|
||||
if got == SecurityNone || try >= tlsTriesOnFile {
|
||||
return "none"
|
||||
}
|
||||
return "auto"
|
||||
}
|
||||
return asked
|
||||
}
|
||||
|
||||
// configPHP is config.php: the token as a constant (putenv is disabled on most
|
||||
|
||||
@@ -153,6 +153,7 @@ func TestDeployUploadsTheBundleAndKeepsTheTokenOnReinstall(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestDeployCatchesATruncatedUpload(t *testing.T) {
|
||||
quickRetries(t)
|
||||
srv := newFakeFTP(t, "u", "pw", "htdocs")
|
||||
srv.truncate = true
|
||||
_, err := Deploy(ctx(t), srv.target(), "", nil)
|
||||
@@ -161,6 +162,107 @@ func TestDeployCatchesATruncatedUpload(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func quickRetries(t *testing.T) {
|
||||
old := retryPause
|
||||
retryPause = time.Millisecond
|
||||
t.Cleanup(func() { retryPause = old })
|
||||
}
|
||||
|
||||
// InfinityFree's Pure-FTPd cut the 2 MB parser short ("451 Transfer aborted") and the whole install failed on it.
|
||||
func TestDeploySendsADroppedFileAgain(t *testing.T) {
|
||||
quickRetries(t)
|
||||
srv := newFakeFTP(t, "u", "pw", "htdocs")
|
||||
srv.abort["/htdocs/lib/mux.php"] = 2
|
||||
var retries []Progress
|
||||
in, err := Deploy(ctx(t), srv.target(), "", func(p Progress) {
|
||||
if p.Phase == "retry" {
|
||||
retries = append(retries, p)
|
||||
}
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, f := range bundle.Files() {
|
||||
if got := srv.files["/htdocs/"+f.Path]; string(got) != string(f.Data) {
|
||||
t.Errorf("%s was not uploaded intact (%d bytes there)", f.Path, len(got))
|
||||
}
|
||||
}
|
||||
if len(retries) != 2 || retries[0].File != "lib/mux.php" || !strings.Contains(retries[0].Note, "Transfer aborted") {
|
||||
t.Errorf("retries = %+v", retries)
|
||||
}
|
||||
if srv.stors["/htdocs/lib/mux.php"] != 3 || len(in.Skipped) != 0 {
|
||||
t.Errorf("mux.php sent %d times, skipped %v", srv.stors["/htdocs/lib/mux.php"], in.Skipped)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDeployGivesUpOnAFileTheHostKeepsRefusing(t *testing.T) {
|
||||
quickRetries(t)
|
||||
srv := newFakeFTP(t, "u", "pw", "htdocs")
|
||||
srv.abort["/htdocs/lib/mux.php"] = 100
|
||||
_, err := Deploy(ctx(t), srv.target(), "", nil)
|
||||
var e *Error
|
||||
if !errors.As(err, &e) || e.Code != CodeUpload || e.Param != "lib/mux.php" {
|
||||
t.Fatalf("err = %v", err)
|
||||
}
|
||||
if !strings.Contains(err.Error(), "451") {
|
||||
t.Errorf("the host's own words are lost: %v", err)
|
||||
}
|
||||
if n := srv.stors["/htdocs/lib/mux.php"]; n != uploadTries {
|
||||
t.Errorf("mux.php sent %d times, want %d", n, uploadTries)
|
||||
}
|
||||
}
|
||||
|
||||
// The parser only serves the page in a browser: a host that will not take it still gets a working node.
|
||||
func TestDeployGoesOnWithoutTheOptionalParser(t *testing.T) {
|
||||
quickRetries(t)
|
||||
srv := newFakeFTP(t, "u", "pw", "htdocs")
|
||||
srv.abort["/htdocs/assets/share.wasm.gz"] = 100
|
||||
var skipped []Progress
|
||||
in, err := Deploy(ctx(t), srv.target(), "", func(p Progress) {
|
||||
if p.Phase == "skipped" {
|
||||
skipped = append(skipped, p)
|
||||
}
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(in.Skipped) != 1 || in.Skipped[0] != "assets/share.wasm.gz" || len(skipped) != 1 {
|
||||
t.Fatalf("skipped = %v / %+v", in.Skipped, skipped)
|
||||
}
|
||||
for _, f := range bundle.Files() {
|
||||
if f.Path == "assets/share.wasm.gz" {
|
||||
continue
|
||||
}
|
||||
if got := srv.files["/htdocs/"+f.Path]; string(got) != string(f.Data) {
|
||||
t.Errorf("%s was not uploaded intact", f.Path)
|
||||
}
|
||||
}
|
||||
if !strings.Contains(string(srv.files["/htdocs/config.php"]), in.Token) {
|
||||
t.Error("config.php missing: the node would not run")
|
||||
}
|
||||
}
|
||||
|
||||
func TestOnlyAutoLeavesTLSForPlainFTP(t *testing.T) {
|
||||
cases := []struct {
|
||||
asked, got string
|
||||
try int
|
||||
want string
|
||||
}{
|
||||
{"auto", SecurityTLS, 1, "auto"}, // one drop: TLS again
|
||||
{"auto", SecurityTLSUnverified, 2, "none"}, // two over TLS: plain, as curl
|
||||
{"", SecurityTLS, 2, "none"},
|
||||
{"auto", SecurityNone, 1, "none"},
|
||||
{"explicit", SecurityTLS, 3, "explicit"}, // the user asked for TLS: never weaker
|
||||
{"implicit", SecurityTLS, 3, "implicit"},
|
||||
{"none", SecurityNone, 1, "none"},
|
||||
}
|
||||
for _, c := range cases {
|
||||
if got := redialMode(c.asked, c.got, c.try); got != c.want {
|
||||
t.Errorf("redialMode(%q, %q, %d) = %q, want %q", c.asked, c.got, c.try, got, c.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestRemoveDeletesOnlyWhatWasUploaded(t *testing.T) {
|
||||
srv := newFakeFTP(t, "u", "pw", "htdocs")
|
||||
srv.files["/htdocs/mysite.html"] = []byte("<p>mine</p>")
|
||||
@@ -219,3 +321,19 @@ func TestProbeFindsAWebRootBelowTheLoginFolder(t *testing.T) {
|
||||
t.Errorf("sites/shop/www: %+v %v", p, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDeployTakesAChosenTokenOnlyOfASafeShape(t *testing.T) {
|
||||
srv := newFakeFTP(t, "u", "pw", "htdocs")
|
||||
for _, bad := range []string{"short", "has space in it", "quote'breaks-config", strings.Repeat("a", 65)} {
|
||||
if _, err := Deploy(ctx(t), srv.target(), bad, nil); code(err) != CodeBadParams {
|
||||
t.Errorf("token %q was taken: %v", bad, err)
|
||||
}
|
||||
}
|
||||
if len(srv.files) != 0 {
|
||||
t.Error("files were uploaded before the token was checked")
|
||||
}
|
||||
in, err := Deploy(ctx(t), srv.target(), "My-own_key-2026", nil)
|
||||
if err != nil || in.Token != "My-own_key-2026" {
|
||||
t.Fatalf("a good chosen token: %+v, %v", in, err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -60,6 +60,12 @@ func (s *Site) endpoint(q url.Values) (string, error) {
|
||||
return u.String(), nil
|
||||
}
|
||||
|
||||
// PageURL is the node's control panel for a person's browser: its status, log, Start/Stop and the generation
|
||||
// serving now. auto=0: opening it only looks; it does not start a node that is not running.
|
||||
func (s *Site) PageURL(target string) (string, error) {
|
||||
return s.endpoint(url.Values{"url": {strings.TrimSpace(target)}, "auto": {"0"}})
|
||||
}
|
||||
|
||||
// The browser check some free hosts (InfinityFree and other iFastNet sites) put in
|
||||
// front of every page: a script decrypts a value with AES-128-CBC and sets it as the
|
||||
// cookie "__test", then reloads with ?i=1. A browser does that unseen; so can we.
|
||||
|
||||
Reference in New Issue
Block a user