• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

cle-b / httpdbg / 17174092107

23 Aug 2025 09:52AM UTC coverage: 87.753% (-0.2%) from 87.916%
17174092107

Pull #204

github

cle-b
mark as not a http request
Pull Request #204: fixes

14 of 16 new or added lines in 5 files covered. (87.5%)

4 existing lines in 2 files now uncovered.

2164 of 2466 relevant lines covered (87.75%)

0.88 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

80.05
/httpdbg/hooks/socket.py
1
# -*- coding: utf-8 -*-
2
import asyncio
1✔
3
import asyncio.proactor_events
1✔
4
from collections.abc import Callable
1✔
5
from contextlib import contextmanager
1✔
6
import datetime
1✔
7
import platform
1✔
8
import socket
1✔
9
import ssl
1✔
10
import sys
1✔
11
import traceback
1✔
12
from typing import Generator
1✔
13
from typing import Union
1✔
14

15
from httpdbg.initiator import httpdbg_initiator
1✔
16
from httpdbg.log import logger
1✔
17
from httpdbg.hooks.recordhttp1 import HTTP1Record
1✔
18
from httpdbg.hooks.utils import getcallargs
1✔
19
from httpdbg.hooks.utils import decorate
1✔
20
from httpdbg.hooks.utils import undecorate
1✔
21

22
from httpdbg.records import HTTPRecords
1✔
23

24

25
class SocketRawData(object):
1✔
26
    """Store the request data without encryption, even when using an SSLSocket."""
27

28
    def __init__(self, id: int, address: tuple[str, int], ssl: bool) -> None:
1✔
29
        self.id: int = id
1✔
30
        self.address: tuple[str, int] = address
1✔
31
        self.ssl: bool = ssl
1✔
32
        self._rawdata: bytes = bytes()
1✔
33
        self.record: Union[HTTP1Record, None] = None
1✔
34
        self.tbegin: datetime.datetime = datetime.datetime.now(datetime.timezone.utc)
1✔
35

36
    @property
1✔
37
    def rawdata(self) -> bytes:
1✔
38
        return self._rawdata
1✔
39

40
    @rawdata.setter
1✔
41
    def rawdata(self, value: bytes) -> None:
1✔
42
        logger().info(
1✔
43
            f"SocketRawData id={self.id} newdata={value[:20]!r} len={len(value)}"
44
        )
45
        self._rawdata = value
1✔
46

47
    def http_detected(self) -> Union[bool, None]:
1✔
48
        end_of_first_line = self.rawdata[:2048].find(b"\r\n")
1✔
49
        if end_of_first_line == -1:
1✔
50
            if len(self.rawdata) > 2048:
1✔
51
                return False
1✔
52
            else:
53
                return None
1✔
54
        firstline = self.rawdata[:end_of_first_line]
1✔
55
        if firstline.upper().endswith(b"HTTP/1.1"):
1✔
56
            return True
1✔
57
        if firstline.upper().endswith(b"HTTP/1.0"):
1✔
58
            return True
×
59
        return False
1✔
60

61
    def __repr__(self) -> str:
1✔
62
        return f"SocketRawData id={self.id} {self.address}"
1✔
63

64

65
class TracerHTTP1:
1✔
66

67
    def __init__(
1✔
68
        self,
69
        ignore: tuple[tuple[str, int], ...] = (),
70
    ):
71
        self.sockets: dict[int, Union[SocketRawData, None]] = (
1✔
72
            {}
73
        )  # if None, this is not a HTTP/1 request
74
        self.ignore: tuple[tuple[str, int], ...] = ignore
1✔
75

76
    def get_socket_data(
1✔
77
        self, obj, extra_sock=None, force_new=False, request=None, is_uvicorn=False
78
    ) -> Union[SocketRawData, None]:
79
        """Record a new SocketRawData (or get an existing one) and return it."""
80
        socketdata = None
1✔
81

82
        if force_new:
1✔
83
            self.del_socket_data(obj)
1✔
84

85
        if id(obj) in self.sockets:
1✔
86
            socketdata = self.sockets[id(obj)]
1✔
87
            if (
1✔
88
                request
89
                and socketdata
90
                and socketdata.record
91
                and socketdata.record.is_client
92
                and socketdata.record.response.rawdata
93
            ) or (
94
                (not request)
95
                and socketdata
96
                and socketdata.record
97
                and (not socketdata.record.is_client)
98
                and socketdata.record.request.rawdata
99
            ):
100
                # the socket is reused for a new request
