Log in to the default page and ask for the page after, so a missing page leaves no session; send the password over https only (issue 1514 review)
mesh/merge-gate head 02076ebe9351 merged into trunk e2e990a0ebdb — pass: builds brother → shanks; no bus step; every machine composes with the change as it…
mesh/repo-check head 02076ebe9351 merged into trunk e2e990a0ebdb — pass: its merge-check.sh passed, each part in its toolchain: merge-check.sh (go), merge-…
mesh/delivery delivered
mesh/merge-gate head 02076ebe9351 merged into trunk e2e990a0ebdb — pass: builds brother → shanks; no bus step; every machine composes with the change as it…
mesh/repo-check head 02076ebe9351 merged into trunk e2e990a0ebdb — pass: its merge-check.sh passed, each part in its toolchain: merge-check.sh (go), merge-…
mesh/delivery delivered
This commit is contained in:
@@ -78,6 +78,9 @@ func (a *Admin) fetch(ctx context.Context, method, rawURL string, form url.Value
|
|||||||
return nil, nil, fmt.Errorf("the page %s is larger than %d bytes", rawURL, maxPage)
|
return nil, nil, fmt.Errorf("the page %s is larger than %d bytes", rawURL, maxPage)
|
||||||
}
|
}
|
||||||
if resp.StatusCode >= 400 {
|
if resp.StatusCode >= 400 {
|
||||||
|
if resp.StatusCode == http.StatusNotFound {
|
||||||
|
return nil, nil, ¬FoundError{Path: resp.Request.URL.Path}
|
||||||
|
}
|
||||||
return nil, nil, fmt.Errorf("the printer answered %s for %s", resp.Status, resp.Request.URL.Path)
|
return nil, nil, fmt.Errorf("the printer answered %s for %s", resp.Status, resp.Request.URL.Path)
|
||||||
}
|
}
|
||||||
return buf, resp.Request.URL, nil
|
return buf, resp.Request.URL, nil
|
||||||
@@ -105,9 +108,9 @@ func hasField(f *Form, name string) bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Login asks for the printer's login page, reads its login form, and posts the form's own fields with the
|
// Login asks for the printer's login page, reads its login form, and posts the form's own fields with the
|
||||||
// password in the field the form names and the form's loginurl set to the page that will be asked for. A login
|
// password in the field the form names and the form's loginurl set to the default page, which every firmware has; the page asked for is a request of its own afterwards. A login
|
||||||
// is failed when the answer is a login page again.
|
// is failed when the answer is a login page again.
|
||||||
func (a *Admin) Login(ctx context.Context, page string) error {
|
func (a *Admin) Login(ctx context.Context) error {
|
||||||
body, at, err := a.fetch(ctx, http.MethodGet, a.url(loginStart), nil)
|
body, at, err := a.fetch(ctx, http.MethodGet, a.url(loginStart), nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("the printer's admin page cannot be asked for its login: %w", err)
|
return fmt.Errorf("the printer's admin page cannot be asked for its login: %w", err)
|
||||||
@@ -120,8 +123,8 @@ func (a *Admin) Login(ctx context.Context, page string) error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("the login form's action %q is not a URL: %w", form.Action, err)
|
return fmt.Errorf("the login form's action %q is not a URL: %w", form.Action, err)
|
||||||
}
|
}
|
||||||
if target.Host != a.Host {
|
if target.Scheme != "https" || target.Host != a.Host {
|
||||||
return fmt.Errorf("the login form posts to %s, not to the printer %s: the password is not sent", target.Host, a.Host)
|
return fmt.Errorf("the login form posts to %s, not to https on the printer %s: the password is not sent", target.Scheme+"://"+target.Host, a.Host)
|
||||||
}
|
}
|
||||||
vals := url.Values{}
|
vals := url.Values{}
|
||||||
for _, f := range form.Fields {
|
for _, f := range form.Fields {
|
||||||
@@ -130,11 +133,15 @@ func (a *Admin) Login(ctx context.Context, page string) error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if _, ok := vals["loginurl"]; ok {
|
if _, ok := vals["loginurl"]; ok {
|
||||||
vals.Set("loginurl", page)
|
vals.Set("loginurl", DefaultPage)
|
||||||
}
|
}
|
||||||
vals.Set(pw.Name, a.Password)
|
vals.Set(pw.Name, a.Password)
|
||||||
answer, _, err := a.fetch(ctx, http.MethodPost, target.String(), vals)
|
answer, _, err := a.fetch(ctx, http.MethodPost, target.String(), vals)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
// The post may have opened a session before its answer failed: end it if there is one.
|
||||||
|
if home, hat, herr := a.Get(context.Background(), DefaultPage); herr == nil {
|
||||||
|
a.Logout(context.Background(), home, hat)
|
||||||
|
}
|
||||||
return fmt.Errorf("the login was not answered: %w", err)
|
return fmt.Errorf("the login was not answered: %w", err)
|
||||||
}
|
}
|
||||||
// A login page again is a form with a password field and the loginurl field; a page that was asked for may
|
// A login page again is a form with a password field and the loginurl field; a page that was asked for may
|
||||||
@@ -198,3 +205,8 @@ func (a *Admin) Logout(ctx context.Context, page []byte, at *url.URL) string {
|
|||||||
}
|
}
|
||||||
return "no logout link or form on the page: the session is left to expire on the printer"
|
return "no logout link or form on the page: the session is left to expire on the printer"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// notFoundError is the printer's 404 for a page.
|
||||||
|
type notFoundError struct{ Path string }
|
||||||
|
|
||||||
|
func (e *notFoundError) Error() string { return "the printer answered 404 Not Found for " + e.Path }
|
||||||
|
|||||||
@@ -151,7 +151,7 @@ func TestACertificateOtherThanThePinnedOneIsRefusedBeforeAnythingIsSent(t *testi
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
err = a.Login(context.Background(), DefaultPage)
|
err = a.Login(context.Background())
|
||||||
if err == nil {
|
if err == nil {
|
||||||
t.Fatal("a certificate that is not the pinned one was accepted")
|
t.Fatal("a certificate that is not the pinned one was accepted")
|
||||||
}
|
}
|
||||||
@@ -169,7 +169,7 @@ func TestACertificateOtherThanThePinnedOneIsRefusedBeforeAnythingIsSent(t *testi
|
|||||||
func TestThePinnedCertificateIsAccepted(t *testing.T) {
|
func TestThePinnedCertificateIsAccepted(t *testing.T) {
|
||||||
f := newFakePrinter(t, "B1891", "hunter2")
|
f := newFakePrinter(t, "B1891", "hunter2")
|
||||||
a, _ := NewAdmin(f.host(), strings.ToLower(f.print()), "hunter2")
|
a, _ := NewAdmin(f.host(), strings.ToLower(f.print()), "hunter2")
|
||||||
if err := a.Login(context.Background(), DefaultPage); err != nil {
|
if err := a.Login(context.Background()); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -181,7 +181,7 @@ func TestLoginUsesThePasswordFieldNameTheFormGives(t *testing.T) {
|
|||||||
f := newFakePrinter(t, name, "s3cret pass")
|
f := newFakePrinter(t, name, "s3cret pass")
|
||||||
a, _ := NewAdmin(f.host(), f.print(), "s3cret pass")
|
a, _ := NewAdmin(f.host(), f.print(), "s3cret pass")
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
if err := a.Login(ctx, "/general/other.html"); err != nil {
|
if err := a.Login(ctx); err != nil {
|
||||||
t.Fatalf("field %s: %v", name, err)
|
t.Fatalf("field %s: %v", name, err)
|
||||||
}
|
}
|
||||||
if len(f.posted) != 1 {
|
if len(f.posted) != 1 {
|
||||||
@@ -190,7 +190,7 @@ func TestLoginUsesThePasswordFieldNameTheFormGives(t *testing.T) {
|
|||||||
if got := f.posted[0].Get(name); got != "s3cret pass" {
|
if got := f.posted[0].Get(name); got != "s3cret pass" {
|
||||||
t.Errorf("field %s: the password field holds %q", name, got)
|
t.Errorf("field %s: the password field holds %q", name, got)
|
||||||
}
|
}
|
||||||
if f.posted[0].Get("loginurl") != "/general/other.html" {
|
if f.posted[0].Get("loginurl") != DefaultPage {
|
||||||
t.Errorf("field %s: the form's hidden loginurl was %q", name, f.posted[0].Get("loginurl"))
|
t.Errorf("field %s: the form's hidden loginurl was %q", name, f.posted[0].Get("loginurl"))
|
||||||
}
|
}
|
||||||
if len(f.posted[0]) != 2 {
|
if len(f.posted[0]) != 2 {
|
||||||
@@ -207,7 +207,7 @@ func TestLoginUsesThePasswordFieldNameTheFormGives(t *testing.T) {
|
|||||||
func TestAWrongPasswordIsAFailedLogin(t *testing.T) {
|
func TestAWrongPasswordIsAFailedLogin(t *testing.T) {
|
||||||
f := newFakePrinter(t, "B1891", "right")
|
f := newFakePrinter(t, "B1891", "right")
|
||||||
a, _ := NewAdmin(f.host(), f.print(), "wrong")
|
a, _ := NewAdmin(f.host(), f.print(), "wrong")
|
||||||
err := a.Login(context.Background(), DefaultPage)
|
err := a.Login(context.Background())
|
||||||
if err == nil || !strings.Contains(err.Error(), "login failed") {
|
if err == nil || !strings.Contains(err.Error(), "login failed") {
|
||||||
t.Fatalf("error = %v, want a failed login", err)
|
t.Fatalf("error = %v, want a failed login", err)
|
||||||
}
|
}
|
||||||
@@ -221,7 +221,7 @@ func TestLogoutEndsTheSessionAndAPageWithoutOneSaysSo(t *testing.T) {
|
|||||||
f := newFakePrinter(t, "B1891", "pw")
|
f := newFakePrinter(t, "B1891", "pw")
|
||||||
a, _ := NewAdmin(f.host(), f.print(), "pw")
|
a, _ := NewAdmin(f.host(), f.print(), "pw")
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
if err := a.Login(ctx, DefaultPage); err != nil {
|
if err := a.Login(ctx); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
body, at, err := a.Get(ctx, DefaultPage)
|
body, at, err := a.Get(ctx, DefaultPage)
|
||||||
@@ -250,8 +250,53 @@ func TestThePasswordIsNotPostedToAnotherHost(t *testing.T) {
|
|||||||
defer srv.Close()
|
defer srv.Close()
|
||||||
host := strings.TrimPrefix(srv.URL, "https://")
|
host := strings.TrimPrefix(srv.URL, "https://")
|
||||||
a, _ := NewAdmin(host, Fingerprint(srv.TLS.Certificates[0].Certificate[0]), "pw")
|
a, _ := NewAdmin(host, Fingerprint(srv.TLS.Certificates[0].Certificate[0]), "pw")
|
||||||
err := a.Login(context.Background(), DefaultPage)
|
err := a.Login(context.Background())
|
||||||
if err == nil || !strings.Contains(err.Error(), "not sent") {
|
if err == nil || !strings.Contains(err.Error(), "not sent") {
|
||||||
t.Fatalf("error = %v", err)
|
t.Fatalf("error = %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 1514 review: a page the printer does not have leaves no session open, and the error says the page is missing,
|
||||||
|
// not that the login failed.
|
||||||
|
func TestAMissingPageLeavesNoSessionOpenAndSaysSo(t *testing.T) {
|
||||||
|
f := newFakePrinter(t, "B1891", "pw")
|
||||||
|
_, run := printerTools(t, f, "pw")
|
||||||
|
_, err := run(map[string]any{"page": "/general/nope.html"})
|
||||||
|
if err == nil || !strings.Contains(err.Error(), "the printer has no page /general/nope.html") {
|
||||||
|
t.Fatalf("error = %v, want the printer has no page", err)
|
||||||
|
}
|
||||||
|
if strings.Contains(err.Error(), "login was not answered") {
|
||||||
|
t.Errorf("a missing page is said to be a failed login: %v", err)
|
||||||
|
}
|
||||||
|
if f.loggedInNow() != 0 {
|
||||||
|
t.Errorf("%d sessions are still open on the printer", f.loggedInNow())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1514 review: the password is posted to https on the printer's host only. A form whose action is plain http
|
||||||
|
// on the same host, or a root that redirects to plain http, is refused and nothing is posted.
|
||||||
|
func TestThePasswordIsNotPostedOverPlainHTTP(t *testing.T) {
|
||||||
|
var posted int
|
||||||
|
mux := http.NewServeMux()
|
||||||
|
page := func(action string) string {
|
||||||
|
return `<html><body><form method="post" action="` + action + `"><input type="password" name="B1891"/><input type="hidden" name="loginurl" value="/home/status.html"/></form></body></html>`
|
||||||
|
}
|
||||||
|
var host string
|
||||||
|
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if r.Method == http.MethodPost {
|
||||||
|
posted++
|
||||||
|
}
|
||||||
|
w.Write([]byte(page("http://" + host + "/home/status.html")))
|
||||||
|
})
|
||||||
|
srv := httptest.NewTLSServer(mux)
|
||||||
|
defer srv.Close()
|
||||||
|
host = strings.TrimPrefix(srv.URL, "https://")
|
||||||
|
a, _ := NewAdmin(host, Fingerprint(srv.TLS.Certificates[0].Certificate[0]), "pw")
|
||||||
|
err := a.Login(context.Background())
|
||||||
|
if err == nil || !strings.Contains(err.Error(), "password is not sent") {
|
||||||
|
t.Errorf("error = %v, want the plain http action refused", err)
|
||||||
|
}
|
||||||
|
if posted != 0 {
|
||||||
|
t.Errorf("%d posts reached the server", posted)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -323,7 +323,7 @@ func (w *deviceWorker) read(ctx context.Context, page string) (map[string]any, e
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if err := a.Login(ctx, page); err != nil {
|
if err := a.Login(ctx); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
// Log out with a context of its own: the ask's may be what ended this.
|
// Log out with a context of its own: the ask's may be what ended this.
|
||||||
@@ -334,6 +334,10 @@ func (w *deviceWorker) read(ctx context.Context, page string) (map[string]any, e
|
|||||||
if home, hat, herr := a.Get(lctx, DefaultPage); herr == nil {
|
if home, hat, herr := a.Get(lctx, DefaultPage); herr == nil {
|
||||||
a.Logout(lctx, home, hat)
|
a.Logout(lctx, home, hat)
|
||||||
}
|
}
|
||||||
|
var nf *notFoundError
|
||||||
|
if errors.As(err, &nf) {
|
||||||
|
return nil, fmt.Errorf("the printer has no page %s", page)
|
||||||
|
}
|
||||||
return nil, fmt.Errorf("page %s cannot be read: %w", page, err)
|
return nil, fmt.Errorf("page %s cannot be read: %w", page, err)
|
||||||
}
|
}
|
||||||
logout := a.Logout(lctx, body, at)
|
logout := a.Logout(lctx, body, at)
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ func TestAPinListingTwoFingerprintsAcceptsTheCertificateTheServerPresents(t *tes
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
if err := a.Login(context.Background(), DefaultPage); err != nil {
|
if err := a.Login(context.Background()); err != nil {
|
||||||
t.Fatalf("a pin listing the served certificate was refused: %v", err)
|
t.Fatalf("a pin listing the served certificate was refused: %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -59,7 +59,7 @@ func TestAPinListingTwoFingerprintsAcceptsTheCertificateTheServerPresents(t *tes
|
|||||||
func TestAPinListingOnlyOtherCertificatesStillRefuses(t *testing.T) {
|
func TestAPinListingOnlyOtherCertificatesStillRefuses(t *testing.T) {
|
||||||
f := newFakePrinter(t, "B1891", "hunter2")
|
f := newFakePrinter(t, "B1891", "hunter2")
|
||||||
a, _ := NewAdmin(f.host(), printerECDSAPrint+" , "+printerRSAPrint, "hunter2")
|
a, _ := NewAdmin(f.host(), printerECDSAPrint+" , "+printerRSAPrint, "hunter2")
|
||||||
err := a.Login(context.Background(), DefaultPage)
|
err := a.Login(context.Background())
|
||||||
if err == nil {
|
if err == nil {
|
||||||
t.Fatal("a certificate that is in no pinned fingerprint was accepted")
|
t.Fatal("a certificate that is in no pinned fingerprint was accepted")
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user