1import warnings
2import logging
3from functools import cached_property
4from collections.abc import Set, Sequence, Mapping
5from typing import ClassVar, Optional, Any
6
7from ...payload import Payload
8from ..types import HarEntry, DictLayers, NameValueDict
9from ..layers import FrameMixin, TCPIPMixin, get_protocols, get_har_communication, get_tcp_stream_id, get_community_id
10from ..utils import get_tshark_bytes_from_raw, har_entry_with_common_fields
11
12LOGGER = logging.getLogger(__name__)
13
14
[docs]
15class Http2Substream(FrameMixin, TCPIPMixin):
16 """
17 Class to represent a HTTP2 substream.
18
19 It wraps the raw HTTP2 substream and the parent layers to extract the relevant information.
20 """
21 KEEP_LAYERS: ClassVar[Set[str]] = {'frame', 'ip', 'ipv6', 'tcp'}
22
23 def __init__(self, raw_http2_substream: Mapping[str, Any], parent_layers: DictLayers):
24 self.layers: DictLayers = {
25 layer_name: layer_data
26 for layer_name, layer_data in parent_layers.items()
27 if layer_name in self.KEEP_LAYERS
28 }
29 self.raw_http2_substream = raw_http2_substream
30
31 @property
32 def http2_flags(self) -> int:
33 return int(self.raw_http2_substream.get('http2.flags', '0x0'), 0)
34
35 @property
36 def http2_type(self) -> int:
37 return int(self.raw_http2_substream.get('http2.type', -1))
38
39 @property
40 def raw_headers(self) -> list[dict[str, Any]]:
41 headers = self.raw_http2_substream.get('http2.header', [])
42 if isinstance(headers, dict):
43 headers = [headers] # when only 1 header tshark does not wrap it into a list
44 assert isinstance(headers, list), headers
45 return headers
46
47
[docs]
48class Http2RequestResponse:
49 """
50 Base class to represent a HTTP2 request or response. It contains the headers and data of the request or response.
51 Implements the common properties of a HTTP2 request or response.
52 """
53 FALLBACK_CONTENT_TYPE: ClassVar[str] = 'application/octet-stream'
54
55 def __init__(self, substreams: Sequence[Http2Substream]):
56 self.substreams = substreams
57 self.headers, self.data, self.headers_streams, self.data_streams = Http2Helper.get_headers_and_data(substreams)
58
59 def __bool__(self) -> bool:
60 return bool(self.substreams)
61
62 @property
63 def frames_nbs(self) -> Sequence[int]:
64 # ordered set of frames numbers
65 return list({s.frame_nb: 0 for s in self.substreams})
66
67 @property
68 def timestamp(self) -> float:
69 return self.substreams[0].timestamp
70
71 @property
72 def src_host(self) -> str:
73 return self.substreams[0].src_host
74
75 @property
76 def dst_host(self) -> str:
77 return self.substreams[0].dst_host
78
79 @property
80 def src_ip(self) -> str:
81 return self.substreams[0].src_ip
82
83 @property
84 def dst_ip(self) -> str:
85 return self.substreams[0].dst_ip
86
87 @property
88 def src_port(self) -> int:
89 return self.substreams[0].src_port
90
91 @property
92 def dst_port(self) -> int:
93 return self.substreams[0].dst_port
94
95 @property
96 def http_version(self) -> str:
97 return 'HTTP/2'
98
99 @property
100 def header_length(self) -> int:
101 # The effective payload sent over network has bytes size `http2.length` <= `http2.headers.length`
102 # (because special headers - like `:status` - have predefined codes)
103 if not self:
104 return -1
105 return sum(int(s.raw_http2_substream.get('http2.length', 0)) for s in self.headers_streams)
106
107 @property
108 def body_length(self) -> int:
109 """
110 This is number of compressed bytes (if any compression)
111
112 - `http2.length` is also populated for header substreams
113 - we do NOT always have the `http2.body.fragments` -> `http2.body.reassembled.length`
114 """
115 if not self:
116 return -1
117 declared_size = sum(int(s.raw_http2_substream.get('http2.length', 0)) for s in self.data_streams)
118 if declared_size != self.data.size and self.headers_map.get('content-encoding', 'identity') == 'identity':
119 warnings.warn(
120 f"Content length mismatch despite no compression: "
121 f"declared ({declared_size}) != computed ({self.data.size})"
122 f"\n{self}"
123 )
124 return declared_size
125
133
134 @property
135 def http_status(self) -> int:
136 return int(self.headers_map.get(':status', 0))
137
138 @property
139 def http_method(self) -> str:
140 return self.headers_map.get(':method', '')
141
142 @property
143 def content_type(self) -> str:
144 if not self or not self.data:
145 return ''
146 return self.headers_map.get('content-type', self.FALLBACK_CONTENT_TYPE)
147
[docs]
148 def get_duration_ms(self) -> float:
149 if not self:
150 return -1
151 return round(1000 * (self.substreams[-1].timestamp - self.substreams[0].timestamp), 2)
152
153
[docs]
154class Http2Request(Http2RequestResponse):
155 """
156 Class to represent a HTTP2 request. It contains the headers and data of the request.
157 """
158 def __init__(self, substreams: Sequence[Http2Substream]):
159 assert substreams, "At least one substream expected for a request"
160 super().__init__(substreams)
161
162 @property
163 def uri(self) -> str:
164 uris = {s.raw_http2_substream['http2.request.full_uri'] for s in self.headers_streams}
165 assert len(uris) == 1, uris
166 return next(iter(uris))
167
168 def __str__(self):
169 return (
170 f"Request [#{','.join(map(str, self.frames_nbs))}]: {len(self.headers_streams)}h + {len(self.data_streams)}d substreams\n\t"
171 f"URI: {self.uri}\n\tHeaders: {self.headers_map}\n\tData: {self.data}"
172 )
173
174
[docs]
175class Http2Response(Http2RequestResponse):
176 """
177 Class to represent a HTTP2 response. It contains the headers and data of the response.
178
179 <!> May be empty for convenience (response never received)
180 """
181 def __str__(self):
182 return (
183 f"Response [#{','.join(map(str, self.frames_nbs))}]: {len(self.headers_streams)}h + {len(self.data_streams)}d substreams\n\t"
184 f"Headers: {self.headers_map}\n\tData: {self.data}"
185 )
186
187
[docs]
188class Http2Stream:
189 """
190 Class to represent an entire HTTP2 stream (multiple substreams). It contains the request and response objects.
191 Http2Stream represents a single HTTP2 stream that can contain multiple substreams as follows:
192
193 .. code-block::
194
195 +-------------------------------------- (tcp stream, http2 stream)
196 | Http2SubStream 1 | Request headers (type: 1)
197 | Http2SubStream ... | Request data (type: 0, flags: 0x0) - partial data
198 | Http2SubStream 3 | Request data (type: 0, flags: 0x1) - end of stream, contains reassembled data
199 | (Http2SubStream 4 | Request trailers (type: 1))
200 +--------------------------------------
201 | Http2SubStream 5 | Response headers (type: 1)
202 | Http2SubStream ... | Response data (type: 0, flags: 0x0) - partial data
203 | Http2SubStream 7 | Response data (type: 0, flags: 0x1) - end of stream, contains reassembled data
204 | (Http2SubStream 8 | Response trailers (type: 1))
205 +--------------------------------------
206
207 Each HTTP2 stream is uniquely identified by a tuple (tcp stream index, http2 stream index)
208 and contains both request and response objects.
209 """
[docs]
210 def __init__(self, tcp_stream_id: int, http2_stream_id: int, community_id: str):
211 """
212 Defines a HTTP2 stream for the given TCP stream and HTTP2 stream.
213
214 :param tcp_stream_id: the ID of the TCP stream
215 :param http2_stream_id: the ID of the HTTP2 stream
216 :param community_id: the community ID (i.e. TCP|UDP + ips & ports) for this conversation
217 """
218 self.tcp_stream_id = tcp_stream_id
219 self.http2_stream_id = http2_stream_id
220 self.community_id = community_id
221 self.request: Optional[Http2Request] = None
222 self.response: Optional[Http2Response] = None
223 self.substreams: list[Http2Substream] = []
224
225 @property
226 def id(self) -> tuple[int, int]:
227 return (self.tcp_stream_id, self.http2_stream_id)
228
[docs]
229 def append(self, raw_http2_substream: Mapping[str, Any], parent_layers: DictLayers) -> None:
230 """
231 Append a new substream to the HTTP2 stream.
232
233 :param substream: the substream to be added
234 :param parent_layers: all layers of the frame containing the substream (a frame can contain multiple substreams)
235 """
236 self.substreams.append(Http2Substream(raw_http2_substream, parent_layers))
237
238 @property
239 def waiting_duration(self) -> float:
240 if not self.response:
241 return 0
242 assert self.request, self.id
243 start_stream = self.request.substreams[-1]
244 resp_stream = self.response.substreams[0]
245 return round(1000 * (resp_stream.timestamp - start_stream.timestamp), 2)
246
[docs]
247 def har_entry(self) -> Optional[dict[str, Any]]:
248 """
249 Create a HAR entry for the HTTP2 stream. It contains the request and response objects.
250
251 :return: the HAR entry for the HTTP2 stream
252 """
253 if not self.request: # may happen if we failed to find request among substreams
254 assert not self.response, self.id
255 return None
256 assert self.response is not None, self.id
257 first_stream = self.request.headers_streams[0]
258 return har_entry_with_common_fields({
259 '_timestamp': first_stream.timestamp,
260 'timings': {
261 'send': self.request.get_duration_ms(),
262 'wait': self.waiting_duration,
263 'receive': self.response.get_duration_ms(),
264 },
265 'serverIPAddress': first_stream.dst_ip,
266 '_communityId': self.community_id,
267 'request': Http2Helper.to_har(self.request),
268 'response': Http2Helper.to_har(self.response),
269 })
270
271 @staticmethod
272 def _get_raw_data_one_substream(raw_http2_substream: Mapping[str, Any]) -> Payload:
273 """
274 Note:
275 - when dealing with a reassembled data substream, `http2.data.data_raw` MAY not contain all data
276 - if the payload was compressed, tshark decompresses ALL data for us(even if data is reassembled)
277 under `Content-encoded entity body ...` -> `http2.data.data_raw` key, so we check it first
278 """
279 for k, v in raw_http2_substream.items():
280 if k.lower().startswith('content-encoded entity body '):
281 assert isinstance(v, dict), (k, v)
282 if 'http2.data.data_raw' not in v:
283 if 'data_raw' in v: # special case for failed decompression (not observed but as http protocol?!)
284 return Payload(get_tshark_bytes_from_raw(v['data_raw']))
285 # also happens in special case of empty decompressed payload (observed)
286 assert v['http2.data.data'] == '', v
287 return Payload(get_tshark_bytes_from_raw(v.get('http2.data.data_raw')))
288 if 'http2.body.fragments' in raw_http2_substream:
289 return Payload(get_tshark_bytes_from_raw(raw_http2_substream['http2.body.fragments']['http2.body.reassembled.data_raw']))
290 return Payload(get_tshark_bytes_from_raw(raw_http2_substream.get('http2.data.data_raw')))
291
[docs]
292 @classmethod
293 def get_raw_data(cls, raw_http2_substreams: Sequence[Mapping[str, Any]]) -> Payload:
294 """
295 Find the data in the substreams.
296
297 :param raw_http2_substreams: the data substreams to be analyzed
298 :return: the raw reassembled data if it exists, otherwise an empty Payload
299 """
300 # 1) search for the unique substream with reassembled data if present
301 substreams_reassembled = {
302 ix: raw_http2_substream for ix, raw_http2_substream in enumerate(raw_http2_substreams)
303 if 'http2.body.fragments' in raw_http2_substream
304 }
305 if substreams_reassembled:
306 # should be unique and for last data substream (on rare cases: != at end of stream)
307 assert len(substreams_reassembled) == 1, substreams_reassembled
308 ix_reassembled, substream_reassembled = next(iter(substreams_reassembled.items()))
309 # assert substream_reassembled['http2.flags'] & 0x01, substream_reassembled
310 assert ix_reassembled == len(raw_http2_substreams) - 1, raw_http2_substreams
311 return cls._get_raw_data_one_substream(substream_reassembled)
312 # 2) if there is none (which happens) we manually concatenate fragments
313 # <!> decompression for overall content is NOT implemented (should not happen?!)
314 return Payload.concat(*(cls._get_raw_data_one_substream(ss) for ss in raw_http2_substreams))
315
[docs]
316 def process(self) -> None:
317 """
318 Process the substreams and create the request and response objects accordingly. Substreams are processed in
319 order, the first substreams are request headers, followed by request data, and finally the response headers and
320 data. The reassembled data is used to create the request and response objects.
321
322 Request substreams are identified by the presence of the 'http2.request.full_uri' key in the raw stream.
323 If no response substream is found, the request object is created with the first substreams.
324
325 It retrieves the source and destination IP addresses from the first substream to identify the substreams that
326 belong to the request. The response substreams are identified by checking their source IP address matches
327 the destination IP address of the first substream.
328 """
329 assert self.substreams, self.id
330
331 # Find a request frame and its associated IPs
332 src, dst = None, None
333 for substream in self.substreams:
334 if 'http2.request.full_uri' in substream.raw_http2_substream: # This is a request
335 src, dst = substream.src_ip_port, substream.dst_ip_port
336 break
337 if not (src and dst):
338 LOGGER.warning(
339 f"Ignoring HTTP2 stream {self.id} which is lacking a request, "
340 f"substreams types = {[ss.http2_type for ss in self.substreams]}"
341 )
342 return
343 assert src != dst, (self.id, src)
344
345 # Create the request and response objects with their associated substreams
346 req_substreams = [substream for substream in self.substreams if substream.src_ip_port == src]
347 resp_substreams = [substream for substream in self.substreams if substream.src_ip_port == dst]
348 assert len(req_substreams) + len(resp_substreams) == len(self.substreams), (
349 self.id, len(self.substreams), len(req_substreams), len(resp_substreams)
350 )
351 self.request = Http2Request(req_substreams)
352 self.response = Http2Response(resp_substreams) # may be empty
353
354 def __str__(self):
355 return (
356 f'TCP Stream: {self.tcp_stream_id}, '
357 f'HTTP2 Stream: {self.http2_stream_id}'
358 f'\n{self.request}'
359 f'\n{self.response}'
360 )
361
362
[docs]
363class Http2Helper:
364
370
[docs]
371 @staticmethod
372 def substream_is_data(substream: Http2Substream) -> bool:
373 """Returns whether substream is a data substream."""
374 stream_type = substream.http2_type
375 return stream_type == 0
376
399
[docs]
400 @staticmethod
401 def to_har(message: Http2RequestResponse) -> dict[str, Any]:
402 """
403 Convert the HTTP2 request or response to a HAR entry.
404
405 <!> Some HTTP2 responses are missing
406
407 :param message: the HTTP2 request or response to be converted
408 :return: the HAR entry for the HTTP2 request or response
409 """
410 entry = {
411 '_timestamp': message.timestamp if message else None,
412 '_rawFramesNumbers': message.frames_nbs,
413 'httpVersion': message.http_version,
414 'cookies': [],
415 'headers': message.headers,
416 'headersSize': message.header_length,
417 'bodySize': message.body_length,
418 }
419 if message:
420 entry['_communication'] = get_har_communication(message)
421 if isinstance(message, Http2Request):
422 entry |= {
423 'method': message.http_method,
424 'url': message.uri,
425 'queryString': [],
426 }
427 if message.data.size:
428 message.data.update_har_request(entry, message.content_type)
429 else:
430 entry |= {
431 'status': message.http_status,
432 'statusText': '',
433 'redirectURL': '',
434 }
435 message.data.update_har_response(entry, message.content_type)
436 return entry
437
[docs]
438 @staticmethod
439 def get_data(data_substreams: Sequence[Http2Substream]) -> Payload:
440 """
441 Extract the data from the substreams (precondition: all substreams are data substreams).
442
443 :param data_substreams: the data substreams to be analyzed
444 :return: the reassembled data
445 """
446 return Http2Stream.get_raw_data([ss.raw_http2_substream for ss in data_substreams])
447
[docs]
448 @classmethod
449 def get_headers_and_data(cls, substreams: Sequence[Http2Substream]):
450 """
451 Identify the headers and data substreams and return them.
452
453 The substreams are identified by their types:
454 - Headers substream: type 1
455 - Data substream: type 0
456 We ignore the rest of the substreams.
457
458 Note that (flag & 0x01) identify the end of stream, usually it happens for a data-stream
459 but it may also happen for a header-stream (trailers in gRPC),
460 or even never happen.
461
462 :param substreams: the substreams of a HTTP2 stream
463 :return: the headers and data substreams regardless if it is a request or a response
464 """
465 headers: list[NameValueDict] = []
466 headers_streams: list[Http2Substream] = []
467 data_streams: list[Http2Substream] = []
468
469 for substream in substreams:
470 # Parse headers (HTTP2 substream marked as headers)
471 if cls.substream_is_header(substream):
472 headers_streams.append(substream)
473 headers += Http2Helper.get_headers(substream)
474 # Register data substreams
475 if cls.substream_is_data(substream):
476 data_streams.append(substream)
477
478 if substreams:
479 assert headers_streams, (len(substreams), data_streams)
480
481 return headers, Http2Helper.get_data(data_streams), headers_streams, data_streams
482
483
[docs]
484class Http2Traffic:
485 """
486 Class to represent the HTTP2 traffic. It contains the HTTP2 streams and the parsed traffic data.
487
488 In HTTP/2, frames are the smallest unit of communication.
489 Each frame has a specific type and can have associated flags.
490
491 **HTTP/2 frame types and flags:**
492
493
494 HTTP/2 Frame Types:
495
496 - `DATA (0x0)`: carries arbitrary, variable-length sequences of octets associated with a stream.
497 - `HEADERS (0x1)`: used to open a stream and carry a header block fragment.
498 - `PRIORITY (0x2)`: specifies the sender-advised priority of a stream.
499 - `RST_STREAM (0x3)`: abruptly terminates a stream.
500 - `SETTINGS (0x4)`: used to communicate configuration parameters.
501 - `PUSH_PROMISE (0x5)`: used to notify the peer endpoint in advance of streams the sender intends to initiate.
502 - `PING (0x6)`: used to measure round-trip time and ensure the connection is still active.
503 - `GOAWAY (0x7)`: informs the peer to stop creating streams on this connection.
504 - `WINDOW_UPDATE (0x8)`: used to implement flow control.
505 - `CONTINUATION (0x9)`: used to continue a sequence of header block fragments.
506
507 HTTP/2 Frame Flags:
508
509 - `END_STREAM (0x1)`: indicates that the frame is the last one for the current stream.
510 - `END_HEADERS (0x4)`: indicates that the frame contains the entire header block.
511 - `PADDED (0x8)`: indicates that the frame contains padding.
512 - `PRIORITY (0x20)`: indicates that the frame contains priority information.
513
514 **TCP stream ID and the HTTP/2 stream ID**
515 The TCP stream ID identifies a unique TCP connection. Each TCP connection is assigned a unique stream ID,
516 which is used to track the packets that belong to that connection.
517 The HTTP/2 stream ID, within a single TCP connection, multiple HTTP/2 streams can exist. Each HTTP/2 stream is
518 identified by a unique stream ID within the context of that TCP connection. These stream IDs are used to
519 multiplex multiple HTTP/2 requests and responses over a single TCP connection.
520
521 A single TCP stream (connection) can contain multiple HTTP/2 streams. Each HTTP/2 stream is
522 uniquely identified within the context of its TCP stream. The combination of the TCP stream ID and the
523 HTTP/2 stream ID uniquely identifies an HTTP/2 stream within the network traffic.
524 """
525 def __init__(self, traffic: Sequence[DictLayers]):
526 self.traffic = traffic
527 self.stream_pairs: dict[tuple[int, int], Http2Stream] = {}
528 self.parse_traffic()
529
[docs]
530 def parse_traffic(self) -> None:
531 """
532 Parse the traffic and extract the HTTP2 streams. It creates a dictionary for each HTTP2 stream.
533 Each key is a tuple with the TCP stream ID and the HTTP2 stream ID.
534
535 Identify each HTTP2 request and its associated HTTP2 response by following these steps:
536
537 1. Iterate through packets: it loops through all packets obtained from the `traffic` object.
538 2. Extract protocols: for each packet, it extracts the protocols from the `frame.protocols` field.
539 3. Check for HTTP2 protocol: it checks if the packet contains the `http2` protocol.
540 4. Extract the TCP stream ID: it retrieves the TCP stream ID from the `tcp.stream` field.
541 5. Handle HTTP2 layer: it ensures the `http2` layer is a list of HTTP2 stream objects.
542 6. Process each HTTP2 stream: for each HTTP2 stream in the `http2` layer:
543
544 - extract stream information: it retrieves the stream type and stream ID.
545 - filter relevant streams: it ignores streams that are not data (type 0) or headers (type 1).
546 - create or update stream pair: it creates a new tuple of `(tcp_stream_id, http2_stream_id)` if it does not
547 exist and appends the substream to the list.
548 7. Process streams: after assembling the HTTP2 streams, it processes each stream to create the request and
549 response objects.
550 """
551 # Assemble the HTTP2 streams
552 for layers in self.traffic:
553 # Ignore non-http2 packets
554 if 'http2' not in get_protocols(layers):
555 continue
556 tcp_stream_id = get_tcp_stream_id(layers)
557 community_id = get_community_id(layers)
558
559 # HTTP2 layer can be a list of streams or a single stream, force a list
560 http2_layer: list[dict[str, Any]] = layers['http2']
561 if not isinstance(http2_layer, list):
562 http2_layer = [layers['http2']]
563
564 for http2_layer_stream in http2_layer:
565 stream = http2_layer_stream['http2.stream']
566 assert isinstance(stream, dict), type(stream)
567 http2_frame_type = int(stream.get('http2.type', -1))
568 # Ignore streams that are not data or headers
569 if http2_frame_type not in {0, 1}:
570 continue
571 # <!> Edge-case: reassembled body is at top-level instead of nested in its stream
572 if 'http2.body.fragments' in http2_layer_stream:
573 assert 'http2.body.fragments' not in stream, http2_layer_stream
574 stream['http2_layer_stream'] = http2_layer_stream.pop('http2.body.fragments')
575 # Create a new tuple of (tcp_stream_id, http2_stream_id) if it does not exist
576 http2_stream_id = int(stream['http2.streamid'])
577 sid = (tcp_stream_id, http2_stream_id)
578 if sid not in self.stream_pairs:
579 self.stream_pairs[sid] = Http2Stream(*sid, community_id=community_id)
580 else:
581 assert community_id == self.stream_pairs[sid].community_id, (community_id, self.stream_pairs[sid].community_id)
582 # Append the substream to the list
583 self.stream_pairs[sid].append(stream, layers)
584
585 # Process the streams, once for all
586 for http2_stream in self.stream_pairs.values():
587 http2_stream.process()
588
[docs]
589 def get_http2_streams(self):
590 return list(self.stream_pairs.values())
591
[docs]
592 def get_har_entries(self) -> list[HarEntry]:
593 """
594 Convert the HTTP2 traffic to HTTP Archive (HAR) format.
595
596 :return: the HTTP2 traffic in HAR format
597 """
598 entries = []
599 for stream in self.get_http2_streams():
600 har_entry = stream.har_entry()
601 if har_entry:
602 entries.append(har_entry)
603 return entries