101
                self.sockets[id(obj)] = SocketRawData(
1✔
102
                    id(obj), socketdata.address, socketdata.ssl
103
                )
104
                socketdata = self.sockets[id(obj)]
1✔
105
        else:
106
            if isinstance(obj, socket.socket):
1✔
107
                try:
1✔
108
                    address = obj.getsockname()
1✔
109
                    if address not in self.ignore:
1✔
110
                        self.sockets[id(obj)] = SocketRawData(
1✔
111
                            id(obj), address, isinstance(obj, ssl.SSLSocket)
112
                        )
113
                        socketdata = self.sockets[id(obj)]
1✔
114
                except OSError:
×
115
                    # OSError: [WinError 10022] An invalid argument was supplied
116
                    pass
×
117
            elif isinstance(obj, asyncio.proactor_events._ProactorSocketTransport):
1✔
118
                # only for async HTTP requests (not HTTPS) on Windows
119
                self.sockets[id(obj)] = SocketRawData(id(obj), ("", 0), False)
×
120
                socketdata = self.sockets[id(obj)]
×
121
            elif is_uvicorn:
1✔
122
                self.sockets[id(obj)] = SocketRawData(id(obj), ("", 0), False)
1✔
123
                socketdata = self.sockets[id(obj)]
1✔
124
            else:
125
                if extra_sock:
1✔
126
                    try:
1✔
127
                        address = (
1✔
128
                            extra_sock.getsockname()
129
                            if hasattr(extra_sock, "getsockname")
130
                            else ("", 0)  # wrap_bio
131
                        )
132
                        if address not in self.ignore:
1✔
133
                            self.sockets[id(obj)] = SocketRawData(
1✔
134
                                id(obj),
135
                                address,
136
                                isinstance(obj, (ssl.SSLObject, ssl.SSLSocket)),
137
                            )
138
                            socketdata = self.sockets[id(obj)]
1✔
139
                    except OSError:
×
140
                        # OSError: [WinError 10022] An invalid argument was supplied
141
                        pass
×
142

143
        return socketdata
1✔
144

145
    def move_socket_data(self, dest, ori):
1✔
146
        if id(ori) in self.sockets:
1✔
147
            socketdata = self.get_socket_data(ori)
1✔
148
            if socketdata:
1✔
149
                self.sockets[id(dest)] = socketdata
1✔
150
                if isinstance(dest, (ssl.SSLSocket, ssl.SSLObject)):
1✔
151
                    socketdata.ssl = True
1✔
152
                self.del_socket_data(ori)
1✔
153

154
    def del_socket_data(self, obj):
1✔
155
        if id(obj) in self.sockets:
1✔
156
            logger().info(f"SocketRawData del id={id(obj)}")
1✔
157
            self.sockets.pop(id(obj))
1✔
158

159
    def mark_as_not_a_http_request(self, obj):
1✔
160
        if id(obj) in self.sockets:
1✔
161
            logger().info(f"Socket not HTTP request id={id(obj)}")
1✔
162
            self.sockets[id(obj)] = None
1✔
163

164

165
# hook: socket.socket.__init__
166
# what: A new socket object is created.
167
# action: If an entry exists in the temporary raw socket storage list, it is removed.
168
def set_hook_for_socket_init(records: HTTPRecords, method: Callable):
1✔
169
    def hook(self, *args, **kwargs):
1✔
170
        records._tracerhttp1.del_socket_data(self)
1✔
171

172
        return method(self, *args, **kwargs)
1✔
173

174
    return hook
1✔
175

176

177
# hook: socket.socket.connect, ssl.SSLSocket.connect
178
# what: A connection to a remote socket is initiated.
179
# action: A new entry is added to the temporary raw socket storage list.
180
def set_hook_for_socket_connect(records: HTTPRecords, method: Callable):
1✔
181
    def hook(self, *args, **kwargs):
1✔
182
        tbegin: datetime.datetime = datetime.datetime.now(datetime.timezone.utc)
1✔
183
        socketdata = records._tracerhttp1.get_socket_data(self, force_new=True)
1✔
184
        if socketdata:
1✔
185
            logger().info(
1✔
186
                f"CONNECT - self={self} id={id(self)} socketdata={socketdata} args={args} kwargs={kwargs}"
187
            )
188
        try:
1✔
189
            r = method(self, *args, **kwargs)
1✔
190
        except Exception as ex:
1✔
191
            if not isinstance(
1✔
192
                ex, (BlockingIOError, OSError)
193
            ):  # BlockingIOError for async, OSError for ipv6
