Limit the size of server response bodies.
[champa.git] / armor / encoder.go
blob24ada9adc1c3b95cf328cab610b5f7a00598cf7f
1 package armor
3 import (
4 "encoding/base64"
5 "io"
8 // https://amp.dev/boilerplate/
9 // https://amp.dev/documentation/guides-and-tutorials/learn/spec/amp-boilerplate/?format=websites
10 // https://amp.dev/documentation/guides-and-tutorials/learn/spec/amphtml/?format=websites#the-amp-html-format
11 const (
12 boilerplateStart = `<!doctype html>
13 <html amp>
14 <head>
15 <meta charset="utf-8">
16 <script async src="https://cdn.ampproject.org/v0.js"></script>
17 <link rel="canonical" href="#">
18 <meta name="viewport" content="width=device-width">
19 <style amp-boilerplate>body{-webkit-animation:-amp-start 8s steps(1,end) 0s 1 normal both;-moz-animation:-amp-start 8s steps(1,end) 0s 1 normal both;-ms-animation:-amp-start 8s steps(1,end) 0s 1 normal both;animation:-amp-start 8s steps(1,end) 0s 1 normal both}@-webkit-keyframes -amp-start{from{visibility:hidden}to{visibility:visible}}@-moz-keyframes -amp-start{from{visibility:hidden}to{visibility:visible}}@-ms-keyframes -amp-start{from{visibility:hidden}to{visibility:visible}}@-o-keyframes -amp-start{from{visibility:hidden}to{visibility:visible}}@keyframes -amp-start{from{visibility:hidden}to{visibility:visible}}</style><noscript><style amp-boilerplate>body{-webkit-animation:none;-moz-animation:none;-ms-animation:none;animation:none}</style></noscript>
20 </head>
21 <body>
23 boilerplateEnd = `</body>
24 </html>`
27 const (
28 // We restrict the amount of text may go inside an HTML element, in
29 // order to limit the amount a decoder may have to buffer.
30 elementSizeLimit = 32 * 1024
32 // The payload is conceptually a long base64-encoded string, but we
33 // break the string into short chunks separated by whitespace. This is
34 // to protect against modification by AMP caches, which reportedly may
35 // truncate long words in text:
36 // https://bugs.torproject.org/tpo/anti-censorship/pluggable-transports/snowflake/25985#note_2592348
37 bytesPerChunk = 32
39 // We set the number of chunks per element so as to stay under
40 // elementSizeLimit. Here, we assume that there is 1 byte of whitespace
41 // after each chunk (with an additional whitespace byte at the beginning
42 // of the element).
43 chunksPerElement = (elementSizeLimit - 1) / (bytesPerChunk + 1)
46 // The AMP armor encoder is a chain of a base64 encoder (base64.NewEncoder) and
47 // an HTML element encoder (elementEncoder). A top-level encoder (armorEncoder)
48 // coordinates these two, and handles prepending and appending the AMP
49 // boilerplate. armorEncoder's Write method writes data into the base64 encoder,
50 // where it makes its way through the chain.
52 // NewEncoder returns a new AMP armor encoder. Anything written to the returned
53 // io.WriteCloser will be encoded and written to w. The caller must call Close
54 // to flush any partially written data and output the AMP boilerplate trailer.
55 func NewEncoder(w io.Writer) (io.WriteCloser, error) {
56 // Immediately write the AMP boilerplate header.
57 _, err := w.Write([]byte(boilerplateStart))
58 if err != nil {
59 return nil, err
62 element := &elementEncoder{w: w}
63 // Write a server–client protocol version indicator, outside the base64
64 // layer.
65 _, err = element.Write([]byte("0"))
66 if err != nil {
67 return nil, err
70 base64 := base64.NewEncoder(base64.StdEncoding, element)
71 return &armorEncoder{
72 w: w,
73 element: element,
74 base64: base64,
75 }, nil
78 type armorEncoder struct {
79 base64 io.WriteCloser
80 element *elementEncoder
81 w io.Writer
84 func (enc *armorEncoder) Write(p []byte) (int, error) {
85 // Write into the chain base64 | element | w.
86 return enc.base64.Write(p)
89 func (enc *armorEncoder) Close() error {
90 // Close the base64 encoder first, to flush out any buffered data and
91 // the final padding.
92 err := enc.base64.Close()
93 if err != nil {
94 return err
97 // Next, close the element encoder, to close any open elements.
98 err = enc.element.Close()
99 if err != nil {
100 return err
103 // Finally, output the AMP boilerplate trailer.
104 _, err = enc.w.Write([]byte(boilerplateEnd))
105 if err != nil {
106 return err
109 return nil
112 // elementEncoder arranges written data into pre elements, with the text within
113 // separated into chunks. It does no HTML encoding, so data written must not
114 // contain any bytes that are meaningful in HTML.
115 type elementEncoder struct {
116 w io.Writer
117 chunkCounter int
118 elementCounter int
121 func (enc *elementEncoder) Write(p []byte) (n int, err error) {
122 total := 0
123 for len(p) > 0 {
124 if enc.elementCounter == 0 && enc.chunkCounter == 0 {
125 _, err := enc.w.Write([]byte("<pre>\n"))
126 if err != nil {
127 return total, err
131 n := bytesPerChunk - enc.chunkCounter
132 if n > len(p) {
133 n = len(p)
135 nn, err := enc.w.Write(p[:n])
136 if err != nil {
137 return total, err
139 total += nn
140 p = p[n:]
142 enc.chunkCounter += n
143 if enc.chunkCounter >= bytesPerChunk {
144 enc.chunkCounter = 0
145 enc.elementCounter += 1
146 nn, err = enc.w.Write([]byte("\n"))
147 if err != nil {
148 return total, err
150 total += nn
153 if enc.elementCounter >= chunksPerElement {
154 enc.elementCounter = 0
155 nn, err = enc.w.Write([]byte("</pre>\n"))
156 if err != nil {
157 return total, err
159 total += nn
162 return total, nil
165 func (enc *elementEncoder) Close() error {
166 var err error
167 if !(enc.elementCounter == 0 && enc.chunkCounter == 0) {
168 if enc.chunkCounter == 0 {
169 _, err = enc.w.Write([]byte("</pre>\n"))
170 } else {
171 _, err = enc.w.Write([]byte("\n</pre>\n"))
174 _, err2 := enc.w.Write([]byte(boilerplateEnd))
175 if err == nil {
176 err = err2
178 return err