8 Commits

Author SHA1 Message Date
Adnan Maolood
b6475aa7d9 server: Populate (*Request).Certificate field 2020-11-01 16:25:59 -05:00
Adnan Maolood
cc372e8768 Prevent infinite loop in client requests 2020-11-01 15:14:56 -05:00
adnano
8e442146c3 Update examples/auth.go 2020-11-01 14:47:26 -05:00
adnano
e4dea6f2c8 Refactor Certificate and Input functions 2020-11-01 14:35:03 -05:00
adnano
b57ea57fec Don't expose DefaultClient 2020-11-01 14:27:49 -05:00
adnano
c3fc9a4e9f examples: Tweak client and server timeouts 2020-11-01 14:20:24 -05:00
adnano
22d57dfc9e Update examples/cert.go 2020-11-01 14:19:18 -05:00
Adnan Maolood
12bdb2f997 Update examples/html.go 2020-11-01 00:58:34 -04:00
9 changed files with 94 additions and 151 deletions

View File

@@ -182,6 +182,8 @@ func (c *Client) getClientCertificate(req *Request) (*tls.Certificate, error) {
for {
cert, err := c.Certificates.Lookup(scope)
if err == nil {
// Store the certificate
req.Certificate = cert
return cert, err
}
if err == ErrCertificateExpired {

View File

@@ -3,9 +3,11 @@
package main
import (
"crypto/tls"
"crypto/x509"
"fmt"
"log"
"time"
"git.sr.ht/~adnano/go-gemini"
)
@@ -44,6 +46,12 @@ func main() {
if err := server.Certificates.Load("/var/lib/gemini/certs"); err != nil {
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)
if err := server.ListenAndServe(); err != nil {
@@ -51,22 +59,23 @@ func main() {
}
}
func getSession(crt *x509.Certificate) (*session, bool) {
fingerprint := gemini.Fingerprint(crt)
func getSession(cert *x509.Certificate) (*session, bool) {
fingerprint := gemini.Fingerprint(cert)
session, ok := sessions[fingerprint]
return session, ok
}
func login(w *gemini.ResponseWriter, r *gemini.Request) {
cert, ok := gemini.Certificate(w, r)
if !ok {
if r.Certificate == nil {
w.WriteStatus(gemini.StatusCertificateRequired)
return
}
username, ok := gemini.Input(w, r, "Username")
username, ok := gemini.Input(r)
if !ok {
w.WriteHeader(gemini.StatusInput, "Username")
return
}
fingerprint := gemini.Fingerprint(cert)
fingerprint := gemini.Fingerprint(r.Certificate.Leaf)
sessions[fingerprint] = &session{
username: username,
}
@@ -74,18 +83,19 @@ func login(w *gemini.ResponseWriter, r *gemini.Request) {
}
func loginPassword(w *gemini.ResponseWriter, r *gemini.Request) {
cert, ok := gemini.Certificate(w, r)
if !ok {
if r.Certificate == nil {
w.WriteStatus(gemini.StatusCertificateRequired)
return
}
session, ok := getSession(cert)
session, ok := getSession(r.Certificate.Leaf)
if !ok {
w.WriteStatus(gemini.StatusCertificateNotAuthorized)
return
}
password, ok := gemini.SensitiveInput(w, r, "Password")
password, ok := gemini.Input(r)
if !ok {
w.WriteHeader(gemini.StatusSensitiveInput, "Password")
return
}
expected := logins[session.username].password
@@ -93,26 +103,26 @@ func loginPassword(w *gemini.ResponseWriter, r *gemini.Request) {
session.authorized = true
w.WriteHeader(gemini.StatusRedirect, "/profile")
} 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) {
cert, ok := gemini.Certificate(w, r)
if !ok {
if r.Certificate == nil {
w.WriteStatus(gemini.StatusCertificateRequired)
return
}
fingerprint := gemini.Fingerprint(cert)
fingerprint := gemini.Fingerprint(r.Certificate.Leaf)
delete(sessions, fingerprint)
fmt.Fprintln(w, "Successfully logged out.")
}
func profile(w *gemini.ResponseWriter, r *gemini.Request) {
cert, ok := gemini.Certificate(w, r)
if !ok {
if r.Certificate == nil {
w.WriteStatus(gemini.StatusCertificateRequired)
return
}
session, ok := getSession(cert)
session, ok := getSession(r.Certificate.Leaf)
if !ok {
w.WriteStatus(gemini.StatusCertificateNotAuthorized)
return
@@ -124,11 +134,11 @@ func profile(w *gemini.ResponseWriter, r *gemini.Request) {
}
func admin(w *gemini.ResponseWriter, r *gemini.Request) {
cert, ok := gemini.Certificate(w, r)
if !ok {
if r.Certificate == nil {
w.WriteStatus(gemini.StatusCertificateRequired)
return
}
session, ok := getSession(cert)
session, ok := getSession(r.Certificate.Leaf)
if !ok {
w.WriteStatus(gemini.StatusCertificateNotAuthorized)
return

View File

@@ -3,10 +3,6 @@
package main
import (
"bytes"
"crypto/tls"
"crypto/x509"
"encoding/pem"
"fmt"
"log"
"os"
@@ -33,63 +29,9 @@ func main() {
if err != nil {
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)
}
}
// 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
}

View File

@@ -30,7 +30,7 @@ var (
)
func init() {
client.Timeout = 2 * time.Minute
client.Timeout = 30 * time.Second
client.KnownHosts.LoadDefault()
client.TrustCertificate = func(hostname string, cert *x509.Certificate) gemini.Trust {
fmt.Printf(trustPrompt, hostname, gemini.Fingerprint(cert))

View File

@@ -37,11 +37,10 @@ func textToHTML(text gemini.Text) string {
list = false
fmt.Fprint(&b, "</ul>\n")
}
switch l.(type) {
switch l := l.(type) {
case gemini.LineLink:
link := l.(gemini.LineLink)
url := html.EscapeString(link.URL)
name := html.EscapeString(link.Name)
url := html.EscapeString(l.URL)
name := html.EscapeString(l.Name)
if name == "" {
name = url
}
@@ -54,29 +53,22 @@ func textToHTML(text gemini.Text) string {
fmt.Fprint(&b, "</pre>\n")
}
case gemini.LinePreformattedText:
text := string(l.(gemini.LinePreformattedText))
fmt.Fprintf(&b, "%s\n", html.EscapeString(text))
fmt.Fprintf(&b, "%s\n", html.EscapeString(string(l)))
case gemini.LineHeading1:
text := string(l.(gemini.LineHeading1))
fmt.Fprintf(&b, "<h1>%s</h1>\n", html.EscapeString(text))
fmt.Fprintf(&b, "<h1>%s</h1>\n", html.EscapeString(string(l)))
case gemini.LineHeading2:
text := string(l.(gemini.LineHeading2))
fmt.Fprintf(&b, "<h2>%s</h2>\n", html.EscapeString(text))
fmt.Fprintf(&b, "<h2>%s</h2>\n", html.EscapeString(string(l)))
case gemini.LineHeading3:
text := string(l.(gemini.LineHeading3))
fmt.Fprintf(&b, "<h3>%s</h3>\n", html.EscapeString(text))
fmt.Fprintf(&b, "<h3>%s</h3>\n", html.EscapeString(string(l)))
case gemini.LineListItem:
text := string(l.(gemini.LineListItem))
fmt.Fprintf(&b, "<li>%s</li>\n", html.EscapeString(text))
fmt.Fprintf(&b, "<li>%s</li>\n", html.EscapeString(string(l)))
case gemini.LineQuote:
text := string(l.(gemini.LineQuote))
fmt.Fprintf(&b, "<blockquote>%s</blockquote>\n", html.EscapeString(text))
fmt.Fprintf(&b, "<blockquote>%s</blockquote>\n", html.EscapeString(string(l)))
case gemini.LineText:
text := string(l.(gemini.LineText))
if text == "" {
if l == "" {
fmt.Fprint(&b, "<br>\n")
} else {
fmt.Fprintf(&b, "<p>%s</p>\n", html.EscapeString(text))
fmt.Fprintf(&b, "<p>%s</p>\n", html.EscapeString(string(l)))
}
}
}

View File

@@ -12,8 +12,8 @@ import (
func main() {
var server gemini.Server
server.ReadTimeout = 1 * time.Minute
server.WriteTimeout = 2 * time.Minute
server.ReadTimeout = 30 * time.Second
server.WriteTimeout = 1 * time.Minute
if err := server.Certificates.Load("/var/lib/gemini/certs"); err != nil {
log.Fatal(err)
}

View File

@@ -22,31 +22,25 @@ var (
ErrInputRequired = errors.New("gemini: input required")
)
// DefaultClient is the default client. It is used by Get and Do.
//
// On the first request, DefaultClient loads the default list of known hosts.
var DefaultClient Client
// defaultClient is the default client. It is used by Get and Do.
var defaultClient Client
// Get performs a Gemini request for the given url.
//
// Get is a wrapper around DefaultClient.Get.
func Get(url string) (*Response, error) {
setupDefaultClientOnce()
return DefaultClient.Get(url)
return defaultClient.Get(url)
}
// Do performs a Gemini request and returns a Gemini response.
//
// Do is a wrapper around DefaultClient.Do.
func Do(req *Request) (*Response, error) {
setupDefaultClientOnce()
return DefaultClient.Do(req)
return defaultClient.Do(req)
}
var defaultClientOnce sync.Once
func setupDefaultClientOnce() {
defaultClientOnce.Do(func() {
DefaultClient.KnownHosts.LoadDefault()
defaultClient.KnownHosts.LoadDefault()
})
}

View File

@@ -19,7 +19,9 @@ type Request struct {
// Certificate specifies the TLS certificate to use for the request.
// 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
// RemoteAddr allows servers and other software to record the network

View File

@@ -3,7 +3,6 @@ package gemini
import (
"bufio"
"crypto/tls"
"crypto/x509"
"log"
"net"
"net/url"
@@ -199,10 +198,24 @@ func (s *Server) respond(conn net.Conn) {
if url.Scheme == "" {
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{
URL: url,
RemoteAddr: conn.RemoteAddr(),
TLS: conn.(*tls.Conn).ConnectionState(),
TLS: connState,
Certificate: cert,
}
resp := s.responder(req)
if resp != nil {
@@ -304,41 +317,29 @@ type Responder interface {
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.
type ResponderFunc func(*ResponseWriter, *Request)
func (f ResponderFunc) Respond(w *ResponseWriter, r *Request) {
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
}