194
                if records.client:
×
195
                    with httpdbg_initiator(
×
196
                        records, traceback.extract_stack(), method, *args, **kwargs
197
                    ) as initiator_and_group:
198
                        initiator, group, is_new = initiator_and_group
×
199
                        if is_new:
×
200
                            initiator.tbegin = tbegin
×
201
                            group.tbegin = tbegin
×
202
                            records.add_new_record_exception(
×
203
                                initiator, group, "http:///", ex
204
                            )
205
            raise
1✔
206

207
        return r
1✔
208

209
    return hook
1✔
210

211

212
# hook: ssl.wrap_socket,
213
# what: Takes an instance sock of socket.socket, and returns an instance of ssl.SSLSocket.
214
# a subtype of socket.socket, which wraps the underlying socket in an SSL context.
215
# action: Link the socket and the sslsocket
216
def set_hook_for_ssl_wrap_socket(records: HTTPRecords, method: Callable):
1✔
217
    def hook(sock, *args, **kwargs):
1✔
218
        tbegin: datetime.datetime = datetime.datetime.now(datetime.timezone.utc)
×
219
        try:
×
220
            sslsocket = method(sock, *args, **kwargs)
×
221
        except Exception as ex:
×
222
            if records.client:
×
223
                with httpdbg_initiator(
×
224
                    records, traceback.extract_stack(), method, *args, **kwargs
225
                ) as initiator_and_group:
226
                    initiator, group, is_new = initiator_and_group
×
227
                    if is_new:
×
228
                        initiator.tbegin = tbegin
×
229
                        group.tbegin = tbegin
×
230
                        records.add_new_record_exception(
×
231
                            initiator, group, "http:///", ex
232
                        )
233
            raise
×
234

235
        logger().info(
×
236
            f"WRAP_SOCKET - {type(sock)}={id(sock)} {type(sslsocket)}={id(sslsocket)}"
237
        )
238

239
        socketdata = records._tracerhttp1.move_socket_data(sslsocket, sock)
×
240
        if socketdata:
×
241
            logger().info(f"WRAP_SOCKET * - socketdata={socketdata}")
×
242

243
        return sslsocket
×
244

245
    return hook
1✔
246

247

248
# hook: ssl.SSLContext.wrap_socket
249
# what: Wrap an existing Python socket sock and return an instance of SSLContext.sslsocket_class (default SSLSocket).
250
# action: Link the socket and the sslsocket
251
def set_hook_for_sslcontext_wrap_socket(records: HTTPRecords, method: Callable):
1✔
252
    def hook(self, sock, *args, **kwargs):
1✔
253
        tbegin: datetime.datetime = datetime.datetime.now(datetime.timezone.utc)
1✔
254
        try:
1✔
255
            sslsocket = method(self, sock, *args, **kwargs)
1✔
256
        except Exception as ex:
×
257
            if records.client:
×
258
                with httpdbg_initiator(
×
259
                    records, traceback.extract_stack(), method, *args, **kwargs
260
                ) as initiator_and_group:
261
                    initiator, group, is_new = initiator_and_group
×
262
                    if is_new:
×
263
                        initiator.tbegin = tbegin
×
264
                        group.tbegin = tbegin
×
265
                        records.add_new_record_exception(
×
266
                            initiator, group, "http:///", ex
267
                        )
268
            raise
×
269

270
        logger().info(
1✔
271
            f"WRAP_SOCKET (SSLContext) - {type(self)}={id(self)}  {type(sock)}={id(sock)} {type(sslsocket)}={id(sslsocket)}"
272
        )
273

274
        socketdata = records._tracerhttp1.move_socket_data(sslsocket, sock)
1✔
275
        if socketdata:
1✔
276
            logger().info(f"WRAP_SOCKET (SSLContext) * - socketdata={socketdata}")
×
277

278
        return sslsocket
1✔
279

280
    return hook
1✔
281

282

283
# hook: ssl.SSLContext.wrap_bio
284
# what: Wrap the BIO objects incoming and outgoing and return an instance of SSLContext.sslobject_class (default SSLObject).
285
# action: Record a new SocketRawData if necessary
286
def set_hook_for_socket_wrap_bio(records: HTTPRecords, method: Callable):
1✔
287
    def hook(self, *args, **kwargs):
1✔
288
        tbegin: datetime.datetime = datetime.datetime.now(datetime.timezone.utc)
1✔
289
        try:
1✔
290
            sslobject = method(self, *args, **kwargs)
