Halt pollLoop when PollingPacketConn is closed.
[champa.git] / champa-client / amp.go
blobe31539f21d668eca5cd02faad55702e7c7bf7e7c
1 package main
3 import (
4 "bufio"
5 "context"
6 "crypto/rand"
7 "encoding/base64"
8 "fmt"
9 "io"
10 "net"
11 "net/http"
12 "net/url"
13 "strings"
15 "www.bamsoftware.com/git/champa.git/amp"
16 "www.bamsoftware.com/git/champa.git/armor"
19 // cacheBreaker returns a random byte slice of fixed length.
20 func cacheBreaker() []byte {
21 buf := make([]byte, 12)
22 _, err := rand.Read(buf)
23 if err != nil {
24 panic(err)
26 return buf
29 func exchangeAMP(ctx context.Context, serverURL, cacheURL *url.URL, front string, p []byte) (io.ReadCloser, error) {
30 // Append a cache buster and the encoded p to the path of serverURL.
31 u := serverURL.ResolveReference(&url.URL{
32 // Use strings.Join, rather than path.Join, in order to retain a
33 // closing slash when p is empty.
34 Path: strings.Join([]string{
35 // "0" is the client–server protocol version indicator.
36 "0" + base64.RawURLEncoding.EncodeToString(cacheBreaker()),
37 base64.RawURLEncoding.EncodeToString(p),
38 }, "/"),
41 // Proxy through an AMP cache, if requested.
42 if cacheURL != nil {
43 var err error
44 u, err = amp.CacheURL(u, cacheURL, "c")
45 if err != nil {
46 return nil, err
50 req, err := http.NewRequest("GET", u.String(), nil)
51 if err != nil {
52 return nil, err
54 req = req.WithContext(ctx)
56 // Do domain fronting, if requested.
57 if front != "" {
58 _, port, err := net.SplitHostPort(req.URL.Host)
59 if err == nil {
60 req.URL.Host = net.JoinHostPort(front, port)
61 } else {
62 req.URL.Host = front
66 req.Header.Set("User-Agent", "") // Disable default "Go-http-client/1.1".
68 resp, err := http.DefaultTransport.RoundTrip(req)
69 if err != nil {
70 return nil, err
72 if resp.StatusCode != http.StatusOK {
73 resp.Body.Close()
74 return nil, fmt.Errorf("server returned status %v", resp.Status)
76 if _, err := resp.Location(); err == nil {
77 // The Google AMP Cache can return a "silent redirect" with
78 // status 200, a Location header set, and a JavaScript redirect
79 // in the body. The redirect points directly at the origin
80 // server for the request (bypassing the AMP cache). We do not
81 // follow redirects nor execute JavaScript, but in any case we
82 // cannot extract information from this response and it's better
83 // to treat it as a poll error, rather than an EOF when given to
84 // the AMP armor decoder.
86 // Such a response looks like this (header slightly excerpted):
88 // HTTP/2 200 OK
89 // Cache-Control: private
90 // Content-Type: text/html; charset=UTF-8
91 // Location: https://example.com/champa/...
92 // Server: sffe
93 // X-Silent-Redirect: true
95 // <HTML><HEAD>
96 // <meta http-equiv="content-type" content="text/html;charset=utf-8">
97 // <TITLE>Redirecting</TITLE>
98 // <META HTTP-EQUIV="refresh" content="0; url=https://example.com/champa/...">
99 // </HEAD>
100 // <BODY onLoad="location.replace('https://example.com/champa/...'+document.location.hash)">
101 // </BODY></HTML>
102 resp.Body.Close()
103 return nil, fmt.Errorf("server returned a Location header")
106 dec, err := armor.NewDecoder(bufio.NewReader(resp.Body))
107 if err != nil {
108 resp.Body.Close()
109 return nil, err
112 // The caller should read from the decoder (which reads from the
113 // response body), but close the actual response body when done.
114 return &struct {
115 io.Reader
116 io.Closer
118 Reader: dec,
119 Closer: resp.Body,
120 }, nil