summaryrefslogtreecommitdiff
path: root/gemini
diff options
context:
space:
mode:
Diffstat (limited to 'gemini')
-rw-r--r--gemini/client.go2
-rw-r--r--gemini/client_server_test.go65
-rw-r--r--gemini/handler_test.go117
-rw-r--r--gemini/response_test.go53
-rw-r--r--gemini/serve.go8
-rw-r--r--gemini/testdata/server.crt18
-rw-r--r--gemini/testdata/server.csr18
-rw-r--r--gemini/testdata/server.key27
8 files changed, 286 insertions, 22 deletions
diff --git a/gemini/client.go b/gemini/client.go
index aca4576..05ab8cd 100644
--- a/gemini/client.go
+++ b/gemini/client.go
@@ -14,7 +14,7 @@ import (
// The only reason you might create more than one Client is to support separate TLS-cert
// driven identities.
//
-// The zero value of Client is usable, it simply has no client TLS cert.
+// The zero value is a usable Client with no client TLS certificate.
type Client struct {
tlsConf *tls.Config
}
diff --git a/gemini/client_server_test.go b/gemini/client_server_test.go
new file mode 100644
index 0000000..5dd61f1
--- /dev/null
+++ b/gemini/client_server_test.go
@@ -0,0 +1,65 @@
+package gemini_test
+
+import (
+ "bytes"
+ "context"
+ "crypto/tls"
+ "fmt"
+ "io"
+ "net/url"
+ "testing"
+
+ "tildegit.org/tjp/gus/gemini"
+)
+
+func TestRoundTrip(t *testing.T) {
+ tlsConf, err := gemini.FileTLS("./testdata/server.crt", "./testdata/server.key")
+ if err != nil {
+ t.Fatalf("FileTLS(): %s", err.Error())
+ }
+
+ handler := func(ctx context.Context, req *gemini.Request) *gemini.Response {
+ return gemini.Success("text/gemini", bytes.NewBufferString("you've found my page"))
+ }
+
+ server, err := gemini.NewServer(context.Background(), tlsConf, "tcp", "127.0.0.1:0", handler)
+ if err != nil {
+ t.Fatalf("NewServer(): %s", err.Error())
+ }
+
+ go server.Serve()
+ defer server.Close()
+
+ u, err := url.Parse(fmt.Sprintf("gemini://%s/test", server.Address()))
+ if err != nil {
+ t.Fatalf("url.Parse: %s", err.Error())
+ }
+
+ cli := gemini.NewClient(testClientTLS())
+ response, err := cli.RoundTrip(&gemini.Request{URL: u})
+ if err != nil {
+ t.Fatalf("RoundTrip(): %s", err.Error())
+ }
+
+ if response.Status != gemini.StatusSuccess {
+ t.Errorf("response status: expected %d, got %d", gemini.StatusSuccess, response.Status)
+ }
+ if response.Meta != "text/gemini" {
+ t.Errorf("response meta: expected \"text/gemini\", got %q", response.Meta)
+ }
+
+ if response.Body == nil {
+ t.Fatal("succcess response has nil body")
+ }
+ body, err := io.ReadAll(response.Body)
+ if err != nil {
+ t.Fatalf("ReadAll: %s", err.Error())
+ }
+ if string(body) != "you've found my page" {
+ t.Errorf("response body: expected \"you've found my page\", got %q", string(body))
+ }
+}
+
+func testClientTLS() *tls.Config {
+ return &tls.Config{InsecureSkipVerify: true}
+}
diff --git a/gemini/handler_test.go b/gemini/handler_test.go
new file mode 100644
index 0000000..c83df65
--- /dev/null
+++ b/gemini/handler_test.go
@@ -0,0 +1,117 @@
+package gemini_test
+
+import (
+ "bytes"
+ "context"
+ "io"
+ "net/url"
+ "strings"
+ "testing"
+
+ "tildegit.org/tjp/gus/gemini"
+)
+
+func TestFallthrough(t *testing.T) {
+ h1 := func(ctx context.Context, req *gemini.Request) *gemini.Response {
+ if req.Path == "/one" {
+ return gemini.Success("text/gemini", bytes.NewBufferString("one"))
+ }
+ return gemini.NotFound("nope")
+ }
+
+ h2 := func(ctx context.Context, req *gemini.Request) *gemini.Response {
+ if req.Path == "/two" {
+ return gemini.Success("text/gemini", bytes.NewBufferString("two"))
+ }
+ return gemini.NotFound("no way")
+ }
+
+ fth := gemini.FallthroughHandler(h1, h2)
+
+ u, err := url.Parse("gemini://test.local/one")
+ if err != nil {
+ t.Fatalf("url.Parse: %s", err.Error())
+ }
+
+ resp := fth(context.Background(), &gemini.Request{URL: u})
+
+ if resp.Status != gemini.StatusSuccess {
+ t.Errorf("expected status %d, got %d", gemini.StatusSuccess, resp.Status)
+ }
+
+ if resp.Meta != "text/gemini" {
+ t.Errorf(`expected meta "text/gemini", got %q`, resp.Meta)
+ }
+
+ body, err := io.ReadAll(resp.Body)
+ if err != nil {
+ t.Errorf("Read: %s", err.Error())
+ }
+ if string(body) != "one" {
+ t.Errorf(`expected body "one", got %q`, string(body))
+ }
+
+ u, err = url.Parse("gemini://test.local/two")
+ if err != nil {
+ t.Fatalf("url.Parse: %s", err.Error())
+ }
+
+ resp = fth(context.Background(), &gemini.Request{URL: u})
+
+ if resp.Status != gemini.StatusSuccess {
+ t.Errorf("expected status %d, got %d", gemini.StatusSuccess, resp.Status)
+ }
+
+ if resp.Meta != "text/gemini" {
+ t.Errorf(`expected meta "text/gemini", got %q`, resp.Meta)
+ }
+
+ body, err = io.ReadAll(resp.Body)
+ if err != nil {
+ t.Errorf("Read: %s", err.Error())
+ }
+ if string(body) != "two" {
+ t.Errorf(`expected body "two", got %q`, string(body))
+ }
+
+ u, err = url.Parse("gemini://test.local/three")
+ if err != nil {
+ t.Fatalf("url.Parse: %s", err.Error())
+ }
+
+ resp = fth(context.Background(), &gemini.Request{URL: u})
+
+ if resp.Status != gemini.StatusNotFound {
+ t.Errorf("expected status %d, got %d", gemini.StatusNotFound, resp.Status)
+ }
+}
+
+func TestFilter(t *testing.T) {
+ pred := func(ctx context.Context, req *gemini.Request) bool {
+ return strings.HasPrefix(req.Path, "/allow")
+ }
+ base := func(ctx context.Context, req *gemini.Request) *gemini.Response {
+ return gemini.Success("text/gemini", bytes.NewBufferString("allowed!"))
+ }
+ handler := gemini.Filter(pred, base, nil)
+
+ u, err := url.Parse("gemini://test.local/allow/please")
+ if err != nil {
+ t.Fatalf("url.Parse: %s", err.Error())
+ }
+
+ resp := handler(context.Background(), &gemini.Request{URL: u})
+ if resp.Status != gemini.StatusSuccess {
+ t.Errorf("expected status %d, got %d", gemini.StatusSuccess, resp.Status)
+ }
+
+ u, err = url.Parse("gemini://test.local/disallow/please")
+ if err != nil {
+ t.Fatalf("url.Parse: %s", err.Error())
+ }
+
+ resp = handler(context.Background(), &gemini.Request{URL: u})
+ if resp.Status != gemini.StatusNotFound {
+ t.Errorf("expected status %d, got %d", gemini.StatusNotFound, resp.Status)
+ }
+}
diff --git a/gemini/response_test.go b/gemini/response_test.go
index 7ffb585..616fac4 100644
--- a/gemini/response_test.go
+++ b/gemini/response_test.go
@@ -248,7 +248,7 @@ func TestResponseClose(t *testing.T) {
resp = &gemini.Response{
Status: gemini.StatusInput,
- Meta: "give me more",
+ Meta: "give me more",
}
if err := resp.Close(); err != nil {
@@ -272,7 +272,7 @@ func TestResponseWriteTo(t *testing.T) {
clone := func(resp *gemini.Response) *gemini.Response {
other := &gemini.Response{
Status: resp.Status,
- Meta: resp.Meta,
+ Meta: resp.Meta,
}
if resp.Body != nil {
@@ -291,27 +291,44 @@ func TestResponseWriteTo(t *testing.T) {
other.Body = bytes.NewBuffer(buf2)
}
- return resp
+ return other
}
- r1 := &gemini.Response{
- Status: gemini.StatusSuccess,
- Meta: "text/gemini",
- Body: bytes.NewBufferString("the body goes here"),
+ table := []struct {
+ name string
+ response *gemini.Response
+ }{
+ {
+ name: "simple success",
+ response: gemini.Success(
+ "text/gemini",
+ bytes.NewBufferString("the body goes here"),
+ ),
+ },
+ {
+ name: "no body",
+ response: gemini.Input("need more pls"),
+ },
}
- r2 := clone(r1)
- wtbuf := &bytes.Buffer{}
- if _, err := r1.WriteTo(wtbuf); err != nil {
- t.Fatalf("response.WriteTo(): %s", err.Error())
- }
+ for _, test := range table {
+ t.Run(test.name, func(t *testing.T) {
+ r1 := test.response
+ r2 := clone(test.response)
- rdbuf := make([]byte, wtbuf.Len())
- if n, err := r2.Read(rdbuf); err != nil {
- t.Fatalf("response.Read() -> %d: %s", n, err.Error())
- }
+ rdbuf, err := io.ReadAll(r1)
+ if err != nil {
+ t.Fatalf("response.Read(): %s", err.Error())
+ }
- if wtbuf.String() != string(rdbuf) {
- t.Fatalf("Read produced %q but WriteTo produced %q", string(rdbuf), wtbuf.String())
+ wtbuf := &bytes.Buffer{}
+ if _, err := r2.WriteTo(wtbuf); err != nil {
+ t.Fatalf("response.WriteTo(): %s", err.Error())
+ }
+
+ if wtbuf.String() != string(rdbuf) {
+ t.Fatalf("Read produced %q but WriteTo produced %q", string(rdbuf), wtbuf.String())
+ }
+ })
}
}
diff --git a/gemini/serve.go b/gemini/serve.go
index f9a8a1c..bc13531 100644
--- a/gemini/serve.go
+++ b/gemini/serve.go
@@ -32,10 +32,12 @@ func NewServer(
return nil, err
}
+ addr := listener.Addr()
+
s := &Server{
ctx: ctx,
- network: network,
- address: address,
+ network: addr.Network(),
+ address: addr.String(),
wg: &sync.WaitGroup{},
listener: tls.NewListener(listener, tlsConfig),
handler: handler,
@@ -59,7 +61,7 @@ func (s *Server) Serve() error {
s.ctx, s.cancel = context.WithCancel(s.ctx)
s.wg.Add(1)
- go s.propagateCancel()
+ s.propagateCancel()
for {
conn, err := s.listener.Accept()
diff --git a/gemini/testdata/server.crt b/gemini/testdata/server.crt
new file mode 100644
index 0000000..4e3dc74
--- /dev/null
+++ b/gemini/testdata/server.crt
@@ -0,0 +1,18 @@
+-----BEGIN CERTIFICATE-----
+MIIC7jCCAdYCAQcwDQYJKoZIhvcNAQELBQAwPTESMBAGA1UEAwwJbG9jYWxob3N0
+MQswCQYDVQQGEwJVUzEaMBgGA1UEBwwRU2FuIEZyYW5jaXNjbywgQ0EwHhcNMjMw
+MTExMjAwMDU5WhcNMjUwNDE1MjAwMDU5WjA9MRIwEAYDVQQDDAlsb2NhbGhvc3Qx
+CzAJBgNVBAYTAlVTMRowGAYDVQQHDBFTYW4gRnJhbmNpc2NvLCBDQTCCASIwDQYJ
+KoZIhvcNAQEBBQADggEPADCCAQoCggEBALlaPa1AxDQnMo0qQxY5/Bf7MNf1x6tN
+xjkpMnQnPM+cHmmlkEhI1zwLk/LrLxwq7+OOxMTPrJglrAiDAp1uCZHjKcTMFnwO
+9M5vf8LjtYBjZd8+OSHyYV37gxw7h9/Wsxl+1Yw40QaJKM9auj2xOyaDj5Ou9+yp
+CfbGSpVUTnqReOVFg2QSNwEviOZu1SvAouPyO98WKoXjn7K5mxE545e4mgF1EMht
+jB5kH6kXqZSUszlGA1MkX3AlDsYJIcYnDwelNvw6XTPpkT2wNehxPyD0iP4rs+W4
+5hgV8wYokpgrM3xxe0c4mop5bzrp2Hyz3WxnF7KwtJgHW/6YxhG73skCAwEAATAN
+BgkqhkiG9w0BAQsFAAOCAQEAfI+UE/3d0Fb8BZ2gtv1kUh8yx75LUbpg1aOEsZdP
+Rji+GkL5xiFDsm7BwqTKziAjDtjL2qtGcJJ835shsGiUSK6qJuf9C944utUvCoFm
+b4aUZ8fTmN7PkwRS61nIcHaS1zkiFzUdvbquV3QWSnl9kC+yDLHT0Z535tcvCMVM
+bO7JMj1sxml4Y9B/hfY7zAZJt1giSNH1iDeX2pTpmPPI40UsRn98cC8HZ0d8wFrv
+yc3hKkz8E+WTgZUf7jFk/KX/T5uwu+Y85emwfbb82KIR3oqhkJIfOfpqop2duZXB
+hMuO1QWEBkZ/hpfrAsN/foz8v46P9qgW8gfOfzhyBcqLvA==
+-----END CERTIFICATE-----
diff --git a/gemini/testdata/server.csr b/gemini/testdata/server.csr
new file mode 100644
index 0000000..40934c2
--- /dev/null
+++ b/gemini/testdata/server.csr
@@ -0,0 +1,18 @@
+-----BEGIN CERTIFICATE REQUEST-----
+MIIC0zCCAbsCAQAwPTESMBAGA1UEAwwJbG9jYWxob3N0MQswCQYDVQQGEwJVUzEa
+MBgGA1UEBwwRU2FuIEZyYW5jaXNjbywgQ0EwggEiMA0GCSqGSIb3DQEBAQUAA4IB
+DwAwggEKAoIBAQC5Wj2tQMQ0JzKNKkMWOfwX+zDX9cerTcY5KTJ0JzzPnB5ppZBI
+SNc8C5Py6y8cKu/jjsTEz6yYJawIgwKdbgmR4ynEzBZ8DvTOb3/C47WAY2XfPjkh
+8mFd+4McO4ff1rMZftWMONEGiSjPWro9sTsmg4+TrvfsqQn2xkqVVE56kXjlRYNk
+EjcBL4jmbtUrwKLj8jvfFiqF45+yuZsROeOXuJoBdRDIbYweZB+pF6mUlLM5RgNT
+JF9wJQ7GCSHGJw8HpTb8Ol0z6ZE9sDXocT8g9Ij+K7PluOYYFfMGKJKYKzN8cXtH
+OJqKeW866dh8s91sZxeysLSYB1v+mMYRu97JAgMBAAGgUTBPBgkqhkiG9w0BCQ4x
+QjBAMDEGA1UdJQQqMCgGCCsGAQUFBwMBBggrBgEFBQcDAgYIKwYBBQUHAwMGCCsG
+AQUFBwMEMAsGA1UdDwQEAwIFIDANBgkqhkiG9w0BAQsFAAOCAQEAOKb0Mnnm7oLT
+0fz7+CQ4KYva/dmr75k38PPRXGs/7Ls6nhu59yNhudHJtRyjaAzffwfg1NWxKlUV
+gDf+4K6S+cjz6bWVdU4XwH37V01GWWgzmwDGEsoZZpNstuq87BhI62BKQFKqJrw2
+pqNYoM+p4K7OnOUNT60LshzThguMb4h53YcTXyv7wAf9LABc4v0daVErunDZ5Elh
+QwlUZT/pngTLJiXDjrWB3PGnniTbC0OYhKKmFbX/dIR/TlUH7Fcc4mE9f514mU0n
+zys/mc57gBTdI11oIw1fkQJ6f3LDk3MsFfJntwhxjVeSXJUNOBwsxmxdyigjsifY
+J+SpNczO1Q==
+-----END CERTIFICATE REQUEST-----
diff --git a/gemini/testdata/server.key b/gemini/testdata/server.key
new file mode 100644
index 0000000..44ce348
--- /dev/null
+++ b/gemini/testdata/server.key
@@ -0,0 +1,27 @@
+-----BEGIN RSA PRIVATE KEY-----
+MIIEpQIBAAKCAQEAuVo9rUDENCcyjSpDFjn8F/sw1/XHq03GOSkydCc8z5weaaWQ
+SEjXPAuT8usvHCrv447ExM+smCWsCIMCnW4JkeMpxMwWfA70zm9/wuO1gGNl3z45
+IfJhXfuDHDuH39azGX7VjDjRBokoz1q6PbE7JoOPk6737KkJ9sZKlVROepF45UWD
+ZBI3AS+I5m7VK8Ci4/I73xYqheOfsrmbETnjl7iaAXUQyG2MHmQfqReplJSzOUYD
+UyRfcCUOxgkhxicPB6U2/DpdM+mRPbA16HE/IPSI/iuz5bjmGBXzBiiSmCszfHF7
+RziainlvOunYfLPdbGcXsrC0mAdb/pjGEbveyQIDAQABAoIBAQC36ylkLu4Bahup
+I5RqC6NwEFpJEKLOAmB8+7oKs5yNzTYIUra2Y0DfXgWyd1fJtXlP7aymNgPm/QqV
+b5o6qKNqVWRu2Kw+8YBNDypRMi45dWfyewWp/55J6XYRn6iVna8dz1MKzp3qxFLw
+XfCLor802jqvqmBsPteaPOxo/LzatKhXp/mcO/hsxeMr1iSUVHTrQEIU/aIkmAqT
+/eXp/zVZk7O9Tx8wwCijB3v7j3zTEkcKSwFlAp0w01XeqllmqA5P9rW3vVGXJVIM
+t6t9C8XcJWPIOURz3JWZJpUBSZsyNe2N/wbCgkQV81A0s+4praKzgDbjE+njb0C/
+1CClbHV5AoGBAO/mnOzHe7ZJyYfuiu6ZR2REBY61n2J6DkL1stkN5xd+Op25afHT
+jLBjU98hM/AMtP1aHWFQpdEe0uyqRjV6PbpNE8j/m9AVfjZxzwR4ITW2xqUhXOSz
+89o832RO54TTr19YGnIhdU8dDQmYOcKmCSuw6KwCfHwBzkFuDFZGk/4/AoGBAMXK
+gzNyX3tN9Ug5AUo/Az4jQRSoyLjfnce0a0TF4jxEacUBx2COq3zaV/VADEFBla1t
+5roOAUyJ3V6fXtZnoqwZPYh6iGP8p7Tj6vyXI4SDktV0uAV57qSdajqxTrA7yoXr
+zrbxv3U/3vXr3JTsP42U5zp1m5n1VfVqCXBkynD3AoGBAOvs7JjDWXuctzASPNmH
+LjmB18FQBk3vYQUi4l8pmAF3pyejx3gGJw70r+/4lD5YEMozjD8+88Njv+T1U5SW
+Agysbm+2SMJr0LK0W/W2Olq7xEFzPQrBmmgeg0b/fhoXoBlw6JkjJF3IYSD1bqBp
+bw1jrn4y979weynHkyRpxnM7AoGBALGSzRPlPR/gr7P1qdjUlb61u/omRn7kFC11
+J1EJL8HX0fXTUQK5U/C1vn4q0FXN4elgX+LuK/BhXeNTxbtMM9m6l2nuSIEsFgzr
+Cs9XicWwsqT9MzGHdN9JjFPBV9oU9BAj0uSgSbmkbDHxXYo+SBh+dNIhQF+KyW+Z
+kXvcoXulAoGAA2hnEA17nJ7Vj1DZ4CoRblgjZFAMB64slcSesaorp3WWehvaXO8u
+jbvWuvj58DgvTLiv8xPIn4Zsjd0a77ysifvUcmxSRa/k9UIle/lwjmXGjQ1GSMEI
+FB5ZTqjLZwS9Y5BDxlPcYF7vqE9fNpcxmcfHGmSF5YAHvFOfGH6B63M=
+-----END RSA PRIVATE KEY-----