1✔
291
        except Exception as ex:
×
292
            if records.client:
×
293
                with httpdbg_initiator(
×
294
                    records, traceback.extract_stack(), method, *args, **kwargs
295
                ) as initiator_and_group:
296
                    initiator, group, is_new = initiator_and_group
×
297
                    if is_new:
×
298
                        initiator.tbegin = tbegin
×
299
                        group.tbegin = tbegin
×
300
                        records.add_new_record_exception(
×
301
                            initiator, group, "http:///", ex
302
                        )
303
            raise
×
304

305
        logger().info(
1✔
306
            f"WRAP_SOCKET_BIO - {type(self)}={id(self)} {type(sslobject)}={id(sslobject)}"
307
        )
308

309
        socketdata = records._tracerhttp1.get_socket_data(sslobject, self)
1✔
310
        if socketdata:
1✔
311
            logger().info(f"WRAP_SOCKET_BIO * - socketdata={socketdata}")
1✔
312

313
        return sslobject
1✔
314

315
    return hook
1✔
316

317

318
# hook: socket.socket.recv_into, ssl.SSLSocket.recv_into, asyncio.proactor_events._ProactorReadPipeTransport._data_received
319
# what: Receive up to nbytes bytes from the socket, storing the data into a buffer rather than creating a new bytestring.
320
# action: Append the data to an existing SocketRawData
321
def set_hook_for_socket_recv_into(records: HTTPRecords, method: Callable):
1✔
322
    def hook(self, buffer, *args, **kwargs):
1✔
323
        socketdata = records._tracerhttp1.get_socket_data(self)
1✔
324
        if socketdata:
1✔
325
            logger().info(
1✔
326
                f"RECV_INTO - self={self} id={id(self)} socketdata={socketdata} args={args} kwargs={kwargs}"
327
            )
328

329
        nbytes = method(self, buffer, *args, **kwargs)
1✔
330

331
        if buffer:  # it appears that the buffer may be None (observed on Windows).
1✔
332
            if socketdata:
1✔
333
                if socketdata.record:
1✔
334
                    logger().info(
1✔
335
                        f"RECV_INTO (after) - id={id(self)} buffer={(b''+buffer)[:20]}"
336
                    )
337
                    socketdata.record.receive_data(buffer[:nbytes])
1✔
338
                else:
339
                    socketdata.rawdata += buffer[:nbytes]
1✔
340
                    http_detected = socketdata.http_detected()
1✔
341
                    if http_detected:
1✔
342
                        logger().info("RECV_INTO - http detected")
1✔
343
                        logger().info(
1✔
344
                            f"RECV_INTO (after) - id={id(self)} buffer={(b''+buffer)[:20]}"
345
                        )
346
                        with httpdbg_initiator(
1✔
347
                            records,
348
                            traceback.extract_stack(),
349
                            method,
350
                            self,
351
                            buffer,
352
                            *args,
353
                            **kwargs,
354
                        ) as initiator_and_group:
355
                            initiator, group, is_new = initiator_and_group
1✔
356
                            if is_new:
1✔
357
                                tbegin = socketdata.tbegin - datetime.timedelta(
1✔
358
                                    milliseconds=1
359
                                )
360
                                initiator.tbegin = tbegin
1✔
361
                                group.tbegin = tbegin
1✔
362
                            socketdata.record = HTTP1Record(
1✔
363
                                initiator.id,
364
                                group.id,
365
                                records.current_tag,
366
                                tbegin=socketdata.tbegin,
367
                                is_client=False,
368
                            )
369
                            socketdata.record.address = socketdata.address
1✔
370
                            socketdata.record.ssl = socketdata.ssl
1✔
371
                            socketdata.record.receive_data(socketdata.rawdata)
1✔
372
                            if records.server:
1✔
373
                                records.requests[socketdata.record.id] = (
1✔
374
                                    socketdata.record
375
                                )
376
                    elif http_detected is False:  # if None, there is nothing to do
1✔
NEW
377
                        records._tracerhttp1.mark_as_not_a_http_request(self)
×
378

379
        return nbytes
1✔
380

381
    return hook
1✔
382

383

384
# hook: socket.socket.recv, ssl.SSLSocket.recv
385
# what: Receive data from the socket. The return value is a bytes object representing the data received.
386
# action: Append the data to an existing SocketRawData
387
def set_hook_for_socket_recv(records: HTTPRecords, method: Callable):
1✔
388
    def hook(self, bufsize, *args, **kwargs):
