gmikit.git

git @ anachronauts.club

summary

tree

log

refs

gmikit.git/client.go | 6 KB

view raw

  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 }