Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b6475aa7d9 | ||
|
|
cc372e8768 | ||
|
|
8e442146c3 | ||
|
|
e4dea6f2c8 | ||
|
|
b57ea57fec | ||
|
|
c3fc9a4e9f | ||
|
|
22d57dfc9e | ||
|
|
12bdb2f997 |
@@ -182,6 +182,8 @@ func (c *Client) getClientCertificate(req *Request) (*tls.Certificate, error) {
|
|||||||
for {
|
for {
|
||||||
cert, err := c.Certificates.Lookup(scope)
|
cert, err := c.Certificates.Lookup(scope)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
|
// Store the certificate
|
||||||
|
req.Certificate = cert
|
||||||
return cert, err
|
return cert, err
|
||||||
}
|
}
|
||||||
if err == ErrCertificateExpired {
|
if err == ErrCertificateExpired {
|
||||||
|
|||||||
@@ -3,9 +3,11 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"crypto/tls"
|
||||||
"crypto/x509"
|
"crypto/x509"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
|
"time"
|
||||||
|
|
||||||
"git.sr.ht/~adnano/go-gemini"
|
"git.sr.ht/~adnano/go-gemini"
|
||||||
)
|
)
|
||||||
@@ -44,6 +46,12 @@ func main() {
|
|||||||
if err := server.Certificates.Load("/var/lib/gemini/certs"); err != nil {
|
if err := server.Certificates.Load("/var/lib/gemini/certs"); err != nil {
|
||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
server.CreateCertificate = func(hostname string) (tls.Certificate, error) {
|
||||||
|
return gemini.CreateCertificate(gemini.CertificateOptions{
|
||||||
|
DNSNames: []string{hostname},
|
||||||
|
Duration: time.Hour,
|
||||||
|
})
|
||||||
|
}
|
||||||
server.Register("localhost", &mux)
|
server.Register("localhost", &mux)
|
||||||
|
|
||||||
if err := server.ListenAndServe(); err != nil {
|
if err := server.ListenAndServe(); err != nil {
|
||||||
@@ -51,22 +59,23 @@ func main() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func getSession(crt *x509.Certificate) (*session, bool) {
|
func getSession(cert *x509.Certificate) (*session, bool) {
|
||||||
fingerprint := gemini.Fingerprint(crt)
|
fingerprint := gemini.Fingerprint(cert)
|
||||||
session, ok := sessions[fingerprint]
|
session, ok := sessions[fingerprint]
|
||||||
return session, ok
|
return session, ok
|
||||||
}
|
}
|
||||||
|
|
||||||
func login(w *gemini.ResponseWriter, r *gemini.Request) {
|
func login(w *gemini.ResponseWriter, r *gemini.Request) {
|
||||||
cert, ok := gemini.Certificate(w, r)
|
if r.Certificate == nil {
|
||||||
if !ok {
|
w.WriteStatus(gemini.StatusCertificateRequired)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
username, ok := gemini.Input(w, r, "Username")
|
username, ok := gemini.Input(r)
|
||||||
if !ok {
|
if !ok {
|
||||||
|
w.WriteHeader(gemini.StatusInput, "Username")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
fingerprint := gemini.Fingerprint(cert)
|
fingerprint := gemini.Fingerprint(r.Certificate.Leaf)
|
||||||
sessions[fingerprint] = &session{
|
sessions[fingerprint] = &session{
|
||||||
username: username,
|
username: username,
|
||||||
}
|
}
|
||||||
@@ -74,18 +83,19 @@ func login(w *gemini.ResponseWriter, r *gemini.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func loginPassword(w *gemini.ResponseWriter, r *gemini.Request) {
|
func loginPassword(w *gemini.ResponseWriter, r *gemini.Request) {
|
||||||
cert, ok := gemini.Certificate(w, r)
|
if r.Certificate == nil {
|
||||||
if !ok {
|
w.WriteStatus(gemini.StatusCertificateRequired)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
session, ok := getSession(cert)
|
session, ok := getSession(r.Certificate.Leaf)
|
||||||
if !ok {
|
if !ok {
|
||||||
w.WriteStatus(gemini.StatusCertificateNotAuthorized)
|
w.WriteStatus(gemini.StatusCertificateNotAuthorized)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
password, ok := gemini.SensitiveInput(w, r, "Password")
|
password, ok := gemini.Input(r)
|
||||||
if !ok {
|
if !ok {
|
||||||
|
w.WriteHeader(gemini.StatusSensitiveInput, "Password")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
expected := logins[session.username].password
|
expected := logins[session.username].password
|
||||||
@@ -93,26 +103,26 @@ func loginPassword(w *gemini.ResponseWriter, r *gemini.Request) {
|
|||||||
session.authorized = true
|
session.authorized = true
|
||||||
w.WriteHeader(gemini.StatusRedirect, "/profile")
|
w.WriteHeader(gemini.StatusRedirect, "/profile")
|
||||||
} else {
|
} else {
|
||||||
gemini.SensitiveInput(w, r, "Wrong password. Try again")
|
w.WriteHeader(gemini.StatusSensitiveInput, "Wrong password. Try again")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func logout(w *gemini.ResponseWriter, r *gemini.Request) {
|
func logout(w *gemini.ResponseWriter, r *gemini.Request) {
|
||||||
cert, ok := gemini.Certificate(w, r)
|
if r.Certificate == nil {
|
||||||
if !ok {
|
w.WriteStatus(gemini.StatusCertificateRequired)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
fingerprint := gemini.Fingerprint(cert)
|
fingerprint := gemini.Fingerprint(r.Certificate.Leaf)
|
||||||
delete(sessions, fingerprint)
|
delete(sessions, fingerprint)
|
||||||
fmt.Fprintln(w, "Successfully logged out.")
|
fmt.Fprintln(w, "Successfully logged out.")
|
||||||
}
|
}
|
||||||
|
|
||||||
func profile(w *gemini.ResponseWriter, r *gemini.Request) {
|
func profile(w *gemini.ResponseWriter, r *gemini.Request) {
|
||||||
cert, ok := gemini.Certificate(w, r)
|
if r.Certificate == nil {
|
||||||
if !ok {
|
w.WriteStatus(gemini.StatusCertificateRequired)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
session, ok := getSession(cert)
|
session, ok := getSession(r.Certificate.Leaf)
|
||||||
if !ok {
|
if !ok {
|
||||||
w.WriteStatus(gemini.StatusCertificateNotAuthorized)
|
w.WriteStatus(gemini.StatusCertificateNotAuthorized)
|
||||||
return
|
return
|
||||||
@@ -124,11 +134,11 @@ func profile(w *gemini.ResponseWriter, r *gemini.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func admin(w *gemini.ResponseWriter, r *gemini.Request) {
|
func admin(w *gemini.ResponseWriter, r *gemini.Request) {
|
||||||
cert, ok := gemini.Certificate(w, r)
|
if r.Certificate == nil {
|
||||||
if !ok {
|
w.WriteStatus(gemini.StatusCertificateRequired)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
session, ok := getSession(cert)
|
session, ok := getSession(r.Certificate.Leaf)
|
||||||
if !ok {
|
if !ok {
|
||||||
w.WriteStatus(gemini.StatusCertificateNotAuthorized)
|
w.WriteStatus(gemini.StatusCertificateNotAuthorized)
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -3,10 +3,6 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
|
||||||
"crypto/tls"
|
|
||||||
"crypto/x509"
|
|
||||||
"encoding/pem"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
"os"
|
"os"
|
||||||
@@ -33,63 +29,9 @@ func main() {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
if err := writeCertificate(host, cert); err != nil {
|
certPath := host + ".crt"
|
||||||
|
keyPath := host + ".key"
|
||||||
|
if err := gemini.WriteCertificate(cert, certPath, keyPath); err != nil {
|
||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// writeCertificate writes the provided certificate and private key
|
|
||||||
// to path.crt and path.key respectively.
|
|
||||||
func writeCertificate(path string, cert tls.Certificate) error {
|
|
||||||
crt, err := marshalX509Certificate(cert.Leaf.Raw)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
key, err := marshalPrivateKey(cert.PrivateKey)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Write the certificate
|
|
||||||
crtPath := path + ".crt"
|
|
||||||
crtOut, err := os.OpenFile(crtPath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if _, err := crtOut.Write(crt); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Write the private key
|
|
||||||
keyPath := path + ".key"
|
|
||||||
keyOut, err := os.OpenFile(keyPath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if _, err := keyOut.Write(key); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// marshalX509Certificate returns a PEM-encoded version of the given raw certificate.
|
|
||||||
func marshalX509Certificate(cert []byte) ([]byte, error) {
|
|
||||||
var b bytes.Buffer
|
|
||||||
if err := pem.Encode(&b, &pem.Block{Type: "CERTIFICATE", Bytes: cert}); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return b.Bytes(), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// marshalPrivateKey returns PEM encoded versions of the given certificate and private key.
|
|
||||||
func marshalPrivateKey(priv interface{}) ([]byte, error) {
|
|
||||||
var b bytes.Buffer
|
|
||||||
privBytes, err := x509.MarshalPKCS8PrivateKey(priv)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if err := pem.Encode(&b, &pem.Block{Type: "PRIVATE KEY", Bytes: privBytes}); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return b.Bytes(), nil
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ var (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
client.Timeout = 2 * time.Minute
|
client.Timeout = 30 * time.Second
|
||||||
client.KnownHosts.LoadDefault()
|
client.KnownHosts.LoadDefault()
|
||||||
client.TrustCertificate = func(hostname string, cert *x509.Certificate) gemini.Trust {
|
client.TrustCertificate = func(hostname string, cert *x509.Certificate) gemini.Trust {
|
||||||
fmt.Printf(trustPrompt, hostname, gemini.Fingerprint(cert))
|
fmt.Printf(trustPrompt, hostname, gemini.Fingerprint(cert))
|
||||||
|
|||||||
@@ -37,11 +37,10 @@ func textToHTML(text gemini.Text) string {
|
|||||||
list = false
|
list = false
|
||||||
fmt.Fprint(&b, "</ul>\n")
|
fmt.Fprint(&b, "</ul>\n")
|
||||||
}
|
}
|
||||||
switch l.(type) {
|
switch l := l.(type) {
|
||||||
case gemini.LineLink:
|
case gemini.LineLink:
|
||||||
link := l.(gemini.LineLink)
|
url := html.EscapeString(l.URL)
|
||||||
url := html.EscapeString(link.URL)
|
name := html.EscapeString(l.Name)
|
||||||
name := html.EscapeString(link.Name)
|
|
||||||
if name == "" {
|
if name == "" {
|
||||||
name = url
|
name = url
|
||||||
}
|
}
|
||||||
@@ -54,29 +53,22 @@ func textToHTML(text gemini.Text) string {
|
|||||||
fmt.Fprint(&b, "</pre>\n")
|
fmt.Fprint(&b, "</pre>\n")
|
||||||
}
|
}
|
||||||
case gemini.LinePreformattedText:
|
case gemini.LinePreformattedText:
|
||||||
text := string(l.(gemini.LinePreformattedText))
|
fmt.Fprintf(&b, "%s\n", html.EscapeString(string(l)))
|
||||||
fmt.Fprintf(&b, "%s\n", html.EscapeString(text))
|
|
||||||
case gemini.LineHeading1:
|
case gemini.LineHeading1:
|
||||||
text := string(l.(gemini.LineHeading1))
|
fmt.Fprintf(&b, "<h1>%s</h1>\n", html.EscapeString(string(l)))
|
||||||
fmt.Fprintf(&b, "<h1>%s</h1>\n", html.EscapeString(text))
|
|
||||||
case gemini.LineHeading2:
|
case gemini.LineHeading2:
|
||||||
text := string(l.(gemini.LineHeading2))
|
fmt.Fprintf(&b, "<h2>%s</h2>\n", html.EscapeString(string(l)))
|
||||||
fmt.Fprintf(&b, "<h2>%s</h2>\n", html.EscapeString(text))
|
|
||||||
case gemini.LineHeading3:
|
case gemini.LineHeading3:
|
||||||
text := string(l.(gemini.LineHeading3))
|
fmt.Fprintf(&b, "<h3>%s</h3>\n", html.EscapeString(string(l)))
|
||||||
fmt.Fprintf(&b, "<h3>%s</h3>\n", html.EscapeString(text))
|
|
||||||
case gemini.LineListItem:
|
case gemini.LineListItem:
|
||||||
text := string(l.(gemini.LineListItem))
|
fmt.Fprintf(&b, "<li>%s</li>\n", html.EscapeString(string(l)))
|
||||||
fmt.Fprintf(&b, "<li>%s</li>\n", html.EscapeString(text))
|
|
||||||
case gemini.LineQuote:
|
case gemini.LineQuote:
|
||||||
text := string(l.(gemini.LineQuote))
|
fmt.Fprintf(&b, "<blockquote>%s</blockquote>\n", html.EscapeString(string(l)))
|
||||||
fmt.Fprintf(&b, "<blockquote>%s</blockquote>\n", html.EscapeString(text))
|
|
||||||
case gemini.LineText:
|
case gemini.LineText:
|
||||||
text := string(l.(gemini.LineText))
|
if l == "" {
|
||||||
if text == "" {
|
|
||||||
fmt.Fprint(&b, "<br>\n")
|
fmt.Fprint(&b, "<br>\n")
|
||||||
} else {
|
} else {
|
||||||
fmt.Fprintf(&b, "<p>%s</p>\n", html.EscapeString(text))
|
fmt.Fprintf(&b, "<p>%s</p>\n", html.EscapeString(string(l)))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,8 +12,8 @@ import (
|
|||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
var server gemini.Server
|
var server gemini.Server
|
||||||
server.ReadTimeout = 1 * time.Minute
|
server.ReadTimeout = 30 * time.Second
|
||||||
server.WriteTimeout = 2 * time.Minute
|
server.WriteTimeout = 1 * time.Minute
|
||||||
if err := server.Certificates.Load("/var/lib/gemini/certs"); err != nil {
|
if err := server.Certificates.Load("/var/lib/gemini/certs"); err != nil {
|
||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|||||||
16
gemini.go
16
gemini.go
@@ -22,31 +22,25 @@ var (
|
|||||||
ErrInputRequired = errors.New("gemini: input required")
|
ErrInputRequired = errors.New("gemini: input required")
|
||||||
)
|
)
|
||||||
|
|
||||||
// DefaultClient is the default client. It is used by Get and Do.
|
// defaultClient is the default client. It is used by Get and Do.
|
||||||
//
|
var defaultClient Client
|
||||||
// On the first request, DefaultClient loads the default list of known hosts.
|
|
||||||
var DefaultClient Client
|
|
||||||
|
|
||||||
// Get performs a Gemini request for the given url.
|
// Get performs a Gemini request for the given url.
|
||||||
//
|
|
||||||
// Get is a wrapper around DefaultClient.Get.
|
|
||||||
func Get(url string) (*Response, error) {
|
func Get(url string) (*Response, error) {
|
||||||
setupDefaultClientOnce()
|
setupDefaultClientOnce()
|
||||||
return DefaultClient.Get(url)
|
return defaultClient.Get(url)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Do performs a Gemini request and returns a Gemini response.
|
// Do performs a Gemini request and returns a Gemini response.
|
||||||
//
|
|
||||||
// Do is a wrapper around DefaultClient.Do.
|
|
||||||
func Do(req *Request) (*Response, error) {
|
func Do(req *Request) (*Response, error) {
|
||||||
setupDefaultClientOnce()
|
setupDefaultClientOnce()
|
||||||
return DefaultClient.Do(req)
|
return defaultClient.Do(req)
|
||||||
}
|
}
|
||||||
|
|
||||||
var defaultClientOnce sync.Once
|
var defaultClientOnce sync.Once
|
||||||
|
|
||||||
func setupDefaultClientOnce() {
|
func setupDefaultClientOnce() {
|
||||||
defaultClientOnce.Do(func() {
|
defaultClientOnce.Do(func() {
|
||||||
DefaultClient.KnownHosts.LoadDefault()
|
defaultClient.KnownHosts.LoadDefault()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,7 +19,9 @@ type Request struct {
|
|||||||
|
|
||||||
// Certificate specifies the TLS certificate to use for the request.
|
// Certificate specifies the TLS certificate to use for the request.
|
||||||
// Request certificates take precedence over client certificates.
|
// Request certificates take precedence over client certificates.
|
||||||
// This field is ignored by the server.
|
//
|
||||||
|
// On the server side, if the client provided a certificate then
|
||||||
|
// Certificate.Leaf is guaranteed to be non-nil.
|
||||||
Certificate *tls.Certificate
|
Certificate *tls.Certificate
|
||||||
|
|
||||||
// RemoteAddr allows servers and other software to record the network
|
// RemoteAddr allows servers and other software to record the network
|
||||||
|
|||||||
73
server.go
73
server.go
@@ -3,7 +3,6 @@ package gemini
|
|||||||
import (
|
import (
|
||||||
"bufio"
|
"bufio"
|
||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
"crypto/x509"
|
|
||||||
"log"
|
"log"
|
||||||
"net"
|
"net"
|
||||||
"net/url"
|
"net/url"
|
||||||
@@ -199,10 +198,24 @@ func (s *Server) respond(conn net.Conn) {
|
|||||||
if url.Scheme == "" {
|
if url.Scheme == "" {
|
||||||
url.Scheme = "gemini"
|
url.Scheme = "gemini"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Store information about the TLS connection
|
||||||
|
connState := conn.(*tls.Conn).ConnectionState()
|
||||||
|
var cert *tls.Certificate
|
||||||
|
if len(connState.PeerCertificates) > 0 {
|
||||||
|
peerCert := connState.PeerCertificates[0]
|
||||||
|
// Store the TLS certificate
|
||||||
|
cert = &tls.Certificate{
|
||||||
|
Certificate: [][]byte{peerCert.Raw},
|
||||||
|
Leaf: peerCert,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
req := &Request{
|
req := &Request{
|
||||||
URL: url,
|
URL: url,
|
||||||
RemoteAddr: conn.RemoteAddr(),
|
RemoteAddr: conn.RemoteAddr(),
|
||||||
TLS: conn.(*tls.Conn).ConnectionState(),
|
TLS: connState,
|
||||||
|
Certificate: cert,
|
||||||
}
|
}
|
||||||
resp := s.responder(req)
|
resp := s.responder(req)
|
||||||
if resp != nil {
|
if resp != nil {
|
||||||
@@ -304,41 +317,29 @@ type Responder interface {
|
|||||||
Respond(*ResponseWriter, *Request)
|
Respond(*ResponseWriter, *Request)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Input returns the request query.
|
|
||||||
// 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 != "" {
|
|
||||||
query, err := url.QueryUnescape(r.URL.RawQuery)
|
|
||||||
return query, err == nil
|
|
||||||
}
|
|
||||||
w.WriteHeader(StatusInput, prompt)
|
|
||||||
return "", false
|
|
||||||
}
|
|
||||||
|
|
||||||
// SensitiveInput returns the request query.
|
|
||||||
// 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 != "" {
|
|
||||||
query, err := url.QueryUnescape(r.URL.RawQuery)
|
|
||||||
return query, err == nil
|
|
||||||
}
|
|
||||||
w.WriteHeader(StatusSensitiveInput, prompt)
|
|
||||||
return "", false
|
|
||||||
}
|
|
||||||
|
|
||||||
// Certificate returns the request certificate. If one is not provided,
|
|
||||||
// it returns nil and responds with StatusCertificateRequired.
|
|
||||||
func Certificate(w *ResponseWriter, r *Request) (*x509.Certificate, bool) {
|
|
||||||
if len(r.TLS.PeerCertificates) == 0 {
|
|
||||||
w.WriteStatus(StatusCertificateRequired)
|
|
||||||
return nil, false
|
|
||||||
}
|
|
||||||
return r.TLS.PeerCertificates[0], true
|
|
||||||
}
|
|
||||||
|
|
||||||
// ResponderFunc is a wrapper around a bare function that implements Responder.
|
// ResponderFunc is a wrapper around a bare function that implements Responder.
|
||||||
type ResponderFunc func(*ResponseWriter, *Request)
|
type ResponderFunc func(*ResponseWriter, *Request)
|
||||||
|
|
||||||
func (f ResponderFunc) Respond(w *ResponseWriter, r *Request) {
|
func (f ResponderFunc) Respond(w *ResponseWriter, r *Request) {
|
||||||
f(w, r)
|
f(w, r)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Input returns the request query.
|
||||||
|
// If the query is invalid or no query is provided, ok will be false.
|
||||||
|
//
|
||||||
|
// Example:
|
||||||
|
//
|
||||||
|
// input, ok := gemini.Input(req)
|
||||||
|
// if !ok {
|
||||||
|
// w.WriteHeader(gemini.StatusInput, "Prompt")
|
||||||
|
// return
|
||||||
|
// }
|
||||||
|
// // ...
|
||||||
|
//
|
||||||
|
func Input(r *Request) (query string, ok bool) {
|
||||||
|
if r.URL.ForceQuery || r.URL.RawQuery != "" {
|
||||||
|
query, err := url.QueryUnescape(r.URL.RawQuery)
|
||||||
|
return query, err == nil
|
||||||
|
}
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user