go-gemini-0.12.1/0000755000175000017500000000000014153772743013011 5ustar nileshnileshgo-gemini-0.12.1/LICENSE-GO0000644000175000017500000000301514153772743014320 0ustar nileshnileshPortions of this program were taken from Go: Copyright (c) 2009 The Go Authors. All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name of Google Inc. nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. go-gemini-0.12.1/go.sum0000644000175000017500000000152314153772743014145 0ustar nileshnileshgithub.com/google/go-cmp v0.3.1 h1:Xye71clBPdm5HgqGwUkwhbynsUJZhDbS20FvLhQ2izg= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= golang.org/x/net v0.0.0-20201216054612-986b41b23924 h1:QsnDpLLOKwHBBDa8nDws4DYNc/ryVW2vCpxCs09d4PY= golang.org/x/net v0.0.0-20201216054612-986b41b23924/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= go-gemini-0.12.1/client_test.go0000644000175000017500000001034614153772743015661 0ustar nileshnileshpackage gemini import ( "fmt" "io/ioutil" "net/url" "os" "strings" "testing" "github.com/google/go-cmp/cmp" ) func compareResponses(expected, given *Response) (diff string) { diff = cmp.Diff(expected.Meta, given.Meta) if diff != "" { return } diff = cmp.Diff(expected.Meta, given.Meta) if diff != "" { return } expectedBody, err := ioutil.ReadAll(expected.Body) if err != nil { return fmt.Sprintf("failed to get expected body: %v", err) } givenBody, err := ioutil.ReadAll(given.Body) if err != nil { return fmt.Sprintf("failed to get givenponse body: %v", err) } diff = cmp.Diff(expectedBody, givenBody) return } func TestGetResponse(t *testing.T) { tests := []struct { file string expected Response }{ {"resources/tests/simple_response", Response{20, "text/gemini", ioutil.NopCloser(strings.NewReader("This is the content of the page\r\n")), nil, nil}}, } for _, tc := range tests { f, err := os.Open(tc.file) if err != nil { t.Fatalf("failed to get test case file %s: %v", tc.file, err) } res := Response{} err = getResponse(&res, f) if err != nil { t.Fatalf("failed to parse response %s: %v", tc.file, err) } diff := compareResponses(&tc.expected, &res) if diff != "" { t.Fatalf(diff) } } } func TestGetResponseEmptyResponse(t *testing.T) { err := getResponse(&Response{}, ioutil.NopCloser(strings.NewReader(""))) if err == nil { t.Fatalf("expected to get an error for empty response, got nil instead") } } func TestGetResponseInvalidStatus(t *testing.T) { err := getResponse(&Response{}, ioutil.NopCloser(strings.NewReader("AA\tmeta\r\n"))) if err == nil { t.Fatalf("expected to get an error for invalid status response, got nil instead") } } func TestGetHeaderLongMeta(t *testing.T) { // Meta longer than allowed _, err := getHeader(strings.NewReader("20 " + strings.Repeat("a", MetaMaxLength+1) + "\r\n")) if err == nil { t.Fatalf(fmt.Sprintf("expected to get an error for meta longer than %d", MetaMaxLength)) } } func TestGetHeaderOnlyLF(t *testing.T) { // Meta longer than 1024 chars _, err := getHeader(strings.NewReader("20 test" + "\n")) if err == nil { t.Fatalf("expected to get an error for header ending only in LF") } } func TestGetHeaderNoSpace(t *testing.T) { _, err := getHeader(strings.NewReader("20\r\n")) if err == nil { t.Fatalf("expected to get an error for header with no space") } } func parse(s string) *url.URL { p, _ := url.Parse(s) return p } func TestGetHost(t *testing.T) { tests := []struct { host string url string }{ {"example.com:1965", "gemini://example.com:1965"}, {"example.com:1965", "gemini://example.com"}, {"example.com:1965", "gemini://example.com/test//"}, {"example.com:123", "gemini://example.com:123"}, {"example.com:123", "gemini://example.com:123/test//"}, {"0.0.0.0:1965", "gemini://0.0.0.0:1965"}, {"0.0.0.0:1965", "gemini://0.0.0.0"}, {"0.0.0.0:1965", "gemini://0.0.0.0/test//"}, {"0.0.0.0:123", "gemini://0.0.0.0:123"}, {"0.0.0.0:123", "gemini://0.0.0.0:123/test//"}, {"[::1]:1965", "gemini://[::1]:1965"}, {"[::1]:1965", "gemini://[::1]"}, {"[::1]:1965", "gemini://[::1]/test//"}, {"[::1]:123", "gemini://[::1]:123"}, {"[::1]:123", "gemini://[::1]:123/test//"}, } for _, tc := range tests { host := getHost(parse(tc.url)) if tc.host != host { t.Errorf("Got %s but expected %s for URL %s", host, tc.host, tc.url) } } } func TestGetPunycodeURL(t *testing.T) { tests := []struct { expected string url string }{ {"gemini://example.com:1965", "gemini://example.com:1965"}, {"gemini://example.com", "gemini://example.com"}, {"gemini://example.com/test//", "gemini://example.com/test//"}, {"gemini://xn--gmeaux-bva.bortzmeyer.org/", "gemini://gémeaux.bortzmeyer.org/"}, {"gemini://1.2.3.4/", "gemini://1.2.3.4/"}, {"gemini://1.2.3.4:33/", "gemini://1.2.3.4:33/"}, {"gemini://[::1]:1234", "gemini://[::1]:1234"}, {"gemini://xn--gmeaux-bva.bortzmeyer.org:1965/", "gemini://gémeaux.bortzmeyer.org:1965/"}, } for _, tc := range tests { u, err := GetPunycodeURL(tc.url) if err != nil { t.Errorf("Got error %v for URL %s , expected %s", err, tc.url, tc.expected) } if tc.expected != u { t.Errorf("Got %s but expected %s for URL %s", u, tc.expected, tc.url) } } } go-gemini-0.12.1/README.md0000644000175000017500000000441414153772743014273 0ustar nileshnilesh# go-gemini [![Go Reference](https://pkg.go.dev/badge/github.com/makeworld-the-better-one/go-gemini.svg)](https://pkg.go.dev/github.com/makeworld-the-better-one/go-gemini) go-gemini is a library that provides an easy interface to create client ~~and servers~~ that speak the [Gemini protocol](https://gemini.circumlunar.space/). **Spec version supported:** v0.16.0, November 14th 2021 This version of the library was forked from [~yotam/go-gemini](https://git.sr.ht/~yotam/go-gemini/) to add additional features, as well as update it to support newer specs. At the time of forking, it had not seen any new commit for 5 months, and was based on v0.9.2. If there are any future upstream updates, I will make an effort to include them. **The server part of this library has been removed.** I don't use it and don't want to maintain it. This is mostly a personal library. You might want to check out [go-gemini](https://sr.ht/~adnano/go-gemini) (no relation) for more features. ## Improvements This fork of the library improves on the original in several ways, some listed above already. - Client supports self-signed certs sent by the server, but still has other checks like expiry date and hostname - The original library could only work with self-signed certs by disabling all security. - Invalid status code numbers raise an error - Set default port and scheme for client requests - Raise error when META strings are too long in the response header - Supports new status code updates - If `SSLKEYLOGFILE` is set, session keys are written to the file in NSS format. This is useful for debugging TLS connections (but breaks security, so don't use unless necessary). - Support proxies - Support client certs - Add connection/header timeouts, and read timeouts - And more! ## Notes This library only works with Go 1.15 and higher. If you want relatively reliable code, use the latest tag, not the latest commit. Code in the latest master might be untested/buggy. The API might change between tags since it is still v0. ## License This library is under the ISC License, see the [LICENSE](./LICENSE) file for details. Portions of this library's code are taken from Go, and are under a different license, which can be found in [LICENSE-GO](./LICENSE-GO). Those files are marked accordingly in their comments. go-gemini-0.12.1/resources/0000755000175000017500000000000014153772743015023 5ustar nileshnileshgo-gemini-0.12.1/resources/tests/0000755000175000017500000000000014153772743016165 5ustar nileshnileshgo-gemini-0.12.1/resources/tests/simple_response0000644000175000017500000000006114153772743021314 0ustar nileshnilesh20 text/gemini This is the content of the page go-gemini-0.12.1/go.mod0000644000175000017500000000024114153772743014114 0ustar nileshnileshmodule github.com/makeworld-the-better-one/go-gemini go 1.14 require ( github.com/google/go-cmp v0.3.1 golang.org/x/net v0.0.0-20201216054612-986b41b23924 ) go-gemini-0.12.1/verify_hostname.go0000644000175000017500000001400114153772743016536 0ustar nileshnilesh// Source: https://git.sr.ht/~adnano/go-gemini/tree/f6b0443a6262d17f90b4e75cf5ae37577db7f897/vendor.go // No code changes were made. // Hostname verification code from the crypto/x509 package. // Modified to allow Common Names in the short term, until new certificates // can be issued with SANs. // Copyright 2011 The Go Authors. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE-GO file. package gemini import ( "crypto/x509" "net" "strings" "unicode/utf8" ) var oidExtensionSubjectAltName = []int{2, 5, 29, 17} func hasSANExtension(c *x509.Certificate) bool { for _, e := range c.Extensions { if e.Id.Equal(oidExtensionSubjectAltName) { return true } } return false } func validHostnamePattern(host string) bool { return validHostname(host, true) } func validHostnameInput(host string) bool { return validHostname(host, false) } // validHostname reports whether host is a valid hostname that can be matched or // matched against according to RFC 6125 2.2, with some leniency to accommodate // legacy values. func validHostname(host string, isPattern bool) bool { if !isPattern { host = strings.TrimSuffix(host, ".") } if len(host) == 0 { return false } for i, part := range strings.Split(host, ".") { if part == "" { // Empty label. return false } if isPattern && i == 0 && part == "*" { // Only allow full left-most wildcards, as those are the only ones // we match, and matching literal '*' characters is probably never // the expected behavior. continue } for j, c := range part { if 'a' <= c && c <= 'z' { continue } if '0' <= c && c <= '9' { continue } if 'A' <= c && c <= 'Z' { continue } if c == '-' && j != 0 { continue } if c == '_' { // Not a valid character in hostnames, but commonly // found in deployments outside the WebPKI. continue } return false } } return true } // commonNameAsHostname reports whether the Common Name field should be // considered the hostname that the certificate is valid for. This is a legacy // behavior, disabled by default or if the Subject Alt Name extension is present. // // It applies the strict validHostname check to the Common Name field, so that // certificates without SANs can still be validated against CAs with name // constraints if there is no risk the CN would be matched as a hostname. // See NameConstraintsWithoutSANs and issue 24151. func commonNameAsHostname(c *x509.Certificate) bool { return !hasSANExtension(c) && validHostnamePattern(c.Subject.CommonName) } func matchExactly(hostA, hostB string) bool { if hostA == "" || hostA == "." || hostB == "" || hostB == "." { return false } return toLowerCaseASCII(hostA) == toLowerCaseASCII(hostB) } func matchHostnames(pattern, host string) bool { pattern = toLowerCaseASCII(pattern) host = toLowerCaseASCII(strings.TrimSuffix(host, ".")) if len(pattern) == 0 || len(host) == 0 { return false } patternParts := strings.Split(pattern, ".") hostParts := strings.Split(host, ".") if len(patternParts) != len(hostParts) { return false } for i, patternPart := range patternParts { if i == 0 && patternPart == "*" { continue } if patternPart != hostParts[i] { return false } } return true } // toLowerCaseASCII returns a lower-case version of in. See RFC 6125 6.4.1. We use // an explicitly ASCII function to avoid any sharp corners resulting from // performing Unicode operations on DNS labels. func toLowerCaseASCII(in string) string { // If the string is already lower-case then there's nothing to do. isAlreadyLowerCase := true for _, c := range in { if c == utf8.RuneError { // If we get a UTF-8 error then there might be // upper-case ASCII bytes in the invalid sequence. isAlreadyLowerCase = false break } if 'A' <= c && c <= 'Z' { isAlreadyLowerCase = false break } } if isAlreadyLowerCase { return in } out := []byte(in) for i, c := range out { if 'A' <= c && c <= 'Z' { out[i] += 'a' - 'A' } } return string(out) } // verifyHostname returns nil if c is a valid certificate for the named host. // Otherwise it returns an error describing the mismatch. // // IP addresses can be optionally enclosed in square brackets and are checked // against the IPAddresses field. Other names are checked case insensitively // against the DNSNames field. If the names are valid hostnames, the certificate // fields can have a wildcard as the left-most label. // // The legacy Common Name field is ignored unless it's a valid hostname, the // certificate doesn't have any Subject Alternative Names, and the GODEBUG // environment variable is set to "x509ignoreCN=0". Support for Common Name is // deprecated will be entirely removed in the future. func verifyHostname(c *x509.Certificate, h string) error { // IP addresses may be written in [ ]. candidateIP := h if len(h) >= 3 && h[0] == '[' && h[len(h)-1] == ']' { candidateIP = h[1 : len(h)-1] } if ip := net.ParseIP(candidateIP); ip != nil { // We only match IP addresses against IP SANs. // See RFC 6125, Appendix B.2. for _, candidate := range c.IPAddresses { if ip.Equal(candidate) { return nil } } return x509.HostnameError{c, candidateIP} } names := c.DNSNames if commonNameAsHostname(c) { names = []string{c.Subject.CommonName} } candidateName := toLowerCaseASCII(h) // Save allocations inside the loop. validCandidateName := validHostnameInput(candidateName) for _, match := range names { // Ideally, we'd only match valid hostnames according to RFC 6125 like // browsers (more or less) do, but in practice Go is used in a wider // array of contexts and can't even assume DNS resolution. Instead, // always allow perfect matches, and only apply wildcard and trailing // dot processing to valid hostnames. if validCandidateName && validHostnamePattern(match) { if matchHostnames(match, candidateName) { return nil } } else { if matchExactly(match, candidateName) { return nil } } } return x509.HostnameError{c, h} } go-gemini-0.12.1/client.go0000644000175000017500000002715514153772743014630 0ustar nileshnileshpackage gemini import ( "bytes" "crypto/tls" "crypto/x509" "fmt" "io" "net" "net/url" "os" "strconv" "strings" "time" "golang.org/x/net/idna" ) func punycodeHost(host string) (string, error) { hostname, port, err := net.SplitHostPort(host) if err != nil { // Likely means no port hostname = host port = "" } if net.ParseIP(hostname) != nil { // Hostname is IP address, not domain return host, nil } pc, err := idna.ToASCII(hostname) if err != nil { return host, err } if port == "" { return pc, nil } return net.JoinHostPort(pc, port), nil } func punycodeHostFromURL(u string) (string, error) { parsed, err := url.Parse(u) if err != nil { return "", err } return punycodeHost(parsed.Host) } // GetPunycodeURL takes a full URL that potentially has Unicode in the // domain name, and returns a URL with the domain punycoded. func GetPunycodeURL(u string) (string, error) { parsed, err := url.Parse(u) if err != nil { return "", nil } host, err := punycodeHostFromURL(u) if err != nil { return "", err } parsed.Host = host return parsed.String(), nil } // Response represents the response from a Gemini server. type Response struct { Status int Meta string Body io.ReadCloser Cert *x509.Certificate conn net.Conn } type header struct { status int meta string } type Client struct { // NoTimeCheck allows connections with expired or future certs if set to true. NoTimeCheck bool // NoHostnameCheck allows connections when the cert doesn't match the // requested hostname or IP. NoHostnameCheck bool // Insecure disables all TLS-based checks, use with caution. // It overrides all the variables above. Insecure bool // AllowOutOfRangeStatuses means the client won't raise an error if a status // that is out of range is returned. // Use CleanStatus() to handle statuses that are in range but not specified in // the spec. AllowOutOfRangeStatuses bool // ConnectTimeout is equivalent to the Timeout field in net.Dialer. // It's the max amount of time allowed for the initial connection/handshake. // The timeout of the DefaultClient is 15 seconds. // // If ReadTimeout is not set, then this value is also used to time out on getting // the header after the connection is made. ConnectTimeout time.Duration // ReadTimeout is the max amount of time reading to a server can take. // This should not be set if you want to support streams. // It is equivalent to net.Conn.SetDeadline, see that func for more documentation. // // For example, if this is set to 30 seconds, then no more reading from the connection // can happen 30 seconds after the initial handshake. ReadTimeout time.Duration } var DefaultClient = &Client{ConnectTimeout: 15 * time.Second} // getHost returns a full host for the given URL, always including a port. // It also punycodes the host, in case it contains Unicode. func getHost(parsedURL *url.URL) string { host, _ := punycodeHostFromURL(parsedURL.String()) if parsedURL.Port() == "" { host = net.JoinHostPort(parsedURL.Hostname(), "1965") } return host } // SetReadTimeout changes the read timeout after the connection has been made. // You can set it to 0 or less to disable the timeout. Otherwise, the duration // is relative to the time the function was called. func (r *Response) SetReadTimeout(d time.Duration) error { if d <= 0 { return r.conn.SetDeadline(time.Time{}) } return r.conn.SetDeadline(time.Now().Add(d)) } // TODO: apply punycoding to hosts // Fetch a resource from a Gemini server with the given URL. // It assumes port 1965 if no port is specified. func (c *Client) Fetch(rawURL string) (*Response, error) { parsedURL, err := url.Parse(rawURL) if err != nil { return nil, fmt.Errorf("failed to parse URL: %w", err) } return c.FetchWithHost(getHost(parsedURL), rawURL) } // FetchWithHost fetches a resource from a Gemini server at the given host, with the given URL. // This can be used for proxying, where the URL host and actual server don't match. // It assumes the host is using port 1965 if no port number is provided. func (c *Client) FetchWithHost(host, rawURL string) (*Response, error) { // Call with empty PEM bytes to skip using a cert return c.FetchWithHostAndCert(host, rawURL, []byte{}, []byte{}) } // FetchWithCert fetches a resource from a Gemini server with the given URL. // It allows you to provide the bytes of a PEM encoded block for a client // certificate and its key. This allows you to make requests using client // certs. // // It assumes port 1965 if no port is specified. func (c *Client) FetchWithCert(rawURL string, certPEM, keyPEM []byte) (*Response, error) { parsedURL, err := url.Parse(rawURL) if err != nil { return nil, fmt.Errorf("failed to parse URL: %w", err) } // Call with empty PEM bytes to skip using a cert return c.FetchWithHostAndCert(getHost(parsedURL), rawURL, certPEM, keyPEM) } // FetchWithHostAndCert combines FetchWithHost and FetchWithCert. func (c *Client) FetchWithHostAndCert(host, rawURL string, certPEM, keyPEM []byte) (*Response, error) { u, err := GetPunycodeURL(rawURL) if err != nil { return nil, fmt.Errorf("error when punycoding URL: %w", err) } parsedURL, _ := url.Parse(u) if len(u) > URLMaxLength { // Out of spec return nil, fmt.Errorf("url is too long") } // Add port to host if needed _, _, err = net.SplitHostPort(host) if err != nil { // Error likely means there's no port in the host host = net.JoinHostPort(host, "1965") } ogHost := host host, err = punycodeHost(host) if err != nil { return nil, fmt.Errorf("failed to punycode host %s: %w", ogHost, err) } // Build tls.Certificate var cert tls.Certificate if len(certPEM) == 0 && len(keyPEM) == 0 { // Cert bytes were intentionally left empty cert = tls.Certificate{} } else { cert, err = tls.X509KeyPair(certPEM, keyPEM) if err != nil { return nil, fmt.Errorf("failed to parse cert/key PEM: %w", err) } } res := Response{} // Connect start := time.Now() conn, err := c.connect(&res, host, parsedURL, cert) if err != nil { return nil, fmt.Errorf("failed to connect to the server: %w", err) } // Send request if c.ReadTimeout == 0 && c.ConnectTimeout != 0 { // No r/w timeout, so a timeout for sending the request must be set conn.SetDeadline(start.Add(c.ConnectTimeout)) } err = sendRequest(conn, u) if err != nil { conn.Close() return nil, err } if c.ReadTimeout == 0 && c.ConnectTimeout != 0 { // Undo deadline conn.SetDeadline(time.Time{}) } // Get header if c.ReadTimeout == 0 && c.ConnectTimeout != 0 { // No r/w timeout, so a timeout for getting the header conn.SetDeadline(start.Add(c.ConnectTimeout)) } err = getResponse(&res, conn) if err != nil { conn.Close() return nil, err } if c.ReadTimeout == 0 && c.ConnectTimeout != 0 { // Undo deadline conn.SetDeadline(time.Time{}) } // Check status code if !c.AllowOutOfRangeStatuses && !StatusInRange(res.Status) { conn.Close() return nil, fmt.Errorf("invalid status code: %v", res.Status) } return &res, nil } // Fetch a resource from a Gemini server with the given URL. // It assumes port 1965 if no port is specified. func Fetch(url string) (*Response, error) { return DefaultClient.Fetch(url) } // FetchWithCert fetches a resource from a Gemini server with the given URL. // It allows you to provide the bytes of a PEM encoded block for a client // certificate and its key. This allows you to make requests using client // certs. // // It assumes port 1965 if no port is specified. func FetchWithCert(url string, certPEM, keyPEM []byte) (*Response, error) { return DefaultClient.FetchWithCert(url, certPEM, keyPEM) } // FetchWithHost fetches a resource from a Gemini server at the given host, with the given URL. // This can be used for proxying, where the URL host and actual server don't match. // It assumes the host is using port 1965 if no port number is provided. func FetchWithHost(host, url string) (*Response, error) { return DefaultClient.FetchWithHost(host, url) } // FetchWithHostAndCert combines FetchWithHost and FetchWithCert. func FetchWithHostAndCert(host, url string, certPEM, keyPEM []byte) (*Response, error) { return DefaultClient.FetchWithHostAndCert(host, url, certPEM, keyPEM) } func (c *Client) connect(res *Response, host string, parsedURL *url.URL, clientCert tls.Certificate) (net.Conn, error) { conf := &tls.Config{ MinVersion: tls.VersionTLS12, InsecureSkipVerify: true, // This must be set to allow self-signed certs } if clientCert.Certificate != nil { // There is data, not an empty struct conf.Certificates = []tls.Certificate{clientCert} } // Support logging TLS keys for debugging - See PR #5 keylogfile := os.Getenv("SSLKEYLOGFILE") if keylogfile != "" { w, err := os.OpenFile(keylogfile, os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0600) if err == nil { conf.KeyLogWriter = w defer w.Close() } } // Dialer timeout for handshake conn, err := tls.DialWithDialer(&net.Dialer{Timeout: c.ConnectTimeout}, "tcp", host, conf) res.conn = conn if err != nil { return conn, err } if c.ReadTimeout != 0 { conn.SetDeadline(time.Now().Add(c.ReadTimeout)) } cert := conn.ConnectionState().PeerCertificates[0] res.Cert = cert if c.Insecure { return conn, nil } // Verify hostname if !c.NoHostnameCheck { // Cert hostname has to match connection host, not request host hostname, _, _ := net.SplitHostPort(host) if err := verifyHostname(cert, hostname); err != nil { // Try with Unicode version uniHost, uniErr := idna.ToUnicode(hostname) err2 := verifyHostname(cert, uniHost) if uniErr != nil { return nil, fmt.Errorf("punycoded hostname does not verify and could not be converted to Unicode: %w", err) } if err2 != nil { return nil, fmt.Errorf("hostname does not verify: %w", err2) } return nil, fmt.Errorf("hostname does not verify: %w", err) } } // Verify expiry if !c.NoTimeCheck { if cert.NotBefore.After(time.Now()) { return nil, fmt.Errorf("server cert is for the future") } else if cert.NotAfter.Before(time.Now()) { return nil, fmt.Errorf("server cert is expired") } } return conn, nil } func sendRequest(conn io.Writer, requestURL string) error { _, err := fmt.Fprintf(conn, "%s\r\n", requestURL) if err != nil { return fmt.Errorf("could not send request to the server: %w", err) } return nil } func getResponse(res *Response, conn io.ReadCloser) error { header, err := getHeader(conn) if err != nil { conn.Close() return fmt.Errorf("failed to get header: %w", err) } res.Status = header.status res.Meta = header.meta res.Body = conn return nil } func getHeader(conn io.Reader) (header, error) { line, err := readHeader(conn) if err != nil { return header{}, fmt.Errorf("failed to read header: %w", err) } fields := strings.Fields(string(line)) if len(fields) < 2 && line[len(line)-1] != ' ' { return header{}, fmt.Errorf("header not formatted correctly") } status, err := strconv.Atoi(fields[0]) if err != nil { return header{}, fmt.Errorf("unexpected status value %v: %w", fields[0], err) } var meta string if len(line) <= 3 { meta = "" } else { meta = string(line)[len(fields[0])+1:] } if len(meta) > MetaMaxLength { return header{}, fmt.Errorf("meta string is too long") } return header{status, meta}, nil } func readHeader(conn io.Reader) ([]byte, error) { var line []byte delim := []byte("\r\n") // A small buffer is inefficient but the maximum length of the header is small so it's okay buf := make([]byte, 1) for { n, err := conn.Read(buf) if err == io.EOF && n <= 0 { return []byte{}, err } else if err != nil && err != io.EOF { return []byte{}, err } line = append(line, buf...) if bytes.HasSuffix(line, delim) { return line[:len(line)-len(delim)], nil } } } go-gemini-0.12.1/doc.go0000644000175000017500000000106314153772743014105 0ustar nileshnilesh// Package gemini provides an easy interface to create client and servers that // speak the Gemini protocol. // // At the moment, this library is client-side only, and support is not guaranteed. // It is mostly a personal library. // // It will automatically handle URLs that have IDNs in them, ie domains with Unicode. // It will convert to punycode for DNS and for sending to the server, but accept // certs with either punycode or Unicode as the hostname. // // This also applies to hosts, for functions where a host can be passed specifically. package gemini go-gemini-0.12.1/gemini.go0000644000175000017500000000730514153772743014615 0ustar nileshnileshpackage gemini import ( "fmt" "net/url" "strings" ) const ( URLMaxLength = 1024 MetaMaxLength = 1024 ) // Gemini status codes as defined in the Gemini spec Appendix 1. const ( StatusInput = 10 StatusSensitiveInput = 11 StatusSuccess = 20 StatusRedirect = 30 StatusRedirectTemporary = 30 StatusRedirectPermanent = 31 StatusTemporaryFailure = 40 StatusUnavailable = 41 StatusCGIError = 42 StatusProxyError = 43 StatusSlowDown = 44 StatusPermanentFailure = 50 StatusNotFound = 51 StatusGone = 52 StatusProxyRequestRefused = 53 StatusBadRequest = 59 StatusClientCertificateRequired = 60 StatusCertificateNotAuthorised = 61 StatusCertificateNotValid = 62 ) var statusText = map[int]string{ StatusInput: "Input", StatusSensitiveInput: "Sensitive Input", StatusSuccess: "Success", // StatusRedirect: "Redirect - Temporary" StatusRedirectTemporary: "Redirect - Temporary", StatusRedirectPermanent: "Redirect - Permanent", StatusTemporaryFailure: "Temporary Failure", StatusUnavailable: "Server Unavailable", StatusCGIError: "CGI Error", StatusProxyError: "Proxy Error", StatusSlowDown: "Slow Down", StatusPermanentFailure: "Permanent Failure", StatusNotFound: "Not Found", StatusGone: "Gone", StatusProxyRequestRefused: "Proxy Request Refused", StatusBadRequest: "Bad Request", StatusClientCertificateRequired: "Client Certificate Required", StatusCertificateNotAuthorised: "Certificate Not Authorised", StatusCertificateNotValid: "Certificate Not Valid", } // StatusText returns a text for the Gemini status code. It returns the empty // string if the code is unknown. func StatusText(code int) string { return statusText[code] } // SimplifyStatus simplify the response status by ommiting the detailed second digit of the status code. func SimplifyStatus(status int) int { return (status / 10) * 10 } // IsStatusValid checks whether an int status is covered by the spec. // Note that: // A client SHOULD deal with undefined status codes // between '10' and '69' per the default action of the initial digit. func IsStatusValid(status int) bool { _, found := statusText[status] return found } // StatusInRange returns true if the status has a valid first digit. // This means it can be handled even if it's not defined by the spec, // because it has a known category func StatusInRange(status int) bool { if status < 10 || status > 69 { return false } return true } // CleanStatus returns the status code as is, unless it's invalid but still in range // Then it returns the status code with the second digit zeroed. So 51 returns 51, // but 22 returns 20. // // This corresponds with the spec: // A client SHOULD deal with undefined status codes // between '10' and '69' per the default action of the initial digit. func CleanStatus(status int) int { // All the functions come together! if !IsStatusValid(status) && StatusInRange(status) { return SimplifyStatus(status) } return status } // QueryEscape provides URL query escaping in a way that follows the Gemini spec. // It is the same as url.PathEscape, but it also replaces the +, because Gemini // requires percent-escaping for queries. func QueryEscape(query string) string { return strings.ReplaceAll(url.PathEscape(query), "+", "%2B") } // QueryUnescape is the same as url.PathUnescape func QueryUnescape(query string) (string, error) { return url.PathUnescape(query) } type Error struct { Err error Status int } func (e Error) Error() string { return fmt.Sprintf("Status %d: %v", e.Status, e.Err) } func (e Error) Unwrap() error { return e.Err } go-gemini-0.12.1/.gitignore0000644000175000017500000000161614153772743015005 0ustar nileshnilesh # Created by https://www.toptal.com/developers/gitignore/api/code,linux,go # Edit at https://www.toptal.com/developers/gitignore?templates=code,linux,go ### Code ### .vscode/* !.vscode/settings.json !.vscode/tasks.json !.vscode/launch.json !.vscode/extensions.json ### Go ### # Binaries for programs and plugins *.exe *.exe~ *.dll *.so *.dylib # Test binary, built with `go test -c` *.test # Output of the go coverage tool, specifically when used with LiteIDE *.out ### Go Patch ### /vendor/ /Godeps/ ### Linux ### *~ # temporary files which can be created if a process still has a handle open of a deleted file .fuse_hidden* # KDE directory preferences .directory # Linux trash folder which might appear on any partition or disk .Trash-* # .nfs files are created when an open file is removed but is still being accessed .nfs* # End of https://www.toptal.com/developers/gitignore/api/code,linux,go go-gemini-0.12.1/LICENSE0000644000175000017500000000134514153772743014021 0ustar nileshnileshISC License Copyright (c) 2020, makeworld Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies. THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. go-gemini-0.12.1/gemini_test.go0000644000175000017500000000132314153772743015646 0ustar nileshnileshpackage gemini import "testing" func TestSimplifyStatus(t *testing.T) { tests := []struct { ComplexStatus int SimpleStatus int }{ {10, 10}, {20, 20}, {21, 20}, {44, 40}, {59, 50}, } for _, tt := range tests { result := SimplifyStatus(tt.ComplexStatus) if result != tt.SimpleStatus { t.Errorf("Expected the simplified status of %d to be %d, got %d instead", tt.ComplexStatus, tt.SimpleStatus, result) } } } func TestQuery(t *testing.T) { query := `t/&^*% es\++\t` escaped := `t%2F&%5E%2A%25%20es%5C%2B%2B%5Ct` if QueryEscape(query) != escaped { t.Errorf("Query escape failed") } q, err := QueryUnescape(escaped) if q != query || err != nil { t.Errorf("Query unescape failed") } } go-gemini-0.12.1/.github/0000755000175000017500000000000014153772743014351 5ustar nileshnileshgo-gemini-0.12.1/.github/workflows/0000755000175000017500000000000014153772743016406 5ustar nileshnileshgo-gemini-0.12.1/.github/workflows/test.yml0000644000175000017500000000103614153772743020110 0ustar nileshnileshname: Test on: push: paths-ignore: - "**.md" - "LICENSE" pull_request: paths-ignore: - "**.md" - "LICENSE" - "LICENSE-GO" jobs: test: runs-on: ubuntu-latest strategy: fail-fast: false matrix: go-version: ["1.15", "1.16", "1.17"] steps: - name: Install Go uses: actions/setup-go@v2 with: go-version: ${{ matrix.go-version }} - name: Checkout code uses: actions/checkout@v2 - name: Test run: go test ./...