1✔
389
        socketdata = records._tracerhttp1.get_socket_data(self)
1✔
390
        if socketdata:
1✔
391
            logger().info(
1✔
392
                f"RECV - self={self} id={id(self)} socketdata={socketdata} bufsize={bufsize} args={args} kwargs={kwargs}"
393
            )
394

395
        buffer = method(self, bufsize, *args, **kwargs)
1✔
396

397
        if socketdata:
1✔
398
            if socketdata.record:
1✔
399
                socketdata.record.receive_data(buffer)
1✔
400
            else:
401
                socketdata.rawdata += buffer
1✔
402
                http_detected = socketdata.http_detected()
1✔
403
                if http_detected:
1✔
404
                    logger().info("RECV - http detected")
×
405
                    with httpdbg_initiator(
×
406
                        records,
407
                        traceback.extract_stack(),
408
                        method,
409
                        self,
410
                        bufsize,
411
                        *args,
412
                        **kwargs,
413
                    ) as initiator_and_group:
414
                        initiator, group, is_new = initiator_and_group
×
415
                        if is_new:
×
416
                            tbegin = socketdata.tbegin - datetime.timedelta(
×
417
                                milliseconds=1
418
                            )
419
                            initiator.tbegin = tbegin
×
420
                            group.tbegin = tbegin
×
421
                        socketdata.record = HTTP1Record(
×
422
                            initiator.id,
423
                            group.id,
424
                            records.current_tag,
425
                            tbegin=socketdata.tbegin,
426
                            is_client=False,
427
                        )
428
                        socketdata.record.address = socketdata.address
×
429
                        socketdata.record.ssl = socketdata.ssl
×
430
                        socketdata.record.receive_data(socketdata.rawdata)
×
431
                        if records.server:
×
432
                            records.requests[socketdata.record.id] = socketdata.record
×
433
                elif http_detected is False:  # if None, there is nothing to do
1✔
434
                    records._tracerhttp1.mark_as_not_a_http_request(self)
1✔
435

436
        return buffer
1✔
437

438
    return hook
1✔
439

440

441
# hook: socket.socket.sendall
442
# what: Send data to the socket.
443
# action: Append the data to an existing SocketRawData. Check if this is an HTTP request.
444
# and record it if this is case, otherwise delete the temporay SocketRawData.
445
def set_hook_for_socket_sendall(records: HTTPRecords, method: Callable):
1✔
446
    def hook(self, data, *args, **kwargs):
1✔
447
        socketdata = records._tracerhttp1.get_socket_data(self, request=True)
1✔
448
        if socketdata:
1✔
449
            logger().info(
1✔
450
                f"SENDALL - self={self} id={id(self)} socketdata={socketdata} data={(b''+bytes(data))[:20]!r} type={type(data)} args={args} kwargs={kwargs}"
451
            )
452
        if socketdata:
1✔
453
            if socketdata.record:
1✔
454
                socketdata.record.send_data(data)
1✔
455
            else:
456
                socketdata.rawdata += data
1✔
457
                http_detected = socketdata.http_detected()
1✔
458
                if http_detected:
1✔
459
                    logger().info("SENDALL - http detected")
1✔
460
                    with httpdbg_initiator(
1✔
461
                        records,
462
                        traceback.extract_stack(),
463
                        method,
464
                        self,
465
                        data,
466
                        *args,
467
                        **kwargs,
468
                    ) as initiator_and_group:
469
                        initiator, group, is_new = initiator_and_group
1✔
470
                        if is_new:
1✔
471
                            tbegin = socketdata.tbegin - datetime.timedelta(
×
472
                                milliseconds=1
473
                            )
474
                            initiator.tbegin = tbegin
×
475
                            group.tbegin = tbegin
×
476
                        socketdata.record = HTTP1Record(
1✔
477
                            initiator.id,
478
                            group.id,
479
                            records.current_tag,
480
                            tbegin=socketdata.tbegin,
481
                        )
482
                        socketdata.record.address = socketdata.address
1✔
483
                        socketdata.record.ssl = socketdata.ssl
1✔
484
                        socketdata.record.send_data(socketdata.rawdata)
1✔
485
                        if records.client:
1✔
486
                            records.requests[socketdata.record.id] = socketdata.record
1✔
487
                elif http_detected is False:  # if None, there is nothing to do
1✔
488
                    records._tracerhttp1.mark_as_not_a_http_request(self)
1✔
489

490
        return method(self, data, *args, **kwargs)
1✔
491

492
    return hook
