Philipp Hörist pushed to branch master at gajim / python-nbxmpp

Commits:

3 changed files:

Changes:

  • nbxmpp/const.py
    ... ... @@ -23,6 +23,7 @@ from packaging.version import Version
    23 23
     
    
    24 24
     from gi.repository import Gio
    
    25 25
     from gi.repository import GLib
    
    26
    +from gi.repository import Soup
    
    26 27
     
    
    27 28
     
    
    28 29
     GLIB_VERSION = Version(
    
    ... ... @@ -706,3 +707,13 @@ NOT_ALLOWED_XML_CHARS = {
    706 707
         '\x0C': '',
    
    707 708
         '\x1B': ''
    
    708 709
     }
    
    710
    +
    
    711
    +
    
    712
    +SOUP_ENCODING: set[Soup.Encoding] = {
    
    713
    +    Soup.Encoding.UNRECOGNIZED,
    
    714
    +    Soup.Encoding.NONE,
    
    715
    +    Soup.Encoding.CONTENT_LENGTH,
    
    716
    +    Soup.Encoding.EOF,
    
    717
    +    Soup.Encoding.CHUNKED,
    
    718
    +    Soup.Encoding.BYTERANGES,
    
    719
    +}

  • nbxmpp/http.py
    ... ... @@ -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()
    

  • nbxmpp/util.py
    ... ... @@ -36,9 +36,11 @@ from functools import lru_cache
    36 36
     from packaging.version import Version
    
    37 37
     
    
    38 38
     from gi.repository import Gio
    
    39
    +from gi.repository import Soup
    
    39 40
     
    
    40 41
     from nbxmpp.protocol import DiscoInfoMalformed
    
    41 42
     from nbxmpp.const import GIO_TLS_ERRORS
    
    43
    +from nbxmpp.const import SOUP_ENCODING
    
    42 44
     from nbxmpp.const import GLIB_VERSION
    
    43 45
     from nbxmpp.namespaces import Namespace
    
    44 46
     from nbxmpp.protocol import StanzaMalformed
    
    ... ... @@ -400,6 +402,10 @@ def convert_tls_error_flags(flags):
    400 402
         return set(filter(lambda error: error & flags, GIO_TLS_ERRORS.keys()))
    
    401 403
     
    
    402 404
     
    
    405
    +def convert_soup_encoding(flags: int) -> set[Soup.Encoding]:
    
    406
    +    return set(filter(lambda enc: enc & flags, SOUP_ENCODING))
    
    407
    +
    
    408
    +
    
    403 409
     def get_websocket_close_string(websocket: Any) -> str:
    
    404 410
         data = websocket.get_close_data()
    
    405 411
         code = websocket.get_close_code()