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 {
|
||||
cert, err := c.Certificates.Lookup(scope)
|
||||
if err == nil {
|
||||
// Store the certificate
|
||||
req.Certificate = cert
|
||||
return cert, err
|
||||
}
|
||||
if err == ErrCertificateExpired {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
16
gemini.go
16
gemini.go
@@ -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()
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
69
server.go
69
server.go
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user