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

cle-b / httpdbg / 17172967858

23 Aug 2025 07:42AM UTC coverage: 87.794% (-0.08%) from 87.875%
17172967858

Pull #203

github

cle-b
fix call after refactoring
Pull Request #203: prepare to http2 support

0 of 1 new or added line in 1 file covered. (0.0%)

3 existing lines in 1 file now uncovered.

2165 of 2466 relevant lines covered (87.79%)

0.88 hits per line

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

79.9
/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, SocketRawData] = {}
1✔
72
        self.ignore: tuple[tuple[str, int], ...] = ignore
1✔
73

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

80
        if force_new:
1✔
81
            self.del_socket_data(obj)
1✔
82

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

141
        return socketdata
1✔
142

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

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

158

159
# hook: socket.socket.__init__
160
# what: A new socket object is created.
161
# action: If an entry exists in the temporary raw socket storage list, it is removed.
162
def set_hook_for_socket_init(records: HTTPRecords, method: Callable):
1✔
163
    def hook(self, *args, **kwargs):
1✔
164
        records._tracerhttp1.del_socket_data(self)
1✔
165

166
        return method(self, *args, **kwargs)
1✔
167

168
    return hook
1✔
169

170

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

201
        return r
1✔
202

203
    return hook
1✔
204

205

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

229
        logger().info(
×
230
            f"WRAP_SOCKET - {type(sock)}={id(sock)} {type(sslsocket)}={id(sslsocket)}"
231
        )
232

NEW
233
        socketdata = records._tracerhttp1.move_socket_data(sslsocket, sock)
×
234
        if socketdata:
×
235
            logger().info(f"WRAP_SOCKET * - socketdata={socketdata}")
×
236

237
        return sslsocket
×
238

239
    return hook
1✔
240

241

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

264
        logger().info(
1✔
265
            f"WRAP_SOCKET (SSLContext) - {type(self)}={id(self)}  {type(sock)}={id(sock)} {type(sslsocket)}={id(sslsocket)}"
266
        )
267

268
        socketdata = records._tracerhttp1.move_socket_data(sslsocket, sock)
1✔
269
        if socketdata:
1✔
270
            logger().info(f"WRAP_SOCKET (SSLContext) * - socketdata={socketdata}")
×
271

272
        return sslsocket
1✔
273

274
    return hook
1✔
275

276

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

299
        logger().info(
1✔
300
            f"WRAP_SOCKET_BIO - {type(self)}={id(self)} {type(sslobject)}={id(sslobject)}"
301
        )
302

303
        socketdata = records._tracerhttp1.get_socket_data(sslobject, self)
1✔
304
        if socketdata:
1✔
305
            logger().info(f"WRAP_SOCKET_BIO * - socketdata={socketdata}")
1✔
306

307
        return sslobject
1✔
308

309
    return hook
1✔
310

311

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

323
        nbytes = method(self, buffer, *args, **kwargs)
1✔
324

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

373
        return nbytes
1✔
374

375
    return hook
1✔
376

377

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

389
        buffer = method(self, bufsize, *args, **kwargs)
1✔
390

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

430
        return buffer
1✔
431

432
    return hook
1✔
433

434

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

484
        return method(self, data, *args, **kwargs)
1✔
485

486
    return hook
1✔
487

488

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

501
        size = method(self, data, *args, **kwargs)
1✔
502

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

541
    return hook
1✔
542

543

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

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

567
    return hook
1✔
568

569

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

581
        size = method(self, buf, *args, **kwargs)
1✔
582

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

621
    return hook
1✔
622

623

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

634
        r = method(self, *args, **kwargs)
1✔
635

636
        if socketdata and socketdata.record:
1✔
637
            allargs = getcallargs(method, self, *args, **kwargs)
1✔
638
            if allargs.get("buffer"):
1✔
639
                socketdata.record.receive_data(bytes(allargs.get("buffer"))[:r])
×
640
            else:
641
                socketdata.record.receive_data(bytes(r)[: allargs.get("len")])
1✔
642

643
        return r
1✔
644

645
    return hook
1✔
646

647

648
@contextmanager
1✔
649
def hook_socket(records: HTTPRecords) -> Generator[None, None, None]:
1✔
650
    socket.socket.__init__ = decorate(
1✔
651
        records, socket.socket.__init__, set_hook_for_socket_init
652
    )
653

654
    socket.socket.connect = decorate(
1✔
655
        records, socket.socket.connect, set_hook_for_socket_connect
656
    )
657
    socket.socket.recv_into = decorate(
1✔
658
        records, socket.socket.recv_into, set_hook_for_socket_recv_into
659
    )
660
    socket.socket.recv = decorate(records, socket.socket.recv, set_hook_for_socket_recv)
1✔
661
    socket.socket.sendall = decorate(
1✔
662
        records, socket.socket.sendall, set_hook_for_socket_sendall
663
    )
664
    socket.socket.send = decorate(records, socket.socket.send, set_hook_for_socket_send)
