From 9bed6157351eea6fdbccff69aba5cc967a0b2a56 Mon Sep 17 00:00:00 2001 From: Feuerfuchs Date: Sat, 16 Nov 2019 00:55:25 +0100 Subject: Initial Gemini support --- libgemini.go | 153 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 153 insertions(+) create mode 100644 libgemini.go (limited to 'libgemini.go') diff --git a/libgemini.go b/libgemini.go new file mode 100644 index 0000000..05321ef --- /dev/null +++ b/libgemini.go @@ -0,0 +1,153 @@ +package gopherproxy + +import ( + "bufio" + "crypto/tls" + "errors" + "io" + "net/url" + "regexp" + "strconv" + "strings" +) + +const ( + CRLF = "\r\n" +) + +const ( + STATUS_INPUT = 10 + STATUS_SUCCESS = 20 + STATUS_SUCCESS_CERT = 21 + STATUS_REDIRECT_TEMP = 30 + STATUS_REDIRECT_PERM = 31 + STATUS_TEMP_FAILURE = 40 + STATUS_SERVER_UNAVAILABLE = 41 + STATUS_CGI_ERROR = 42 + STATUS_PROXY_ERROR = 43 + STATUS_SLOW_DOWN = 44 + STATUS_PERM_FAILURE = 50 + STATUS_NOT_FOUND = 51 + STATUS_GONE = 52 + STATUS_PROXY_REFUSED = 53 + STATUS_BAD_REQUEST = 59 + STATUS_CLIENT_CERT_EXPIRED = 60 + STATUS_TRANSIENT_CERT_REQUEST = 61 + STATUS_AUTH_CERT_REQUIRED = 62 + STATUS_CERT_REJECTED = 63 + STATUS_FUTURE_CERT_REJECTED = 64 + STATUS_EXPIRED_CERT_REJECTED = 65 +) + +const ( + MIME_GEMINI = "text/gemini" + DEFAULT_MIME = MIME_GEMINI + DEFAULT_CHARSET = "utf-8" +) + +var ( + HeaderPattern = regexp.MustCompile("^(\\d\\d)[ \\t]+(.*)$") + MimeTypePattern = regexp.MustCompile("^[-\\w.]+/[-\\w.]+") + MimeCharsetPattern = regexp.MustCompile("charset=([^ ;]+)") + GeminiLinkPattern = regexp.MustCompile("^=>[ \\t]*([^ ]+)(?:[ \\t]+(.*))?$") +) + +type GeminiHeader struct { + Status int + Meta string +} + +type GeminiResponse struct { + Header *GeminiHeader + Body io.Reader +} + +func GeminiGet(uri string) (*GeminiResponse, error) { + u, err := url.Parse(uri) + if err != nil { + return nil, err + } + + if u.Scheme != "gemini" { + return nil, errors.New("invalid scheme for uri") + } + + var ( + host string + port int + ) + + hostport := strings.Split(u.Host, ":") + if len(hostport) == 2 { + host = hostport[0] + n, err := strconv.ParseInt(hostport[1], 10, 32) + if err != nil { + return nil, err + } + port = int(n) + } else { + host, port = hostport[0], 1965 + } + + conn, err := tls.Dial("tcp", host+":"+strconv.Itoa(port), &tls.Config{ + MinVersion: tls.VersionTLS12, + InsecureSkipVerify: true, + }) + if err != nil { + return nil, err + } + + _, err = conn.Write([]byte(u.String() + CRLF)) + if err != nil { + conn.Close() + return nil, err + } + + reader := bufio.NewReader(conn) + + line, _, err := reader.ReadLine() + if err != nil { + conn.Close() + return nil, err + } + + header, err := ParseGeminiHeader(string(line)) + if err != nil { + conn.Close() + return nil, err + } + + return &GeminiResponse{ + Header: header, + Body: reader, + }, nil +} + +func ParseGeminiHeader(line string) (header *GeminiHeader, err error) { + matches := HeaderPattern.FindStringSubmatch(line) + + status, err := strconv.Atoi(matches[1]) + if err != nil { + return nil, err + } + + meta := matches[2] + + if int(status/10) == 2 { + if meta == "" { + meta = DEFAULT_MIME + ";charset=" + DEFAULT_CHARSET + } + + mimeType := MimeTypePattern.FindString(meta) + if strings.HasPrefix(mimeType, "text/") && MimeCharsetPattern.FindString(meta) == "" { + meta += ";charset=" + DEFAULT_CHARSET + } + } + + header = &GeminiHeader{ + Status: status, + Meta: meta, + } + + return +} -- cgit v1.3.1