//go:build linux || freebsd // +build linux freebsd package gemini import ( "bufio" "context" "crypto/ecdsa" "crypto/elliptic" "crypto/rand" "crypto/tls" "crypto/x509" "crypto/x509/pkix" "math/big" "net" "net/url" "strconv" "strings" "testing" "time" "codeberg.org/petrbalvin/goget/internal/core" ) type stringWriter struct{ sb *strings.Builder } func (w *stringWriter) Write(p []byte) (int, error) { return w.sb.Write(p) } func testTLSConfig() *tls.Config { key, _ := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) tmpl := &x509.Certificate{ SerialNumber: big.NewInt(1), Subject: pkix.Name{CommonName: "localhost"}, NotBefore: time.Now(), NotAfter: time.Now().Add(time.Hour), IPAddresses: []net.IP{net.ParseIP("127.0.0.1")}, } certDER, _ := x509.CreateCertificate(rand.Reader, tmpl, tmpl, &key.PublicKey, key) return &tls.Config{ Certificates: []tls.Certificate{{ Certificate: [][]byte{certDER}, PrivateKey: key, }}, } } func TestGeminiSuccess(t *testing.T) { cfg := testTLSConfig() listener, err := tls.Listen("tcp", "127.0.0.1:0", cfg) if err != nil { t.Fatal(err) } defer listener.Close() port := listener.Addr().(*net.TCPAddr).Port go func() { conn, _ := listener.Accept() if conn == nil { return } defer conn.Close() r := bufio.NewReader(conn) r.ReadString('\n') conn.Write([]byte("20 text/gemini\r\n# Hello\n\nContent.\n")) }() proto := NewProtocol() proto.TLSInsecure = true u, _ := url.Parse("gemini://127.0.0.1:" + strconv.Itoa(port) + "/") var buf strings.Builder _, err = proto.Download(context.Background(), &core.DownloadRequest{ URL: u, Writer: &stringWriter{&buf}, }) if err != nil { t.Fatal(err) } if !strings.Contains(buf.String(), "Hello") { t.Errorf("got %q", buf.String()) } } func TestGeminiTemporaryFailure(t *testing.T) { cfg := testTLSConfig() listener, _ := tls.Listen("tcp", "127.0.0.1:0", cfg) defer listener.Close() port := listener.Addr().(*net.TCPAddr).Port go func() { conn, _ := listener.Accept() if conn == nil { return } defer conn.Close() r := bufio.NewReader(conn) r.ReadString('\n') conn.Write([]byte("40 overloaded\r\n")) }() proto := NewProtocol() proto.TLSInsecure = true u, _ := url.Parse("gemini://127.0.0.1:" + strconv.Itoa(port) + "/") _, err := proto.Download(context.Background(), &core.DownloadRequest{URL: u}) if err == nil { t.Error("expected temporary failure error") } } func TestGeminiPermanentFailure(t *testing.T) { cfg := testTLSConfig() listener, _ := tls.Listen("tcp", "127.0.0.1:0", cfg) defer listener.Close() port := listener.Addr().(*net.TCPAddr).Port go func() { conn, _ := listener.Accept() if conn == nil { return } defer conn.Close() r := bufio.NewReader(conn) r.ReadString('\n') conn.Write([]byte("51 not found\r\n")) }() proto := NewProtocol() proto.TLSInsecure = true u, _ := url.Parse("gemini://127.0.0.1:" + strconv.Itoa(port) + "/") _, err := proto.Download(context.Background(), &core.DownloadRequest{URL: u}) if err == nil { t.Error("expected permanent failure error") } } // TestGeminiPinnedCertMismatch exercises the certificate-pinning path. // The server presents a self-signed cert whose hash does NOT match the // pinned value, so the download must fail with a pinning error. func TestGeminiPinnedCertMismatch(t *testing.T) { cfg := testTLSConfig() listener, err := tls.Listen("tcp", "127.0.0.1:0", cfg) if err != nil { t.Fatal(err) } defer listener.Close() port := listener.Addr().(*net.TCPAddr).Port go func() { conn, _ := listener.Accept() if conn == nil { return } defer conn.Close() r := bufio.NewReader(conn) r.ReadString('\n') conn.Write([]byte("20 text/gemini\r\nhello\n")) }() proto := NewProtocol() proto.TLSInsecure = true // accept self-signed, then let pinning reject u, _ := url.Parse("gemini://127.0.0.1:" + strconv.Itoa(port) + "/") _, err = proto.Download(context.Background(), &core.DownloadRequest{ URL: u, PinnedCertHash: "0000000000000000000000000000000000000000000000000000000000000000", }) if err == nil { t.Fatal("expected pinning failure") } if !strings.Contains(err.Error(), "pinning failed") { t.Errorf("expected pinning error, got: %v", err) } }