1✔
493

494

495
# hook: socket.socket.send, ssl.SSLSocket.send, asyncio.proactor_events._ProactorBaseWritePipeTransport.write
496
# what: Send data to the socket.
497
# action: Append the data to an existing SocketRawData. Check if this is an HTTP request.
498
# and record it if this is case, otherwise delete the temporay SocketRawData.
499
def set_hook_for_socket_send(records: HTTPRecords, method: Callable):
1✔
500
    def hook(self, data, *args, **kwargs):
1✔
501
        socketdata = records._tracerhttp1.get_socket_data(self, request=True)
1✔
502
        if socketdata:
1✔
503
            logger().info(
1✔
504
                f"SEND - self={self} id={id(self)} socketdata={socketdata} bytes={(b''+data)[:20]} args={args} kwargs={kwargs}"
505
            )
506

507
        size = method(self, data, *args, **kwargs)
1✔
508

509
        if socketdata:
1✔
510
            if socketdata.record:
1✔
511
                socketdata.record.send_data(data[:size])
1✔
512
            else:
513
                socketdata.rawdata += data[:size]
1✔
514
                http_detected = socketdata.http_detected()
1✔
515
                if http_detected:
1✔
516
                    with httpdbg_initiator(
1✔
517
                        records,
518
                        traceback.extract_stack(),
519
                        method,
520
                        self,
521
                        data,
522
                        *args,
523
                        **kwargs,
524
                    ) as initiator_and_group:
525
                        initiator, group, is_new = initiator_and_group
1✔
526
                        if is_new:
1✔
UNCOV
527
                            tbegin = socketdata.tbegin - datetime.timedelta(
×
528
                                milliseconds=1
529
                            )
UNCOV
530
                            initiator.tbegin = tbegin
×
UNCOV
531
                            group.tbegin = tbegin
×
532
                        socketdata.record = HTTP1Record(
1✔
533
                            initiator.id,
534
                            group.id,
535
                            records.current_tag,
536
                            tbegin=socketdata.tbegin,
537
                        )
538
                        socketdata.record.address = socketdata.address
1✔
539
                        socketdata.record.ssl = socketdata.ssl
1✔
540
                        socketdata.record.send_data(socketdata.rawdata)
1✔
541
                        if records.client:
1✔
542
                            records.requests[socketdata.record.id] = socketdata.record
1✔
543
                elif http_detected is False:  # if None, there is nothing to do
1✔
544
                    records._tracerhttp1.mark_as_not_a_http_request(self)
1✔
545
        return size
1✔
546

547
    return hook
1✔
548

549

550
# hook: asyncio.BaseEventLoop.create_connection
551
# what: Open a streaming transport connection to a given address specified by host and port.
552
# action: Link the socket and the sslsocket
553
def set_hook_for_asyncio_create_connection(records: HTTPRecords, method: Callable):
1✔
554
    async def hook(self, *args, **kwargs):
1✔
555
        logger().info(
1✔
556
            f"CREATE_CONNECTION - self={self} id={id(self)} args={args} kwargs={kwargs}"
557
        )
558
        r = await method(self, *args, **kwargs)
1✔
559

560
        transport = r[0]
1✔
561
        sock = transport.get_extra_info("socket")
1✔
562
        if sock:
1✔
563
            ssl_object = transport.get_extra_info("ssl_object")
1✔
564
            if ssl_object:
1✔
565
                socketdata = records._tracerhttp1.get_socket_data(
1✔
566
                    ssl_object, sock, force_new=True
567
                )  # to link the cnx info to the sslobject
568
                logger().info(
1✔
569
                    f"CREATE_CONNECTION - ssl_object ssl_object={ssl_object} ssl_objectid={id(ssl_object)} socketdata={socketdata}"
570
                )
571
        return r
1✔
572

573
    return hook
1✔
574

575

576
# hook: ssl.SSLObject.write
577
# what: Write buf to the SSL socket and return the number of bytes written.
578
# action: Append the data to an existing SocketRawData. Check if this is an HTTP request.
579
# and record it if this is case, otherwise delete the temporay SocketRawData.
580
def set_hook_for_sslobject_write(records: HTTPRecords, method: Callable):
1✔
581
    def hook(self, buf, *args, **kwargs):
1✔
582
        logger().info(f"WRITE - {type(self)}={id(self)} buf={(b'' + buf)[:20]}")
1✔
583
        socketdata = records._tracerhttp1.get_socket_data(self, request=True)
1✔
584
        if socketdata:
