Skip to content

Commit

Permalink
fix: support variations in hostname casing, fixes #15
Browse files Browse the repository at this point in the history
  • Loading branch information
a-h committed Nov 21, 2021
1 parent fb6d06a commit 2742dbe
Show file tree
Hide file tree
Showing 6 changed files with 152 additions and 6 deletions.
63 changes: 63 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
```
3 changes: 2 additions & 1 deletion client.go
Original file line number Diff line number Diff line change
Expand Up @@ -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{})
}
Expand Down Expand Up @@ -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))
Expand Down
2 changes: 1 addition & 1 deletion cmd/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
7 changes: 5 additions & 2 deletions server.go
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,9 @@ func IsErrorCode(code Code) bool {
// addr is in the form "<optional_ip>:<port>", 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,
Expand Down Expand Up @@ -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)
}
Expand Down Expand Up @@ -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))
}
Expand Down
67 changes: 67 additions & 0 deletions server_integration_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
})
}
}
16 changes: 14 additions & 2 deletions server_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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(),
}
Expand Down

0 comments on commit 2742dbe

Please sign in to comment.