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 | } | ||