1✔
665

666
    if (sys.version_info.major == 3) and (sys.version_info.minor < 12):
1✔
667
        ssl.wrap_socket = decorate(  # type: ignore[attr-defined]
1✔
668
            records, ssl.wrap_socket, set_hook_for_ssl_wrap_socket  # type: ignore[attr-defined]
669
        )
670

671
    ssl.SSLContext.wrap_socket = decorate(
1✔
672
        records, ssl.SSLContext.wrap_socket, set_hook_for_sslcontext_wrap_socket
673
    )
674
    ssl.SSLContext.wrap_bio = decorate(
1✔
675
        records, ssl.SSLContext.wrap_bio, set_hook_for_socket_wrap_bio
676
    )
677

678
    ssl.SSLSocket.connect = decorate(
1✔
679
        records, ssl.SSLSocket.connect, set_hook_for_socket_connect
680
    )
681
    ssl.SSLSocket.recv_into = decorate(
1✔
682
        records, ssl.SSLSocket.recv_into, set_hook_for_socket_recv_into
683
    )
684
    ssl.SSLSocket.recv = decorate(records, ssl.SSLSocket.recv, set_hook_for_socket_recv)
1✔
685
    # ssl.SSLSocket.sendall = decorate(
686
    #     records, ssl.SSLSocket.sendall, set_hook_for_socket_sendall
687
    # )
688
    ssl.SSLSocket.send = decorate(records, ssl.SSLSocket.send, set_hook_for_socket_send)
1✔
689

690
    # for aiohttp
691
    ssl.SSLObject.write = decorate(
1✔
692
        records, ssl.SSLObject.write, set_hook_for_sslobject_write
693
    )
694
    ssl.SSLObject.read = decorate(
1✔
695
        records, ssl.SSLObject.read, set_hook_for_sslobject_read
696
    )
697
    asyncio.BaseEventLoop.create_connection = decorate(
1✔
698
        records,
699
        asyncio.BaseEventLoop.create_connection,
700
        set_hook_for_asyncio_create_connection,
701
    )
702

703
    # only for async HTTP requests (not HTTPS) on Windows
704
    if platform.system().lower() == "windows":
1✔
705
        asyncio.proactor_events._ProactorReadPipeTransport._data_received = decorate(  # type: ignore
×
706
            records,
707
            asyncio.proactor_events._ProactorReadPipeTransport._data_received,  # type: ignore
708
            set_hook_for_socket_recv_into,
709
        )
710
        asyncio.proactor_events._ProactorBaseWritePipeTransport.write = decorate(
×
711
            records,
712
            asyncio.proactor_events._ProactorBaseWritePipeTransport.write,
713
            set_hook_for_socket_send,
714
        )
715

716
    yield
1✔
717

718
    socket.socket.__init__ = undecorate(socket.socket.__init__)
1✔
719

720
    socket.socket.connect = undecorate(socket.socket.connect)
1✔
721
    socket.socket.recv_into = undecorate(socket.socket.recv_into)
1✔
722
    socket.socket.recv = undecorate(socket.socket.recv)
1✔
723
    socket.socket.sendall = undecorate(socket.socket.sendall)
1✔
724
    socket.socket.send = undecorate(socket.socket.send)
1✔
725

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

729
    ssl.SSLContext.wrap_socket = undecorate(ssl.SSLContext.wrap_socket)
1✔
730
    ssl.SSLContext.wrap_bio = undecorate(ssl.SSLContext.wrap_bio)
1✔
731

732
    ssl.SSLSocket.connect = undecorate(ssl.SSLSocket.connect)
1✔
733
    ssl.SSLSocket.recv_into = undecorate(ssl.SSLSocket.recv_into)
1✔
734
    ssl.SSLSocket.recv = undecorate(ssl.SSLSocket.recv)
1✔
735
    # ssl.SSLSocket.sendall = undecorate(ssl.SSLSocket.sendall)
736
    ssl.SSLSocket.send = undecorate(ssl.SSLSocket.send)
1✔
737

738
    # for aiohttp / async httpx
739
    ssl.SSLObject.write = undecorate(ssl.SSLObject.write)
1✔
740
    ssl.SSLObject.read = undecorate(ssl.SSLObject.read)
1✔
741
    asyncio.BaseEventLoop.create_connection = undecorate(
1✔
742
        asyncio.BaseEventLoop.create_connection
743
    )
744

745
    # only for async HTTP requests (not HTTPS) on Windows
746
    if platform.system().lower() == "windows":
1✔
747
        asyncio.proactor_events._ProactorReadPipeTransport._data_received = undecorate(  # type: ignore
×
748
            asyncio.proactor_events._ProactorReadPipeTransport._data_received  # type: ignore
749
        )
750
        asyncio.proactor_events._ProactorBaseWritePipeTransport.write = undecorate(
×
751
            asyncio.proactor_events._ProactorBaseWritePipeTransport.write
752
        )
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