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]+)(?:[ \\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 }