diff options
Diffstat (limited to 'libgemini.go')
| -rw-r--r-- | libgemini.go | 153 |
1 files changed, 153 insertions, 0 deletions
diff --git a/libgemini.go b/libgemini.go new file mode 100644 index 0000000..05321ef --- /dev/null +++ b/libgemini.go | |||
| @@ -0,0 +1,153 @@ | |||
| 1 | package gopherproxy | ||
| 2 | |||
| 3 | import ( | ||
| 4 | "bufio" | ||
| 5 | "crypto/tls" | ||
| 6 | "errors" | ||
| 7 | "io" | ||
| 8 | "net/url" | ||
| 9 | "regexp" | ||
| 10 | "strconv" | ||
| 11 | "strings" | ||
| 12 | ) | ||
| 13 | |||
| 14 | const ( | ||
| 15 | CRLF = "\r\n" | ||
| 16 | ) | ||
| 17 | |||
| 18 | const ( | ||
| 19 | STATUS_INPUT = 10 | ||
| 20 | STATUS_SUCCESS = 20 | ||
| 21 | STATUS_SUCCESS_CERT = 21 | ||
| 22 | STATUS_REDIRECT_TEMP = 30 | ||
| 23 | STATUS_REDIRECT_PERM = 31 | ||
| 24 | STATUS_TEMP_FAILURE = 40 | ||
| 25 | STATUS_SERVER_UNAVAILABLE = 41 | ||
| 26 | STATUS_CGI_ERROR = 42 | ||
| 27 | STATUS_PROXY_ERROR = 43 | ||
| 28 | STATUS_SLOW_DOWN = 44 | ||
| 29 | STATUS_PERM_FAILURE = 50 | ||
| 30 | STATUS_NOT_FOUND = 51 | ||
| 31 | STATUS_GONE = 52 | ||
| 32 | STATUS_PROXY_REFUSED = 53 | ||
| 33 | STATUS_BAD_REQUEST = 59 | ||
| 34 | STATUS_CLIENT_CERT_EXPIRED = 60 | ||
| 35 | STATUS_TRANSIENT_CERT_REQUEST = 61 | ||
| 36 | STATUS_AUTH_CERT_REQUIRED = 62 | ||
| 37 | STATUS_CERT_REJECTED = 63 | ||
| 38 | STATUS_FUTURE_CERT_REJECTED = 64 | ||
| 39 | STATUS_EXPIRED_CERT_REJECTED = 65 | ||
| 40 | ) | ||
| 41 | |||
| 42 | const ( | ||
| 43 | MIME_GEMINI = "text/gemini" | ||
| 44 | DEFAULT_MIME = MIME_GEMINI | ||
| 45 | DEFAULT_CHARSET = "utf-8" | ||
| 46 | ) | ||
| 47 | |||
| 48 | var ( | ||
| 49 | HeaderPattern = regexp.MustCompile("^(\\d\\d)[ \\t]+(.*)$") | ||
| 50 | MimeTypePattern = regexp.MustCompile("^[-\\w.]+/[-\\w.]+") | ||
| 51 | MimeCharsetPattern = regexp.MustCompile("charset=([^ ;]+)") | ||
| 52 | GeminiLinkPattern = regexp.MustCompile("^=>[ \\t]*([^ ]+)(?:[ \\t]+(.*))?$") | ||
| 53 | ) | ||
| 54 | |||
| 55 | type GeminiHeader struct { | ||
| 56 | Status int | ||
| 57 | Meta string | ||
| 58 | } | ||
| 59 | |||
| 60 | type GeminiResponse struct { | ||
| 61 | Header *GeminiHeader | ||
| 62 | Body io.Reader | ||
| 63 | } | ||
| 64 | |||
| 65 | func GeminiGet(uri string) (*GeminiResponse, error) { | ||
| 66 | u, err := url.Parse(uri) | ||
| 67 | if err != nil { | ||
| 68 | return nil, err | ||
| 69 | } | ||
| 70 | |||
| 71 | if u.Scheme != "gemini" { | ||
| 72 | return nil, errors.New("invalid scheme for uri") | ||
| 73 | } | ||
| 74 | |||
| 75 | var ( | ||
| 76 | host string | ||
| 77 | port int | ||
| 78 | ) | ||
| 79 | |||
| 80 | hostport := strings.Split(u.Host, ":") | ||
| 81 | if len(hostport) == 2 { | ||
| 82 | host = hostport[0] | ||
| 83 | n, err := strconv.ParseInt(hostport[1], 10, 32) | ||
| 84 | if err != nil { | ||
| 85 | return nil, err | ||
| 86 | } | ||
| 87 | port = int(n) | ||
| 88 | } else { | ||
| 89 | host, port = hostport[0], 1965 | ||
| 90 | } | ||
| 91 | |||
| 92 | conn, err := tls.Dial("tcp", host+":"+strconv.Itoa(port), &tls.Config{ | ||
| 93 | MinVersion: tls.VersionTLS12, | ||
| 94 | InsecureSkipVerify: true, | ||
| 95 | }) | ||
| 96 | if err != nil { | ||
| 97 | return nil, err | ||
| 98 | } | ||
| 99 | |||
| 100 | _, err = conn.Write([]byte(u.String() + CRLF)) | ||
| 101 | if err != nil { | ||
| 102 | conn.Close() | ||
| 103 | return nil, err | ||
| 104 | } | ||
| 105 | |||
| 106 | reader := bufio.NewReader(conn) | ||
| 107 | |||
| 108 | line, _, err := reader.ReadLine() | ||
| 109 | if err != nil { | ||
| 110 | conn.Close() | ||
| 111 | return nil, err | ||
| 112 | } | ||
| 113 | |||
| 114 | header, err := ParseGeminiHeader(string(line)) | ||
| 115 | if err != nil { | ||
| 116 | conn.Close() | ||
| 117 | return nil, err | ||
| 118 | } | ||
| 119 | |||
| 120 | return &GeminiResponse{ | ||
| 121 | Header: header, | ||
| 122 | Body: reader, | ||
| 123 | }, nil | ||
| 124 | } | ||
| 125 | |||
| 126 | func ParseGeminiHeader(line string) (header *GeminiHeader, err error) { | ||
| 127 | matches := HeaderPattern.FindStringSubmatch(line) | ||
| 128 | |||
| 129 | status, err := strconv.Atoi(matches[1]) | ||
| 130 | if err != nil { | ||
| 131 | return nil, err | ||
| 132 | } | ||
| 133 | |||
| 134 | meta := matches[2] | ||
| 135 | |||
| 136 | if int(status/10) == 2 { | ||
| 137 | if meta == "" { | ||
| 138 | meta = DEFAULT_MIME + ";charset=" + DEFAULT_CHARSET | ||
| 139 | } | ||
| 140 | |||
| 141 | mimeType := MimeTypePattern.FindString(meta) | ||
| 142 | if strings.HasPrefix(mimeType, "text/") && MimeCharsetPattern.FindString(meta) == "" { | ||
| 143 | meta += ";charset=" + DEFAULT_CHARSET | ||
| 144 | } | ||
| 145 | } | ||
| 146 | |||
| 147 | header = &GeminiHeader{ | ||
| 148 | Status: status, | ||
| 149 | Meta: meta, | ||
| 150 | } | ||
| 151 | |||
| 152 | return | ||
| 153 | } | ||
