💾 Archived View for anachronauts.club › cgi-bin › repos › cgi › gmikit.git › tree › trunk › client.g… captured on 2022-01-08 at 14:05:33. Gemini links have been rewritten to link to archived content
⬅️ Previous capture (2021-12-05)
-=-=-=-=-=-=-
1 package gmikit 2 3 import ( 4 "bufio" 5 "context" 6 "crypto/tls" 7 "crypto/x509" 8 "errors" 9 "fmt" 10 "io" 11 "net" 12 "net/url" 13 "strconv" 14 "time" 15 ) 16 17 var ( 18 ErrInvalidURL = errors.New("gemini: invalid URL") 19 ErrInvalidStatus = errors.New("gemini: invalid status") 20 ErrMetaTooLong = errors.New("gemini: meta too long") 21 ErrMalformedHeader = errors.New("gemini: malformed header") 22 ) 23 24 var crlf = []byte("\r\n") 25 26 type Status int 27 28 const ( 29 StatusInput Status = 10 30 StatusSensitiveInput Status = 11 31 StatusSuccess Status = 20 32 StatusRedirect Status = 30 33 StatusPermanentRedirect Status = 31 34 StatusTemporaryFailure Status = 40 35 StatusServerUnavailable Status = 41 36 StatusCGIError Status = 42 37 StatusProxyError Status = 43 38 StatusSlowDown Status = 44 39 StatusPermanentFailure Status = 50 40 StatusNotFound Status = 51 41 StatusGone Status = 52 42 StatusProxyRequestRefused Status = 53 43 StatusBadRequest Status = 59 44 StatusCertificateRequired Status = 60 45 StatusCertificateNotAuthorized Status = 61 46 StatusCertificateNotValid Status = 62 47 ) 48 49 type StatusClass int 50 51 const ( 52 StatusClassInput StatusClass = 1 53 StatusClassSuccess StatusClass = 2 54 StatusClassRedirect StatusClass = 3 55 StatusClassTemporaryFailure StatusClass = 4 56 StatusClassPermanentFailure StatusClass = 5 57 StatusClassCertificateRequired StatusClass = 6 58 ) 59 60 var statusStrings = map[Status]string{ 61 // 1x 62 StatusInput: "INPUT", 63 StatusSensitiveInput: "SENSITIVE INPUT", 64 65 // 2x 66 StatusSuccess: "SUCCESS", 67 68 // 3x 69 StatusRedirect: "REDIRECT - TEMPORARY", 70 StatusPermanentRedirect: "REDIRECT - PERMANENT", 71 72 // 4x 73 StatusTemporaryFailure: "TEMPORARY FAILURE", 74 StatusServerUnavailable: "SERVER UNAVAILABLE", 75 StatusCGIError: "CGI ERROR", 76 StatusProxyError: "PROXY ERROR", 77 StatusSlowDown: "SLOW DOWN", 78 79 // 5x 80 StatusPermanentFailure: "PERMANENT FAILURE", 81 StatusNotFound: "NOT FOUND", 82 StatusGone: "GONE", 83 StatusProxyRequestRefused: "PROXY REQUEST REFUSED", 84 StatusBadRequest: "BAD REQUEST", 85 86 // 6x 87 StatusCertificateRequired: "CLIENT CERTIFICATE REQUIRED", 88 StatusCertificateNotAuthorized: "CERTIFICATE NOT AUTHORIZED", 89 StatusCertificateNotValid: "CERTIFICATE NOT VALID", 90 } 91 92 func (s Status) Class() StatusClass { 93 return StatusClass(s / 10) 94 } 95 96 func (s Status) String() string { 97 str, ok := statusStrings[s] 98 if !ok { 99 str, ok = statusStrings[Status(s.Class()*10)] 100 } 101 if !ok { 102 str = "" 103 } 104 return fmt.Sprintf("%d %s", s, str) 105 } 106 107 type Request struct { 108 URL *url.URL 109 Certificate *tls.Certificate 110 Context context.Context 111 Host string 112 } 113 114 func NewRequest(url *url.URL) *Request { 115 req := &Request{ 116 URL: url, 117 Host: url.Host, 118 } 119 if url.Port() == "" { 120 req.Host = fmt.Sprintf("%s:1965", url.Host) 121 } 122 return req 123 } 124 125 func (r *Request) Write(w *bufio.Writer) error { 126 url := r.URL.String() 127 if r.URL.User != nil || len(url) > 1024 { 128 return ErrInvalidURL 129 } 130 if _, err := w.WriteString(url); err != nil { 131 return err 132 } 133 if _, err := w.Write(crlf); err != nil { 134 return err 135 } 136 return nil 137 } 138 139 type Response struct { 140 Status Status 141 Meta string 142 Body io.Reader 143 TLS tls.ConnectionState 144 closer io.Closer 145 } 146 147 func ReadResponse(rc io.ReadCloser) (*Response, error) { 148 resp := &Response{} 149 br := bufio.NewReader(rc) 150 151 statusB := make([]byte, 2) 152 if _, err := br.Read(statusB); err != nil { 153 return nil, err 154 } 155 status, err := strconv.Atoi(string(statusB)) 156 if err != nil { 157 return nil, err 158 } 159 if status < 10 || status >= 70 { 160 return nil, ErrInvalidStatus 161 } 162 resp.Status = Status(status) 163 164 if b, err := br.ReadByte(); err != nil { 165 return nil, err 166 } else if b != ' ' { 167 return nil, ErrMalformedHeader 168 } 169 170 meta, err := br.ReadString('\r') 171 if err != nil { 172 return nil, err 173 } 174 meta = meta[:len(meta)-1] 175 if len(meta) > 1024 { 176 return nil, ErrMetaTooLong 177 } 178 if resp.Status.Class() == StatusClassSuccess && meta == "" { 179 meta = "text/gemini; charset=utf-8" 180 } 181 resp.Meta = meta 182 183 if b, err := br.ReadByte(); err != nil { 184 return nil, err 185 } else if b != '\n' { 186 return nil, ErrMalformedHeader 187 } 188 189 if resp.Status.Class() != StatusClassSuccess { 190 rc.Close() 191 } else { 192 resp.Body = br 193 resp.closer = rc 194 } 195 196 return resp, nil 197 } 198 199 func (r *Response) Close() error { 200 if r.closer != nil { 201 if err := r.closer.Close(); err != nil { 202 return err 203 } 204 r.closer = nil 205 } 206 return nil 207 } 208 209 type Client struct { 210 TrustCertificate func(hostname string, cert *x509.Certificate) error 211 Timeout time.Duration 212 } 213 214 func (c *Client) Do(req *Request) (*Response, error) { 215 config := &tls.Config{ 216 InsecureSkipVerify: true, 217 MinVersion: tls.VersionTLS12, 218 GetClientCertificate: func(_ *tls.CertificateRequestInfo) (*tls.Certificate, error) { 219 if req.Certificate != nil { 220 return req.Certificate, nil 221 } else { 222 return &tls.Certificate{}, nil 223 } 224 }, 225 VerifyConnection: func(cs tls.ConnectionState) error { 226 if c.TrustCertificate != nil { 227 cert := cs.PeerCertificates[0] 228 return c.TrustCertificate(req.URL.Host, cert) 229 } else { 230 return nil 231 } 232 }, 233 ServerName: req.URL.Host, 234 } 235 236 ctx := req.Context 237 if ctx == nil { 238 ctx = context.Background() 239 } 240 241 start := time.Now() 242 dialer := net.Dialer{ 243 Timeout: c.Timeout, 244 } 245 246 netConn, err := dialer.DialContext(ctx, "tcp", req.Host) 247 if err != nil { 248 return nil, err 249 } 250 251 conn := tls.Client(netConn, config) 252 if c.Timeout != 0 { 253 err := conn.SetDeadline(start.Add(c.Timeout)) 254 if err != nil { 255 return nil, fmt.Errorf("failed to set connection deadline: %w", err) 256 } 257 } 258 259 resp, err := c.do(conn, req) 260 if err != nil { 261 _ = conn.Close() 262 return nil, err 263 } 264 265 resp.TLS = conn.ConnectionState() 266 267 return resp, nil 268 } 269 270 func (c *Client) do(conn *tls.Conn, req *Request) (*Response, error) { 271 w := bufio.NewWriter(conn) 272 err := req.Write(w) 273 if err != nil { 274 return nil, fmt.Errorf("failed to write request: %w", err) 275 } 276 277 if err := w.Flush(); err != nil { 278 return nil, err 279 } 280 281 resp, err := ReadResponse(conn) 282 if err != nil { 283 return nil, err 284 } 285 286 return resp, nil 287 }