1✔
585
            logger().info(f"WRITE * - socketdata={socketdata}")
1✔
586

587
        size = method(self, buf, *args, **kwargs)
1✔
588

589
        if socketdata:
1✔
590
            if socketdata.record:
1✔
591
                socketdata.record.send_data(bytes(buf[:size]))
1✔
592
            else:
593
                socketdata.rawdata += bytes(buf[:size])
1✔
594
                http_detected = socketdata.http_detected()
1✔
595
                if http_detected:
1✔
596
                    with httpdbg_initiator(
1✔
597
                        records,
598
                        traceback.extract_stack(),
599
                        method,
600
                        self,
601
                        buf,
602
                        *args,
603
                        **kwargs,
604
                    ) as initiator_and_group:
605
                        initiator, group, is_new = initiator_and_group
1✔
606
                        if is_new:
1✔
607
                            tbegin = socketdata.tbegin - datetime.timedelta(
×
608
                                milliseconds=1
609
                            )
610
                            initiator.tbegin = tbegin
×
611
                            group.tbegin = tbegin
×
612
                        socketdata.record = HTTP1Record(
1✔
613
                            initiator.id,
614
                            group.id,
615
                            records.current_tag,
616
                            tbegin=socketdata.tbegin,
617
                        )
618
                        socketdata.record.address = socketdata.address
1✔
619
                        socketdata.record.ssl = socketdata.ssl
1✔
620
                        socketdata.record.send_data(socketdata.rawdata)
1✔
621
                        if records.client:
1✔
622
                            records.requests[socketdata.record.id] = socketdata.record
1✔
623
                elif http_detected is False:  # if None, there is nothing to do
×
NEW
624
                    records._tracerhttp1.mark_as_not_a_http_request(self)
×
625
        return size
1✔
626

627
    return hook
1✔
628

629

630
# hook: ssl.SSLObject.read
631
# what: Read up to len bytes of data from the SSL socket and return the result as a bytes instance.
632
# action: Append the data to an existing SocketRawData.
633
def set_hook_for_sslobject_read(records: HTTPRecords, method: Callable):
1✔
634
    def hook(self, *args, **kwargs):
1✔
635
        logger().info(f"READ - {type(self)}={id(self)}")
1✔
636
        socketdata = records._tracerhttp1.get_socket_data(self)
1✔
637
        if socketdata:
1✔
638
            logger().info(f"READ * - socketdata={socketdata}")
1✔
639

640
        r = method(self, *args, **kwargs)
1✔
641

642
        if socketdata and socketdata.record:
1✔
643
            allargs = getcallargs(method, self, *args, **kwargs)
1✔
644
            if allargs.get("buffer"):
1✔
645
                socketdata.record.receive_data(bytes(allargs.get("buffer"))[:r])
×
646
            else:
647
                socketdata.record.receive_data(bytes(r)[: allargs.get("len")])
1✔
648

649
        return r
1✔
650

651
    return hook
1✔
652

653

654
@contextmanager
1✔
655
def hook_socket(records: HTTPRecords) -> Generator[None, None, None]:
1✔
656
    socket.socket.__init__ = decorate(
1✔
657
        records, socket.socket.__init__, set_hook_for_socket_init
658
    )
659

660
    socket.socket.connect = decorate(
1✔
661
        records, socket.socket.connect, set_hook_for_socket_connect
662
    )
663
    socket.socket.recv_into = decorate(
1✔
664
        records, socket.socket.recv_into, set_hook_for_socket_recv_into
665
    )
666
    socket.socket.recv = decorate(records, socket.socket.recv, set_hook_for_socket_recv)
1✔
667
    socket.socket.sendall = decorate(
1✔
668
        records, socket.socket.sendall, set_hook_for_socket_sendall
669
    )
670
    socket.socket.send = decorate(records, socket.socket.send, set_hook_for_socket_send)
1✔
671

672
    if (sys.version_info.major == 3) and (sys.version_info.minor < 12):
1✔
673
        ssl.wrap_socket = decorate(  # type: ignore[attr-defined]
1✔
674
            records, ssl.wrap_socket, set_hook_for_ssl_wrap_socket  # type: ignore[attr-defined]
675
        )
676

677
    ssl.SSLContext.wrap_socket = decorate(
1✔
678
        records, ssl.SSLContext.wrap_socket, set_hook_for_sslcontext_wrap_socket
679
    )
680
    ssl.SSLContext.wrap_bio = decorate(
1✔
681
        records, ssl.SSLContext.wrap_bio, set_hook_for_socket_wrap_bio
682
    )
