diff --git a/README.md b/README.md index 6d5a410..bfc0eb6 100644 --- a/README.md +++ b/README.md @@ -141,3 +141,66 @@ Configure allowed server certificates for trust-on-first-use certificate support ``` client.AddAlllowedCertificateForHost("a.gemini", "3082016c3081f3020900d4c7c9907518eb61300a06082a8648ce3d0403023020310b30090603550406130267623111300f06035504030c08612e67656d696e69301e170d3230303832303139303330335a170d3330303831383139303330335a3020310b30090603550406130267623111300f06035504030c08612e67656d696e693076301006072a8648ce3d020106052b8104002203620004ae5cabe01f708d8f9423725df49601e1a033a1b51eb73cd3a8a9853011346127cbfedb57c4bd14ad6000ccb2f748d32b2a2b817b1860781d937e7666680874876fb4a9a91c44e2cf8c9804d40f6e7122f6c92a1884b62bd9f0749cca4e12cfa8300a06082a8648ce3d0403020368003065023100ae447eb9455e9ca1f02f013390d2c4029a7f29732cf6e29787b53b6435904d622f47f3b1fbffe60a284dbd4cddd6ef580230518dcb0355d5c3d880357128972c630ca90a915f1eb417a7ea0e4518a72dfc8a76c9b50c51d56f6a6835c4dfa989b72be3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855") ``` + +## Tasks + +### test + +Test the project. + +```sh +go test ./... -short +``` + +### test-integration + +Integration test the project. + +```sh +go test ./... +``` + +### build + +Build the CLI. + +```sh +go build -o gemini ./cmd/main.go +``` + +### build-docker + +Build the Docker image. + +```sh +docker build . -t adrianhesketh/gemini +``` + +### build-snapshot + +Build a snapshot release using goreleaser. + +```sh +goreleaser build --snapshot --rm-dist +``` + +### serve-local-tests + +Run a local Gemini server. + +```sh +echo add '127.0.0.1 a-h.gemini' to your /etc/hosts file +openssl ecparam -genkey -name secp384r1 -out server.key +openssl req -new -x509 -sha256 -key server.key -out server.crt -days 3650 -subj "/C=/ST=/L=/O=/OU=/CN=a-h.gemini" +go run ./cmd/main.go serve --domain=a-h.gemini --certFile=server.crt --keyFile=server.key --path=./tests +``` + +### release + +Push a release to Github. + +``` +if [ "${GITHUB_TOKEN}" == "" ]; then echo "Set the GITHUB_TOKEN environment variable"; fi +./push-tag.sh +goreleaser --rm-dist +``` diff --git a/client.go b/client.go index 785d51d..93f5ad0 100644 --- a/client.go +++ b/client.go @@ -152,6 +152,7 @@ func (client *Client) AddClientCertificate(prefix string, cert tls.Certificate) // AddServerCertificate allows the client to connect to a domain based on its hash. func (client *Client) AddServerCertificate(host, certificateHash string) { + host = strings.ToLower(host) if m := client.domainToAllowedCertificateHash[host]; m == nil { client.domainToAllowedCertificateHash[host] = make(map[string]interface{}) } @@ -220,7 +221,7 @@ func (client *Client) RequestURL(ctx context.Context, u *url.URL) (resp *Respons return } conn := cn.(*tls.Conn) - allowedHashesForDomain := client.domainToAllowedCertificateHash[u.Host] + allowedHashesForDomain := client.domainToAllowedCertificateHash[strings.ToLower(u.Host)] ok = false for _, cert := range conn.ConnectionState().PeerCertificates { hash := base64.StdEncoding.EncodeToString(sha256.New().Sum(cert.Raw)) diff --git a/cmd/main.go b/cmd/main.go index fcd2c0d..bd39903 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -193,7 +193,7 @@ func serve(args []string) { dh := gemini.NewDomainHandler(*domainFlag, cert, h) ctx := context.Background() domainToHandler := map[string]*gemini.DomainHandler{ - *domainFlag: dh, + strings.ToLower(*domainFlag): dh, } server := gemini.NewServer(ctx, fmt.Sprintf(":%d", *portFlag), domainToHandler) server.ReadTimeout = *readTimeoutFlag diff --git a/server.go b/server.go index c12dfbf..edaa263 100644 --- a/server.go +++ b/server.go @@ -95,6 +95,9 @@ func IsErrorCode(code Code) bool { // addr is in the form ":", e.g. ":1965". If left empty, it will default to ":1965". // domainToHandler is a map of the server name (domain) to the certificate key pair and the Gemini handler used to serve content. func NewServer(ctx context.Context, addr string, domainToHandler map[string]*DomainHandler) *Server { + for k, v := range domainToHandler { + domainToHandler[strings.ToLower(k)] = v + } return &Server{ Context: ctx, Addr: addr, @@ -181,7 +184,7 @@ func (srv *Server) serveTLS(l net.Listener) (err error) { ClientAuth: tls.RequestClientCert, InsecureSkipVerify: true, GetCertificate: func(hello *tls.ClientHelloInfo) (*tls.Certificate, error) { - dh, ok := srv.DomainToHandler[hello.ServerName] + dh, ok := srv.DomainToHandler[strings.ToLower(hello.ServerName)] if !ok { return nil, fmt.Errorf("gemini: certificate not found for %q", hello.ServerName) } @@ -231,7 +234,7 @@ func (srv *Server) handleTLS(conn *tls.Conn) { } } serverName := conn.ConnectionState().ServerName - dh, ok := srv.DomainToHandler[serverName] + dh, ok := srv.DomainToHandler[strings.ToLower(serverName)] if !ok { log.Warn("gemini: failed to find domain handler", log.String("serverName", serverName)) } diff --git a/server_integration_test.go b/server_integration_test.go new file mode 100644 index 0000000..717e38b --- /dev/null +++ b/server_integration_test.go @@ -0,0 +1,67 @@ +package gemini + +import ( + "context" + "crypto/tls" + "fmt" + "io" + "testing" + "time" +) + +func TestServerIntegration(t *testing.T) { + if testing.Short() { + return + } + // Start local server to listen on a-h.gemini + h := HandlerFunc(func(w ResponseWriter, r *Request) { + io.WriteString(w, "# Hello") + }) + + cf := "./example/server/a.crt" + kf := "./example/server/a.key" + cert, err := tls.LoadX509KeyPair(cf, kf) + if err != nil { + t.Fatalf("failed to load test certs: %v", err) + } + + domain := "a-h.gEmInI" + dh := NewDomainHandler(domain, cert, h) + ctx := context.Background() + domainToHandler := map[string]*DomainHandler{ + domain: dh, + } + server := NewServer(ctx, "localhost:1965", domainToHandler) + go func() { + err = server.ListenAndServe() + if err != nil { + fmt.Printf("error starting server: %v\n", err) + return + } + }() + + // Wait for the server to start up. + time.Sleep(time.Second) + + // Use client. + // Check that variations of case are handled. + c := NewClient() + c.AddServerCertificate("a-h.gEmInI", "MIIBbDCB8wIJANTHyZB1GOthMAoGCCqGSM49BAMCMCAxCzAJBgNVBAYTAmdiMREwDwYDVQQDDAhhLmdlbWluaTAeFw0yMDA4MjAxOTAzMDNaFw0zMDA4MTgxOTAzMDNaMCAxCzAJBgNVBAYTAmdiMREwDwYDVQQDDAhhLmdlbWluaTB2MBAGByqGSM49AgEGBSuBBAAiA2IABK5cq+AfcI2PlCNyXfSWAeGgM6G1Hrc806iphTARNGEny/7bV8S9FK1gAMyy90jTKyorgXsYYHgdk352ZmgIdIdvtKmpHETiz4yYBNQPbnEi9skqGIS2K9nwdJzKThLPqDAKBggqhkjOPQQDAgNoADBlAjEArkR+uUVenKHwLwEzkNLEApp/KXMs9uKXh7U7ZDWQTWIvR/Ox+//mCihNvUzd1u9YAjBRjcsDVdXD2IA1cSiXLGMMqQqRXx60F6fqDkUYpy38inbJtQxR1W9qaDXE36mJtyvjsMRCmPwcFJr79MiZb7kkJ65B5GSbk0yklZkbeFK4VQ==") + testURIs := []string{ + "gemini://a-h.gemini", + "gemini://a-h.gEmInI", + "gemini://a-h.GEMINI", + } + for _, uri := range testURIs { + t.Run(fmt.Sprintf("%v", uri), func(t *testing.T) { + resp, certs, _, ok, err := c.Request(context.Background(), uri) + if err != nil { + t.Fatalf("request failed: %v", err) + return + } + if !ok { + t.Errorf("response not OK: %v, certs: %v", resp, certs) + } + }) + } +} diff --git a/server_test.go b/server_test.go index 4d115d7..9518891 100644 --- a/server_test.go +++ b/server_test.go @@ -46,6 +46,18 @@ func TestServer(t *testing.T) { expectedBody: []byte{}, expectedBodyErr: nil, }, + { + name: "domain lookups are not case sensitive", + request: "gemini://SENSIBLE\r\n", + handler: func(w ResponseWriter, r *Request) { + w.SetHeader(CodeInput, "What's your name?") + }, + expectedCode: CodeInput, + expectedMeta: "What's your name?", + expectedHeaderErr: nil, + expectedBody: []byte{}, + expectedBodyErr: nil, + }, { name: "successful handlers are sent", request: "gemini://sensible\r\n", @@ -182,12 +194,12 @@ func TestServer(t *testing.T) { rec := NewRecorder([]byte(tt.request)) // Skip the usual setup, because this test doesn't carry out integration work. dh := &DomainHandler{ - ServerName: "", + ServerName: "sensible", Handler: HandlerFunc(tt.handler), } s := &Server{ DomainToHandler: map[string]*DomainHandler{ - "": dh, + "sensible": dh, }, Context: context.Background(), }