diff --git a/client.go b/client.go index 91402b1..823fa05 100644 --- a/client.go +++ b/client.go @@ -24,6 +24,10 @@ type Client struct { // redirects will be enforced. CheckRedirect func(req *Request, via []*Request) error + // GetInput, if not nil, will be called to retrieve input when the server + // requests it. + GetInput func(prompt string, sensitive bool) (string, bool) + // GetCertificate, if not nil, will be called when a server requests a certificate. // The returned certificate will be used when sending the request again. // If the certificate is nil, the request will not be sent again and @@ -141,7 +145,17 @@ func (c *Client) do(req *Request, via []*Request) (*Response, error) { return resp, ErrTooManyRedirects } return c.do(redirect, via) + } else if resp.Status.Class() == StatusClassInput { + if c.GetInput != nil { + input, ok := c.GetInput(resp.Meta, resp.Status == StatusSensitiveInput) + if ok { + req.URL.ForceQuery = true + req.URL.RawQuery = url.QueryEscape(input) + return c.do(req, via) + } + } } + return resp, nil } diff --git a/examples/auth.go b/examples/auth.go index bc12258..eb7b129 100644 --- a/examples/auth.go +++ b/examples/auth.go @@ -7,7 +7,7 @@ import ( "fmt" "log" - gmi "git.sr.ht/~adnano/go-gemini" + "git.sr.ht/~adnano/go-gemini" ) type user struct { @@ -33,15 +33,14 @@ var ( ) func main() { - var mux gmi.ServeMux - mux.HandleFunc("/", welcome) - mux.HandleFunc("/login", login) - mux.HandleFunc("/login/password", loginPassword) + var mux gemini.ServeMux + mux.HandleFunc("/", login) + mux.HandleFunc("/password", loginPassword) mux.HandleFunc("/profile", profile) mux.HandleFunc("/admin", admin) mux.HandleFunc("/logout", logout) - var server gmi.Server + var server gemini.Server if err := server.CertificateStore.Load("/var/lib/gemini/certs"); err != nil { log.Fatal(err) } @@ -53,74 +52,69 @@ func main() { } func getSession(crt *x509.Certificate) (*session, bool) { - fingerprint := gmi.Fingerprint(crt) + fingerprint := gemini.Fingerprint(crt) session, ok := sessions[fingerprint] return session, ok } -func welcome(w *gmi.ResponseWriter, r *gmi.Request) { - fmt.Fprintln(w, "Welcome to this example.") - fmt.Fprintln(w, "=> /login Login") -} - -func login(w *gmi.ResponseWriter, r *gmi.Request) { - cert, ok := gmi.Certificate(w, r) +func login(w *gemini.ResponseWriter, r *gemini.Request) { + cert, ok := gemini.Certificate(w, r) if !ok { return } - username, ok := gmi.Input(w, r, "Username") + username, ok := gemini.Input(w, r, "Username") if !ok { return } - fingerprint := gmi.Fingerprint(cert) + fingerprint := gemini.Fingerprint(cert) sessions[fingerprint] = &session{ username: username, } - gmi.Redirect(w, "/login/password") + gemini.Redirect(w, "/password") } -func loginPassword(w *gmi.ResponseWriter, r *gmi.Request) { - cert, ok := gmi.Certificate(w, r) +func loginPassword(w *gemini.ResponseWriter, r *gemini.Request) { + cert, ok := gemini.Certificate(w, r) if !ok { return } session, ok := getSession(cert) if !ok { - w.WriteStatus(gmi.StatusCertificateNotAuthorized) + w.WriteStatus(gemini.StatusCertificateNotAuthorized) return } - password, ok := gmi.SensitiveInput(w, r, "Password") + password, ok := gemini.SensitiveInput(w, r, "Password") if !ok { return } expected := logins[session.username].password if password == expected { session.authorized = true - gmi.Redirect(w, "/profile") + gemini.Redirect(w, "/profile") } else { - gmi.SensitiveInput(w, r, "Wrong password. Try again") + gemini.SensitiveInput(w, r, "Wrong password. Try again") } } -func logout(w *gmi.ResponseWriter, r *gmi.Request) { - cert, ok := gmi.Certificate(w, r) +func logout(w *gemini.ResponseWriter, r *gemini.Request) { + cert, ok := gemini.Certificate(w, r) if !ok { return } - fingerprint := gmi.Fingerprint(cert) + fingerprint := gemini.Fingerprint(cert) delete(sessions, fingerprint) fmt.Fprintln(w, "Successfully logged out.") } -func profile(w *gmi.ResponseWriter, r *gmi.Request) { - cert, ok := gmi.Certificate(w, r) +func profile(w *gemini.ResponseWriter, r *gemini.Request) { + cert, ok := gemini.Certificate(w, r) if !ok { return } session, ok := getSession(cert) if !ok { - w.WriteStatus(gmi.StatusCertificateNotAuthorized) + w.WriteStatus(gemini.StatusCertificateNotAuthorized) return } user := logins[session.username] @@ -129,19 +123,19 @@ func profile(w *gmi.ResponseWriter, r *gmi.Request) { fmt.Fprintln(w, "=> /logout Logout") } -func admin(w *gmi.ResponseWriter, r *gmi.Request) { - cert, ok := gmi.Certificate(w, r) +func admin(w *gemini.ResponseWriter, r *gemini.Request) { + cert, ok := gemini.Certificate(w, r) if !ok { return } session, ok := getSession(cert) if !ok { - w.WriteStatus(gmi.StatusCertificateNotAuthorized) + w.WriteStatus(gemini.StatusCertificateNotAuthorized) return } user := logins[session.username] if !user.admin { - w.WriteStatus(gmi.StatusCertificateNotAuthorized) + w.WriteStatus(gemini.StatusCertificateNotAuthorized) return } fmt.Fprintln(w, "Welcome to the admin portal.") diff --git a/examples/cert.go b/examples/cert.go index dc6cc40..9ab80af 100644 --- a/examples/cert.go +++ b/examples/cert.go @@ -11,13 +11,13 @@ import ( "os" "time" - gmi "git.sr.ht/~adnano/go-gemini" + "git.sr.ht/~adnano/go-gemini" ) func main() { host := "localhost" duration := 365 * 24 * time.Hour - cert, err := gmi.NewCertificate(host, duration) + cert, err := gemini.NewCertificate(host, duration) if err != nil { log.Fatal(err) } diff --git a/examples/client.go b/examples/client.go index e2e1669..852b126 100644 --- a/examples/client.go +++ b/examples/client.go @@ -76,13 +76,7 @@ func sendRequest(req *gemini.Request) error { return err } - switch resp.Status.Class() { - case gemini.StatusClassInput: - fmt.Printf("%s: ", resp.Meta) - scanner.Scan() - req.URL.RawQuery = url.QueryEscape(scanner.Text()) - return sendRequest(req) - case gemini.StatusClassSuccess: + if resp.Status.Class() == gemini.StatusClassSuccess { defer resp.Body.Close() body, err := ioutil.ReadAll(resp.Body) if err != nil { @@ -90,20 +84,8 @@ func sendRequest(req *gemini.Request) error { } fmt.Print(string(body)) return nil - case gemini.StatusClassRedirect: - // This should not happen unless CheckRedirect returns false. - return fmt.Errorf("Failed to redirect to %s", resp.Meta) - case gemini.StatusClassTemporaryFailure: - return fmt.Errorf("Temporary failure: %s", resp.Meta) - case gemini.StatusClassPermanentFailure: - return fmt.Errorf("Permanent failure: %s", resp.Meta) - case gemini.StatusClassCertificateRequired: - // Note that this should not happen unless the server responds with - // CertificateRequired even after we send a certificate. - // CertificateNotAuthorized and CertificateNotValid are handled here. - return fmt.Errorf("Certificate required: %s", resp.Meta) } - panic("unreachable") + return fmt.Errorf("request failed: %d %s: %s", resp.Status, resp.Status.Message(), resp.Meta) } type trust int diff --git a/server.go b/server.go index e0d6214..65e62b5 100644 --- a/server.go +++ b/server.go @@ -273,7 +273,8 @@ type Responder interface { // If no input is provided, it responds with StatusInput. func Input(w *ResponseWriter, r *Request, prompt string) (string, bool) { if r.URL.ForceQuery || r.URL.RawQuery != "" { - return r.URL.RawQuery, true + query, err := url.QueryUnescape(r.URL.RawQuery) + return query, err == nil } w.WriteHeader(StatusInput, prompt) return "", false @@ -283,7 +284,8 @@ func Input(w *ResponseWriter, r *Request, prompt string) (string, bool) { // If no input is provided, it responds with StatusSensitiveInput. func SensitiveInput(w *ResponseWriter, r *Request, prompt string) (string, bool) { if r.URL.ForceQuery || r.URL.RawQuery != "" { - return r.URL.RawQuery, true + query, err := url.QueryUnescape(r.URL.RawQuery) + return query, err == nil } w.WriteHeader(StatusSensitiveInput, prompt) return "", false diff --git a/status.go b/status.go index 7155606..2a4d0d3 100644 --- a/status.go +++ b/status.go @@ -30,11 +30,20 @@ func (s Status) Class() StatusClass { } // Message returns a status message corresponding to this status code. -// It returns an empty string for input, successs, and redirect status codes. func (s Status) Message() string { switch s { + case StatusInput: + return "Input" + case StatusSensitiveInput: + return "Sensitive input" + case StatusSuccess: + return "Success" + case StatusRedirect: + return "Redirect" + case StatusRedirectPermanent: + return "Permanent redirect" case StatusTemporaryFailure: - return "TemporaryFailure" + return "Temporary failure" case StatusServerUnavailable: return "Server unavailable" case StatusCGIError: @@ -44,7 +53,7 @@ func (s Status) Message() string { case StatusSlowDown: return "Slow down" case StatusPermanentFailure: - return "PermanentFailure" + return "Permanent failure" case StatusNotFound: return "Not found" case StatusGone: