1 | """ |
---|
2 | Tests for ``allmydata.webish``. |
---|
3 | """ |
---|
4 | |
---|
5 | import tempfile |
---|
6 | from uuid import ( |
---|
7 | uuid4, |
---|
8 | ) |
---|
9 | from io import ( |
---|
10 | BytesIO, |
---|
11 | ) |
---|
12 | |
---|
13 | from hypothesis import ( |
---|
14 | given, |
---|
15 | ) |
---|
16 | from hypothesis.strategies import ( |
---|
17 | integers, |
---|
18 | ) |
---|
19 | |
---|
20 | from testtools.matchers import ( |
---|
21 | AfterPreprocessing, |
---|
22 | Contains, |
---|
23 | Equals, |
---|
24 | MatchesAll, |
---|
25 | Not, |
---|
26 | IsInstance, |
---|
27 | HasLength, |
---|
28 | ) |
---|
29 | |
---|
30 | from twisted.python.filepath import ( |
---|
31 | FilePath, |
---|
32 | ) |
---|
33 | from twisted.web.test.requesthelper import ( |
---|
34 | DummyChannel, |
---|
35 | ) |
---|
36 | from twisted.web.resource import ( |
---|
37 | Resource, |
---|
38 | ) |
---|
39 | |
---|
40 | from ..common import ( |
---|
41 | SyncTestCase, |
---|
42 | ) |
---|
43 | |
---|
44 | from ...webish import ( |
---|
45 | TahoeLAFSRequest, |
---|
46 | TahoeLAFSSite, |
---|
47 | anonymous_tempfile_factory, |
---|
48 | ) |
---|
49 | |
---|
50 | |
---|
51 | class TahoeLAFSRequestTests(SyncTestCase): |
---|
52 | """ |
---|
53 | Tests for ``TahoeLAFSRequest``. |
---|
54 | """ |
---|
55 | def _fields_test(self, method, request_headers, request_body, match_fields): |
---|
56 | channel = DummyChannel() |
---|
57 | request = TahoeLAFSRequest( |
---|
58 | channel, |
---|
59 | ) |
---|
60 | for (k, v) in request_headers.items(): |
---|
61 | request.requestHeaders.setRawHeaders(k, [v]) |
---|
62 | request.gotLength(len(request_body)) |
---|
63 | request.handleContentChunk(request_body) |
---|
64 | request.requestReceived(method, b"/", b"HTTP/1.1") |
---|
65 | |
---|
66 | # We don't really care what happened to the request. What we do care |
---|
67 | # about is what the `fields` attribute is set to. |
---|
68 | self.assertThat( |
---|
69 | request.fields, |
---|
70 | match_fields, |
---|
71 | ) |
---|
72 | |
---|
73 | def test_no_form_fields(self): |
---|
74 | """ |
---|
75 | When a ``GET`` request is received, ``TahoeLAFSRequest.fields`` is None. |
---|
76 | """ |
---|
77 | self._fields_test(b"GET", {}, b"", Equals(None)) |
---|
78 | |
---|
79 | def test_form_fields_if_filename_set(self): |
---|
80 | """ |
---|
81 | When a ``POST`` request is received, form fields are parsed into |
---|
82 | ``TahoeLAFSRequest.fields`` and the body is bytes (presuming ``filename`` |
---|
83 | is set). |
---|
84 | """ |
---|
85 | form_data, boundary = multipart_formdata([ |
---|
86 | [param(u"name", u"foo"), |
---|
87 | body(u"bar"), |
---|
88 | ], |
---|
89 | [param(u"name", u"baz"), |
---|
90 | param(u"filename", u"quux"), |
---|
91 | body(u"some file contents"), |
---|
92 | ], |
---|
93 | ]) |
---|
94 | self._fields_test( |
---|
95 | b"POST", |
---|
96 | {b"content-type": b"multipart/form-data; boundary=" + bytes(boundary, 'ascii')}, |
---|
97 | form_data.encode("ascii"), |
---|
98 | AfterPreprocessing( |
---|
99 | lambda fs: { |
---|
100 | k: fs.getvalue(k) |
---|
101 | for k |
---|
102 | in fs.keys() |
---|
103 | }, |
---|
104 | Equals({ |
---|
105 | "foo": "bar", |
---|
106 | "baz": b"some file contents", |
---|
107 | }), |
---|
108 | ), |
---|
109 | ) |
---|
110 | |
---|
111 | def test_form_fields_if_name_is_file(self): |
---|
112 | """ |
---|
113 | When a ``POST`` request is received, form fields are parsed into |
---|
114 | ``TahoeLAFSRequest.fields`` and the body is bytes when ``name`` |
---|
115 | is set to ``"file"``. |
---|
116 | """ |
---|
117 | form_data, boundary = multipart_formdata([ |
---|
118 | [param(u"name", u"foo"), |
---|
119 | body(u"bar"), |
---|
120 | ], |
---|
121 | [param(u"name", u"file"), |
---|
122 | body(u"some file contents"), |
---|
123 | ], |
---|
124 | ]) |
---|
125 | self._fields_test( |
---|
126 | b"POST", |
---|
127 | {b"content-type": b"multipart/form-data; boundary=" + bytes(boundary, 'ascii')}, |
---|
128 | form_data.encode("ascii"), |
---|
129 | AfterPreprocessing( |
---|
130 | lambda fs: { |
---|
131 | k: fs.getvalue(k) |
---|
132 | for k |
---|
133 | in fs.keys() |
---|
134 | }, |
---|
135 | Equals({ |
---|
136 | "foo": "bar", |
---|
137 | "file": b"some file contents", |
---|
138 | }), |
---|
139 | ), |
---|
140 | ) |
---|
141 | |
---|
142 | def test_form_fields_require_correct_mime_type(self): |
---|
143 | """ |
---|
144 | The body of a ``POST`` is not parsed into fields if its mime type is |
---|
145 | not ``multipart/form-data``. |
---|
146 | |
---|
147 | Reproducer for https://tahoe-lafs.org/trac/tahoe-lafs/ticket/3854 |
---|
148 | """ |
---|
149 | data = u'{"lalala": "lolo"}' |
---|
150 | data = data.encode("utf-8") |
---|
151 | self._fields_test(b"POST", {"content-type": "application/json"}, |
---|
152 | data, Equals(None)) |
---|
153 | |
---|
154 | |
---|
155 | class TahoeLAFSSiteTests(SyncTestCase): |
---|
156 | """ |
---|
157 | Tests for ``TahoeLAFSSite``. |
---|
158 | """ |
---|
159 | def _test_censoring(self, path, censored): |
---|
160 | """ |
---|
161 | Verify that the event logged for a request for ``path`` does not include |
---|
162 | ``path`` but instead includes ``censored``. |
---|
163 | |
---|
164 | :param bytes path: A request path. |
---|
165 | |
---|
166 | :param bytes censored: A replacement value for the request path in the |
---|
167 | access log. |
---|
168 | |
---|
169 | :return: ``None`` if the logging looks good. |
---|
170 | """ |
---|
171 | logPath = self.mktemp() |
---|
172 | tempdir = self.mktemp() |
---|
173 | FilePath(tempdir).makedirs() |
---|
174 | |
---|
175 | site = TahoeLAFSSite( |
---|
176 | anonymous_tempfile_factory(tempdir), |
---|
177 | Resource(), |
---|
178 | logPath=logPath, |
---|
179 | ) |
---|
180 | site.startFactory() |
---|
181 | |
---|
182 | channel = DummyChannel() |
---|
183 | channel.factory = site |
---|
184 | request = TahoeLAFSRequest(channel) |
---|
185 | |
---|
186 | request.gotLength(None) |
---|
187 | request.requestReceived(b"GET", path, b"HTTP/1.1") |
---|
188 | |
---|
189 | self.assertThat( |
---|
190 | FilePath(logPath).getContent(), |
---|
191 | MatchesAll( |
---|
192 | Contains(censored), |
---|
193 | Not(Contains(path)), |
---|
194 | ), |
---|
195 | ) |
---|
196 | |
---|
197 | def test_private_key_censoring(self): |
---|
198 | """ |
---|
199 | The log event for a request including a **private-key** query |
---|
200 | argument has the private key value censored. |
---|
201 | """ |
---|
202 | self._test_censoring( |
---|
203 | b"/uri?uri=URI:CHK:aaa:bbb&private-key=AAAAaaaabbbb==", |
---|
204 | b"/uri?uri=[CENSORED]&private-key=[CENSORED]", |
---|
205 | ) |
---|
206 | |
---|
207 | def test_uri_censoring(self): |
---|
208 | """ |
---|
209 | The log event for a request for **/uri/<CAP>** has the capability value |
---|
210 | censored. |
---|
211 | """ |
---|
212 | self._test_censoring( |
---|
213 | b"/uri/URI:CHK:aaa:bbb", |
---|
214 | b"/uri/[CENSORED]", |
---|
215 | ) |
---|
216 | |
---|
217 | def test_file_censoring(self): |
---|
218 | """ |
---|
219 | The log event for a request for **/file/<CAP>** has the capability value |
---|
220 | censored. |
---|
221 | """ |
---|
222 | self._test_censoring( |
---|
223 | b"/file/URI:CHK:aaa:bbb", |
---|
224 | b"/file/[CENSORED]", |
---|
225 | ) |
---|
226 | |
---|
227 | def test_named_censoring(self): |
---|
228 | """ |
---|
229 | The log event for a request for **/named/<CAP>** has the capability value |
---|
230 | censored. |
---|
231 | """ |
---|
232 | self._test_censoring( |
---|
233 | b"/named/URI:CHK:aaa:bbb", |
---|
234 | b"/named/[CENSORED]", |
---|
235 | ) |
---|
236 | |
---|
237 | def test_uri_queryarg_censoring(self): |
---|
238 | """ |
---|
239 | The log event for a request for **/uri?cap=<CAP>** has the capability |
---|
240 | value censored. |
---|
241 | """ |
---|
242 | self._test_censoring( |
---|
243 | b"/uri?uri=URI:CHK:aaa:bbb", |
---|
244 | b"/uri?uri=[CENSORED]", |
---|
245 | ) |
---|
246 | |
---|
247 | def _create_request(self, tempdir): |
---|
248 | """ |
---|
249 | Create and return a new ``TahoeLAFSRequest`` hooked up to a |
---|
250 | ``TahoeLAFSSite``. |
---|
251 | |
---|
252 | :param FilePath tempdir: The temporary directory to configure the site |
---|
253 | to write large temporary request bodies to. The temporary files |
---|
254 | will be named for ease of testing. |
---|
255 | |
---|
256 | :return TahoeLAFSRequest: The new request instance. |
---|
257 | """ |
---|
258 | site = TahoeLAFSSite( |
---|
259 | lambda: tempfile.NamedTemporaryFile(dir=tempdir.path), |
---|
260 | Resource(), |
---|
261 | logPath=self.mktemp(), |
---|
262 | ) |
---|
263 | site.startFactory() |
---|
264 | |
---|
265 | channel = DummyChannel() |
---|
266 | channel.site = site |
---|
267 | request = TahoeLAFSRequest(channel) |
---|
268 | return request |
---|
269 | |
---|
270 | @given(integers(min_value=0, max_value=1024 * 1024 - 1)) |
---|
271 | def test_small_content(self, request_body_size): |
---|
272 | """ |
---|
273 | A request body smaller than 1 MiB is kept in memory. |
---|
274 | """ |
---|
275 | tempdir = FilePath(self.mktemp()) |
---|
276 | tempdir.makedirs() |
---|
277 | request = self._create_request(tempdir) |
---|
278 | request.gotLength(request_body_size) |
---|
279 | self.assertThat( |
---|
280 | request.content, |
---|
281 | IsInstance(BytesIO), |
---|
282 | ) |
---|
283 | |
---|
284 | def _large_request_test(self, request_body_size): |
---|
285 | """ |
---|
286 | Assert that when a request with a body of the given size is |
---|
287 | received its content is written a temporary file created by the given |
---|
288 | tempfile factory. |
---|
289 | """ |
---|
290 | tempdir = FilePath(self.mktemp()) |
---|
291 | tempdir.makedirs() |
---|
292 | request = self._create_request(tempdir) |
---|
293 | request.gotLength(request_body_size) |
---|
294 | # We can see the temporary file in the temporary directory we |
---|
295 | # specified because _create_request makes a request that uses named |
---|
296 | # temporary files instead of the usual anonymous temporary files. |
---|
297 | self.assertThat( |
---|
298 | tempdir.children(), |
---|
299 | HasLength(1), |
---|
300 | ) |
---|
301 | |
---|
302 | def test_unknown_request_size(self): |
---|
303 | """ |
---|
304 | A request body with an unknown size is written to a file in the temporary |
---|
305 | directory passed to ``TahoeLAFSSite``. |
---|
306 | """ |
---|
307 | self._large_request_test(None) |
---|
308 | |
---|
309 | @given(integers(min_value=1024 * 1024)) |
---|
310 | def test_large_request(self, request_body_size): |
---|
311 | """ |
---|
312 | A request body of 1 MiB or more is written to a file in the temporary |
---|
313 | directory passed to ``TahoeLAFSSite``. |
---|
314 | """ |
---|
315 | self._large_request_test(request_body_size) |
---|
316 | |
---|
317 | |
---|
318 | def param(name, value): |
---|
319 | return u"; {}={}".format(name, value) |
---|
320 | |
---|
321 | |
---|
322 | def body(value): |
---|
323 | return u"\r\n\r\n{}".format(value) |
---|
324 | |
---|
325 | |
---|
326 | def _field(field): |
---|
327 | yield u"Content-Disposition: form-data" |
---|
328 | for param in field: |
---|
329 | yield param |
---|
330 | |
---|
331 | |
---|
332 | def _multipart_formdata(fields): |
---|
333 | for field in fields: |
---|
334 | yield u"".join(_field(field)) + u"\r\n" |
---|
335 | |
---|
336 | |
---|
337 | def multipart_formdata(fields): |
---|
338 | """ |
---|
339 | Serialize some simple fields into a multipart/form-data string. |
---|
340 | |
---|
341 | :param fields: A list of lists of unicode strings to assemble into the |
---|
342 | result. See ``param`` and ``body``. |
---|
343 | |
---|
344 | :return unicode: The given fields combined into a multipart/form-data |
---|
345 | string. |
---|
346 | """ |
---|
347 | boundary = str(uuid4()) |
---|
348 | parts = list(_multipart_formdata(fields)) |
---|
349 | parts.insert(0, u"") |
---|
350 | return ( |
---|
351 | (u"--" + boundary + u"\r\n").join(parts), |
---|
352 | boundary, |
---|
353 | ) |
---|