683

684
    ssl.SSLSocket.connect = decorate(
1✔
685
        records, ssl.SSLSocket.connect, set_hook_for_socket_connect
686
    )
687
    ssl.SSLSocket.recv_into = decorate(
1✔
688
        records, ssl.SSLSocket.recv_into, set_hook_for_socket_recv_into
689
    )
690
    ssl.SSLSocket.recv = decorate(records, ssl.SSLSocket.recv, set_hook_for_socket_recv)
1✔
691
    # ssl.SSLSocket.sendall = decorate(
692
    #     records, ssl.SSLSocket.sendall, set_hook_for_socket_sendall
693
    # )
694
    ssl.SSLSocket.send = decorate(records, ssl.SSLSocket.send, set_hook_for_socket_send)
1✔
695

696
    # for aiohttp
697
    ssl.SSLObject.write = decorate(
1✔
698
        records, ssl.SSLObject.write, set_hook_for_sslobject_write
699
    )
700
    ssl.SSLObject.read = decorate(
1✔
701
        records, ssl.SSLObject.read, set_hook_for_sslobject_read
702
    )
703
    asyncio.BaseEventLoop.create_connection = decorate(
1✔
704
        records,
705
        asyncio.BaseEventLoop.create_connection,
706
        set_hook_for_asyncio_create_connection,
707
    )
708

709
    # only for async HTTP requests (not HTTPS) on Windows
710
    if platform.system().lower() == "windows":
1✔
711
        asyncio.proactor_events._ProactorReadPipeTransport._data_received = decorate(  # type: ignore
×
712
            records,
713
            asyncio.proactor_events._ProactorReadPipeTransport._data_received,  # type: ignore
714
            set_hook_for_socket_recv_into,
715
        )
716
        asyncio.proactor_events._ProactorBaseWritePipeTransport.write = decorate(
×
717
            records,
718
            asyncio.proactor_events._ProactorBaseWritePipeTransport.write,
719
            set_hook_for_socket_send,
720
        )
721

722
    yield
1✔
723

724
    socket.socket.__init__ = undecorate(socket.socket.__init__)
1✔
725

726
    socket.socket.connect = undecorate(socket.socket.connect)
1✔
727
    socket.socket.recv_into = undecorate(socket.socket.recv_into)
1✔
728
    socket.socket.recv = undecorate(socket.socket.recv)
1✔
729
    socket.socket.sendall = undecorate(socket.socket.sendall)
1✔
730
    socket.socket.send = undecorate(socket.socket.send)
1✔
731

732
    if (sys.version_info.major == 3) and (sys.version_info.minor < 12):
1✔
733
        ssl.wrap_socket = undecorate(ssl.wrap_socket)  # type: ignore[attr-defined]
1✔
734

735
    ssl.SSLContext.wrap_socket = undecorate(ssl.SSLContext.wrap_socket)
1✔
736
    ssl.SSLContext.wrap_bio = undecorate(ssl.SSLContext.wrap_bio)
1✔
737

738
    ssl.SSLSocket.connect = undecorate(ssl.SSLSocket.connect)
1✔
739
    ssl.SSLSocket.recv_into = undecorate(ssl.SSLSocket.recv_into)
1✔
740
    ssl.SSLSocket.recv = undecorate(ssl.SSLSocket.recv)
1✔
741
    # ssl.SSLSocket.sendall = undecorate(ssl.SSLSocket.sendall)
742
    ssl.SSLSocket.send = undecorate(ssl.SSLSocket.send)
1✔
743

744
    # for aiohttp / async httpx
745
    ssl.SSLObject.write = undecorate(ssl.SSLObject.write)
1✔
746
    ssl.SSLObject.read = undecorate(ssl.SSLObject.read)
1✔
747
    asyncio.BaseEventLoop.create_connection = undecorate(
1✔
748
        asyncio.BaseEventLoop.create_connection
749
    )
750

751
    # only for async HTTP requests (not HTTPS) on Windows
752
    if platform.system().lower() == "windows":
1✔
753
        asyncio.proactor_events._ProactorReadPipeTransport._data_received = undecorate(  # type: ignore
×
754
            asyncio.proactor_events._ProactorReadPipeTransport._data_received  # type: ignore
755
        )
756
        asyncio.proactor_events._ProactorBaseWritePipeTransport.write = undecorate(
×
757
            asyncio.proactor_events._ProactorBaseWritePipeTransport.write
758
        )
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc