| ... |
... |
@@ -18,7 +18,6 @@ from __future__ import annotations |
|
18
|
18
|
from pathlib import Path
|
|
19
|
19
|
|
|
20
|
20
|
from typing import Any
|
|
21
|
|
-from typing import cast
|
|
22
|
21
|
from typing import Literal
|
|
23
|
22
|
from typing import Callable
|
|
24
|
23
|
from typing import Optional
|
| ... |
... |
@@ -31,6 +30,7 @@ from gi.repository import GLib |
|
31
|
30
|
from gi.repository import GObject
|
|
32
|
31
|
|
|
33
|
32
|
import nbxmpp
|
|
|
33
|
+from .util import convert_soup_encoding
|
|
34
|
34
|
from .const import HTTPRequestError
|
|
35
|
35
|
|
|
36
|
36
|
|
| ... |
... |
@@ -59,16 +59,11 @@ class HTTPLogAdapter(logging.LoggerAdapter): |
|
59
|
59
|
|
|
60
|
60
|
|
|
61
|
61
|
class HTTPSession:
|
|
62
|
|
- def __init__(self,
|
|
63
|
|
- user_agent: str = DEFAULT_USER_AGENT,
|
|
64
|
|
- sniffer: bool = False
|
|
65
|
|
- ) -> None:
|
|
|
62
|
+ def __init__(self, user_agent: str = DEFAULT_USER_AGENT) -> None:
|
|
66
|
63
|
|
|
67
|
64
|
self._session = Soup.Session()
|
|
68
|
65
|
self._session.set_user_agent(user_agent)
|
|
69
|
|
-
|
|
70
|
|
- if sniffer:
|
|
71
|
|
- self._session.add_feature_by_type(Soup.ContentSniffer)
|
|
|
66
|
+ self._session.add_feature_by_type(Soup.ContentSniffer)
|
|
72
|
67
|
|
|
73
|
68
|
def get_soup_session(self) -> Soup.Session:
|
|
74
|
69
|
return self._session
|
| ... |
... |
@@ -88,6 +83,7 @@ class HTTPRequest(GObject.GObject): |
|
88
|
83
|
__gtype_name__ = "HTTPRequest"
|
|
89
|
84
|
|
|
90
|
85
|
__gsignals__ = {
|
|
|
86
|
+ 'content-sniffed': (SIGNAL_ACTIONS, None, (int, str)),
|
|
91
|
87
|
'starting-response-body': (SIGNAL_ACTIONS, None, ()),
|
|
92
|
88
|
'response-progress': (SIGNAL_ACTIONS, None, (float,)),
|
|
93
|
89
|
'request-progress': (SIGNAL_ACTIONS, None, (float,)),
|
| ... |
... |
@@ -106,16 +102,18 @@ class HTTPRequest(GObject.GObject): |
|
106
|
102
|
self._sent_size = 0
|
|
107
|
103
|
|
|
108
|
104
|
self._cancellable = Gio.Cancellable()
|
|
109
|
|
- self._input_stream = cast(Gio.InputStream, None)
|
|
|
105
|
+ self._input_stream: Optional[Gio.InputStream] = None
|
|
110
|
106
|
self._output_stream: Optional[Gio.OutputStream] = None
|
|
111
|
107
|
self._is_finished = False
|
|
112
|
108
|
self._error: Optional[HTTPRequestError] = None
|
|
113
|
109
|
self._is_complete = False
|
|
114
|
110
|
self._timeout_reached = False
|
|
115
|
111
|
self._timeout_id = None
|
|
|
112
|
+ self._no_content_length_set = False
|
|
116
|
113
|
|
|
117
|
114
|
self._response_body_file: Optional[Gio.File] = None
|
|
118
|
115
|
self._response_body_data = b''
|
|
|
116
|
+ self._body_received = False
|
|
119
|
117
|
|
|
120
|
118
|
self._request_body_file: Optional[Gio.File] = None
|
|
121
|
119
|
self._request_body_data: Optional[bytes] = None
|
| ... |
... |
@@ -146,7 +144,7 @@ class HTTPRequest(GObject.GObject): |
|
146
|
144
|
def get_error_string(self) -> str:
|
|
147
|
145
|
if self._error == HTTPRequestError.STATUS_NOT_OK:
|
|
148
|
146
|
return self._message.get_reason_phrase()
|
|
149
|
|
- return f'{self._error}'
|
|
|
147
|
+ return repr(self._error)
|
|
150
|
148
|
|
|
151
|
149
|
def get_response_headers(self) -> Soup.MessageHeaders:
|
|
152
|
150
|
return self._message.get_response_headers()
|
| ... |
... |
@@ -178,6 +176,7 @@ class HTTPRequest(GObject.GObject): |
|
178
|
176
|
if self._is_finished:
|
|
179
|
177
|
raise ValueError('Session already finished')
|
|
180
|
178
|
|
|
|
179
|
+ self._log.info('Cancel requested')
|
|
181
|
180
|
self._cancellable.cancel()
|
|
182
|
181
|
|
|
183
|
182
|
def set_request_body_from_path(self, content_type: str, path: Path) -> None:
|
| ... |
... |
@@ -277,6 +276,8 @@ class HTTPRequest(GObject.GObject): |
|
277
|
276
|
self._message.connect('wrote-body-data',
|
|
278
|
277
|
self._on_request_body_progress)
|
|
279
|
278
|
|
|
|
279
|
+ self._message.connect('content-sniffed', self._on_content_sniffed)
|
|
|
280
|
+ self._message.connect('got-body', self._on_got_body)
|
|
280
|
281
|
self._message.connect('finished', self._on_finished)
|
|
281
|
282
|
|
|
282
|
283
|
soup_session = self._session.get_soup_session()
|
| ... |
... |
@@ -312,23 +313,21 @@ class HTTPRequest(GObject.GObject): |
|
312
|
313
|
try:
|
|
313
|
314
|
self._input_stream = session.send_finish(result)
|
|
314
|
315
|
except GLib.Error as error:
|
|
315
|
|
- self._log.error(error)
|
|
316
|
316
|
quark = GLib.quark_try_string('g-io-error-quark')
|
|
317
|
317
|
if error.matches(quark, Gio.IOErrorEnum.CANCELLED):
|
|
318
|
|
- self._set_failed(HTTPRequestError.CANCELLED)
|
|
319
|
|
- else:
|
|
320
|
|
- self._set_failed(HTTPRequestError.UNKNOWN)
|
|
321
|
|
- return
|
|
|
318
|
+ if self._no_content_length_set:
|
|
|
319
|
+ self._set_failed(HTTPRequestError.MISSING_CONTENT_LENGTH)
|
|
|
320
|
+ else:
|
|
|
321
|
+ self._set_failed(HTTPRequestError.CANCELLED)
|
|
|
322
|
+ return
|
|
322
|
323
|
|
|
323
|
|
- if self._message.get_status() not in (Soup.Status.OK, Soup.Status.CREATED):
|
|
324
|
|
- self._set_failed(HTTPRequestError.STATUS_NOT_OK)
|
|
|
324
|
+ self._log.error(error)
|
|
|
325
|
+ self._set_failed(HTTPRequestError.UNKNOWN)
|
|
325
|
326
|
return
|
|
326
|
327
|
|
|
327
|
|
- headers = self.get_response_headers()
|
|
328
|
|
- self._response_content_length = headers.get_content_length()
|
|
329
|
|
- self._response_content_type, _params = headers.get_content_type()
|
|
330
|
|
- if self._response_content_length == 0:
|
|
331
|
|
- self._set_failed(HTTPRequestError.MISSING_CONTENT_LENGTH)
|
|
|
328
|
+ if self._message.get_status() not in (Soup.Status.OK,
|
|
|
329
|
+ Soup.Status.CREATED):
|
|
|
330
|
+ self._set_failed(HTTPRequestError.STATUS_NOT_OK)
|
|
332
|
331
|
return
|
|
333
|
332
|
|
|
334
|
333
|
self._log.info('Start downloading response body')
|
| ... |
... |
@@ -337,6 +336,7 @@ class HTTPRequest(GObject.GObject): |
|
337
|
336
|
self._read_async()
|
|
338
|
337
|
|
|
339
|
338
|
def _read_async(self) -> None:
|
|
|
339
|
+ assert self._input_stream is not None
|
|
340
|
340
|
self._input_stream.read_bytes_async(CHUNK_SIZE,
|
|
341
|
341
|
GLib.PRIORITY_LOW,
|
|
342
|
342
|
self._cancellable,
|
| ... |
... |
@@ -399,26 +399,54 @@ class HTTPRequest(GObject.GObject): |
|
399
|
399
|
|
|
400
|
400
|
self._set_failed(error)
|
|
401
|
401
|
|
|
402
|
|
- def _on_finished(self, _message: Soup.Message) -> None:
|
|
403
|
|
- self._set_finished()
|
|
|
402
|
+ def _on_content_sniffed(self,
|
|
|
403
|
+ message: Soup.Message,
|
|
|
404
|
+ content_type: str,
|
|
|
405
|
+ _params: GLib.HashTable,
|
|
|
406
|
+ ) -> None:
|
|
|
407
|
+
|
|
|
408
|
+ headers = message.get_response_headers()
|
|
|
409
|
+ encoding = headers.get_encoding()
|
|
|
410
|
+ if Soup.Encoding.CONTENT_LENGTH not in convert_soup_encoding(encoding):
|
|
|
411
|
+ self._log.warning('No content-length in response')
|
|
|
412
|
+ self._no_content_length_set = True
|
|
|
413
|
+ self.cancel()
|
|
|
414
|
+ return
|
|
404
|
415
|
|
|
405
|
|
- def _set_finished(self) -> None:
|
|
406
|
|
- status = self._message.get_status()
|
|
407
|
|
- if status == Soup.Status.NONE:
|
|
408
|
|
- # Message has not been sent, can happen when we cancel the message
|
|
409
|
|
- # before it is sent. The finished signal triggers before the
|
|
410
|
|
- # cancelled exception.
|
|
|
416
|
+ self._response_content_length = headers.get_content_length()
|
|
|
417
|
+ if self._response_content_length == 0:
|
|
|
418
|
+ self._log.warning('No content-length in response')
|
|
|
419
|
+ self._no_content_length_set = True
|
|
|
420
|
+ self.cancel()
|
|
411
|
421
|
return
|
|
412
|
422
|
|
|
413
|
|
- if self._is_finished:
|
|
|
423
|
+ self._response_content_type = content_type
|
|
|
424
|
+ self.emit('content-sniffed',
|
|
|
425
|
+ self._response_content_length,
|
|
|
426
|
+ self._response_content_type)
|
|
|
427
|
+
|
|
|
428
|
+ def _on_got_body(self, _message: Soup.Message) -> None:
|
|
|
429
|
+ # This signal tells us that the full body was received.
|
|
|
430
|
+ # The `finished` signal is not a sure indicator if the message body
|
|
|
431
|
+ # was received in full, as its also triggered when a message is
|
|
|
432
|
+ # cancelled.
|
|
|
433
|
+ self._log.info('Body received')
|
|
|
434
|
+ self._body_received = True
|
|
|
435
|
+
|
|
|
436
|
+ def _on_finished(self, _message: Soup.Message) -> None:
|
|
|
437
|
+ self._log.info('Message finished')
|
|
|
438
|
+ if not self._body_received:
|
|
|
439
|
+ # This can happen when the message is cancelled. The `finished`
|
|
|
440
|
+ # signal is raised whenever the input stream is closed.
|
|
|
441
|
+ # In the case the message was cancelled other parts of the code
|
|
|
442
|
+ # will call set_failed().
|
|
414
|
443
|
return
|
|
415
|
444
|
|
|
416
|
|
- headers = self._message.get_response_headers()
|
|
417
|
|
- content_length = headers.get_content_length()
|
|
418
|
|
- if self._received_size != content_length:
|
|
|
445
|
+ if self._received_size != self._response_content_length:
|
|
419
|
446
|
self._set_failed(HTTPRequestError.INCOMPLETE)
|
|
420
|
447
|
return
|
|
421
|
448
|
|
|
|
449
|
+ status = self._message.get_status()
|
|
422
|
450
|
if status not in (Soup.Status.OK, Soup.Status.CREATED):
|
|
423
|
451
|
self._set_failed(HTTPRequestError.STATUS_NOT_OK)
|
|
424
|
452
|
return
|
| ... |
... |
@@ -426,6 +454,7 @@ class HTTPRequest(GObject.GObject): |
|
426
|
454
|
self._set_complete()
|
|
427
|
455
|
|
|
428
|
456
|
def _set_failed(self, error: HTTPRequestError) -> None:
|
|
|
457
|
+ self._log.info('Set Failed: %s', error)
|
|
429
|
458
|
self._is_finished = True
|
|
430
|
459
|
if self._timeout_reached:
|
|
431
|
460
|
self._timeout_id = None
|
| ... |
... |
@@ -437,6 +466,7 @@ class HTTPRequest(GObject.GObject): |
|
437
|
466
|
self._cleanup()
|
|
438
|
467
|
|
|
439
|
468
|
def _set_complete(self) -> None:
|
|
|
469
|
+ self._log.info('Set Complete')
|
|
440
|
470
|
self._is_finished = True
|
|
441
|
471
|
self._is_complete = True
|
|
442
|
472
|
self._close_all_streams()
|
| ... |
... |
@@ -460,12 +490,14 @@ class HTTPRequest(GObject.GObject): |
|
460
|
490
|
|
|
461
|
491
|
del self._cancellable
|
|
462
|
492
|
del self._session
|
|
463
|
|
- del self._input_stream
|
|
464
|
|
- del self._output_stream
|
|
465
|
493
|
del self._user_data
|
|
466
|
494
|
|
|
|
495
|
+ self._input_stream = None
|
|
|
496
|
+ self._output_stream = None
|
|
|
497
|
+
|
|
467
|
498
|
if self._timeout_id is not None:
|
|
468
|
499
|
GLib.source_remove(self._timeout_id)
|
|
|
500
|
+ self._timeout_id = None
|
|
469
|
501
|
|
|
470
|
502
|
self.emit('destroy')
|
|
471
|
503
|
self.run_dispose()
|