FlowAnalyzer 0.1.4__tar.gz → 0.1.6__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- FlowAnalyzer-0.1.6/FlowAnalyzer/FlowAnalyzer.py +179 -0
- {FlowAnalyzer-0.1.4 → FlowAnalyzer-0.1.6}/FlowAnalyzer.egg-info/PKG-INFO +1 -1
- {FlowAnalyzer-0.1.4 → FlowAnalyzer-0.1.6}/PKG-INFO +1 -1
- {FlowAnalyzer-0.1.4 → FlowAnalyzer-0.1.6}/setup.py +1 -1
- FlowAnalyzer-0.1.4/FlowAnalyzer/FlowAnalyzer.py +0 -162
- {FlowAnalyzer-0.1.4 → FlowAnalyzer-0.1.6}/FlowAnalyzer/__init__.py +0 -0
- {FlowAnalyzer-0.1.4 → FlowAnalyzer-0.1.6}/FlowAnalyzer.egg-info/SOURCES.txt +0 -0
- {FlowAnalyzer-0.1.4 → FlowAnalyzer-0.1.6}/FlowAnalyzer.egg-info/dependency_links.txt +0 -0
- {FlowAnalyzer-0.1.4 → FlowAnalyzer-0.1.6}/FlowAnalyzer.egg-info/top_level.txt +0 -0
- {FlowAnalyzer-0.1.4 → FlowAnalyzer-0.1.6}/LICENSE +0 -0
- {FlowAnalyzer-0.1.4 → FlowAnalyzer-0.1.6}/README.md +0 -0
- {FlowAnalyzer-0.1.4 → FlowAnalyzer-0.1.6}/setup.cfg +0 -0
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import re
|
|
3
|
+
import json
|
|
4
|
+
import gzip
|
|
5
|
+
import contextlib
|
|
6
|
+
import subprocess
|
|
7
|
+
from typing import Tuple
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class FlowAnalyzer:
|
|
11
|
+
"""FlowAnalyzer是一个流量分析器,用于解析和处理tshark导出的JSON数据文件"""
|
|
12
|
+
|
|
13
|
+
def __init__(self, jsonPath: str):
|
|
14
|
+
"""初始化FlowAnalyzer对象
|
|
15
|
+
|
|
16
|
+
Parameters
|
|
17
|
+
----------
|
|
18
|
+
jsonPath : str
|
|
19
|
+
tshark导出的JSON文件路径
|
|
20
|
+
"""
|
|
21
|
+
self.jsonPath = jsonPath
|
|
22
|
+
self.check_json_file()
|
|
23
|
+
|
|
24
|
+
def check_json_file(self):
|
|
25
|
+
# sourcery skip: remove-redundant-fstring, replace-interpolation-with-fstring
|
|
26
|
+
"""检查JSON文件是否存在并非空
|
|
27
|
+
|
|
28
|
+
Raises
|
|
29
|
+
------
|
|
30
|
+
FileNotFoundError
|
|
31
|
+
当JSON文件不存在时抛出异常
|
|
32
|
+
ValueError
|
|
33
|
+
当JSON文件内容为空时抛出异常
|
|
34
|
+
"""
|
|
35
|
+
if not os.path.exists(self.jsonPath):
|
|
36
|
+
raise FileNotFoundError(
|
|
37
|
+
f"您的tshark导出的JSON文件没有找到!JSON路径:%s" % self.jsonPath)
|
|
38
|
+
|
|
39
|
+
if os.path.getsize(self.jsonPath) == 0:
|
|
40
|
+
raise ValueError("您的tshark导出的JSON文件内容为空!JSON路径:%s" % self.jsonPath)
|
|
41
|
+
|
|
42
|
+
def parse_http_json(self) -> Tuple[dict, list]:
|
|
43
|
+
# sourcery skip: use-named-expression
|
|
44
|
+
"""解析JSON数据文件中的HTTP请求和响应信息
|
|
45
|
+
|
|
46
|
+
Returns
|
|
47
|
+
-------
|
|
48
|
+
tuple
|
|
49
|
+
包含请求字典和响应列表的元组
|
|
50
|
+
"""
|
|
51
|
+
with open(self.jsonPath, "r") as f:
|
|
52
|
+
data = json.load(f)
|
|
53
|
+
|
|
54
|
+
request, response = {}, []
|
|
55
|
+
for packet in data:
|
|
56
|
+
packet = packet["_source"]["layers"]
|
|
57
|
+
time_epoch = float(packet["frame.time_epoch"][0]) if packet.get("frame.time_epoch") else None
|
|
58
|
+
full_request = packet["tcp.reassembled.data"][0] if packet.get("tcp.reassembled.data") else packet["tcp.payload"][0]
|
|
59
|
+
response_num = int(packet["frame.number"][0]) if packet.get("frame.number") else None
|
|
60
|
+
request_num = int(packet["http.request_in"][0]) if packet.get("http.request_in") else None
|
|
61
|
+
|
|
62
|
+
if request_num:
|
|
63
|
+
response.append({"response_num": response_num, "request_in": request_num, "full_request": full_request, "time_epoch": time_epoch})
|
|
64
|
+
else:
|
|
65
|
+
request[response_num] = {"full_request": full_request, "time_epoch": time_epoch}
|
|
66
|
+
return request, response
|
|
67
|
+
|
|
68
|
+
def generate_http_dict_pairs(self, preserve_http_headers=False, preserve_start_time=False):
|
|
69
|
+
"""生成HTTP请求和响应信息的字典对
|
|
70
|
+
|
|
71
|
+
Parameters
|
|
72
|
+
----------
|
|
73
|
+
preserve_http_headers : bool, optional
|
|
74
|
+
指是否保留HTTP除了请求体/返回体以外的所有HTTP头部信息,默认为False,只会返回HTTP请求体或者返回体
|
|
75
|
+
|
|
76
|
+
如下结构:
|
|
77
|
+
请求行/返回行:
|
|
78
|
+
请求行:指的是HTTP请求中的第一行,包含了HTTP方法、请求的资源路径和HTTP协议版本。例如:GET /index.html HTTP/1.1
|
|
79
|
+
返回行:指的是HTTP响应中的第一行,包含了HTTP协议版本、响应状态码和相应的状态描述。例如:HTTP/1.1 200 OK
|
|
80
|
+
请求头/返回头:
|
|
81
|
+
请求头:包含了HTTP请求的相关信息,以键值对的形式出现,每个键值对占据一行。常见的请求头包括Host、User-Agent、Content-Type等。例如:Host: example.com
|
|
82
|
+
返回头:包含了HTTP响应的相关信息,也是以键值对的形式出现,每个键值对占据一行。常见的返回头包括Content-Type、Content-Length、Server等。例如:Content-Type: text/html
|
|
83
|
+
空行:用于分隔请求头和请求体,由两个连续的回车换行符组成
|
|
84
|
+
请求体/返回体:
|
|
85
|
+
请求体:包含了HTTP请求中的实际数据部分,通常在POST请求中使用。请求体可以是表单数据、JSON数据等,格式取决于请求头中的Content-Type字段
|
|
86
|
+
返回体:包含了HTTP响应中的实际数据部分,通常是服务器返回给客户端的内容。返回体的格式和内容取决于具体的请求和服务器的处理逻辑
|
|
87
|
+
|
|
88
|
+
preserve_start_time : bool, optional
|
|
89
|
+
指是保留HTTP请求的时间,指的是开始时间
|
|
90
|
+
|
|
91
|
+
Yields
|
|
92
|
+
------
|
|
93
|
+
Iterator[dict]
|
|
94
|
+
包含请求和响应信息的字典迭代器
|
|
95
|
+
"""
|
|
96
|
+
request, response = self.parse_http_json()
|
|
97
|
+
for resp in response:
|
|
98
|
+
frame_num = resp["response_num"]
|
|
99
|
+
request_num = resp["request_in"]
|
|
100
|
+
requ = request.get(request_num)
|
|
101
|
+
|
|
102
|
+
dic = {"response": [frame_num, self.extract_http_file_data(resp['full_request'], preserve_http_headers)]}
|
|
103
|
+
dic["request"] = [request_num, self.extract_http_file_data(requ["full_request"], preserve_http_headers)] if requ else None
|
|
104
|
+
|
|
105
|
+
if preserve_start_time:
|
|
106
|
+
dic["response"].append(resp["time_epoch"])
|
|
107
|
+
dic["request"].append(requ["time_epoch"]) if requ else None
|
|
108
|
+
yield dic
|
|
109
|
+
|
|
110
|
+
@staticmethod
|
|
111
|
+
def get_json_data(filePath, display_filter):
|
|
112
|
+
"""获取JSON数据并保存至文件,保存目录是当前工作目录,也就是您运行脚本所在目录
|
|
113
|
+
|
|
114
|
+
Parameters
|
|
115
|
+
----------
|
|
116
|
+
filePath : str
|
|
117
|
+
待处理的数据文件路径
|
|
118
|
+
display_filter : str
|
|
119
|
+
Tshark的过滤器表达式
|
|
120
|
+
|
|
121
|
+
Returns
|
|
122
|
+
-------
|
|
123
|
+
str
|
|
124
|
+
保存JSON数据的文件路径
|
|
125
|
+
"""
|
|
126
|
+
# sourcery skip: use-fstring-for-formatting
|
|
127
|
+
# jsonPath = os.path.join(os.path.dirname(filePath), "output.json")
|
|
128
|
+
jsonPath = os.path.join(os.getcwd(), "output.json")
|
|
129
|
+
command = 'tshark -r {} -Y "{}" -T json -e http.request_in -e tcp.reassembled.data -e frame.number -e tcp.payload -e frame.time_epoch > {}'.format(
|
|
130
|
+
filePath, display_filter, jsonPath)
|
|
131
|
+
proc = subprocess.Popen(command, shell=True,
|
|
132
|
+
stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
|
133
|
+
proc.communicate()
|
|
134
|
+
return jsonPath
|
|
135
|
+
|
|
136
|
+
@staticmethod
|
|
137
|
+
def extract_http_file_data(full_request, preserve_http_headers=False):
|
|
138
|
+
# sourcery skip: merge-else-if-into-elif, swap-if-else-branches
|
|
139
|
+
"""提取HTTP请求或响应中的文件数据
|
|
140
|
+
|
|
141
|
+
Parameters
|
|
142
|
+
----------
|
|
143
|
+
full_request : str
|
|
144
|
+
HTTP请求或响应的消息体
|
|
145
|
+
preserve_http_headers : bool, optional
|
|
146
|
+
指是否保留HTTP除了请求体/返回体以外的所有HTTP头部信息,默认为False,只会返回HTTP请求体或者返回体
|
|
147
|
+
|
|
148
|
+
如下结构:
|
|
149
|
+
请求行/返回行:
|
|
150
|
+
请求行:指的是HTTP请求中的第一行,包含了HTTP方法、请求的资源路径和HTTP协议版本。例如:GET /index.html HTTP/1.1
|
|
151
|
+
返回行:指的是HTTP响应中的第一行,包含了HTTP协议版本、响应状态码和相应的状态描述。例如:HTTP/1.1 200 OK
|
|
152
|
+
请求头/返回头:
|
|
153
|
+
请求头:包含了HTTP请求的相关信息,以键值对的形式出现,每个键值对占据一行。常见的请求头包括Host、User-Agent、Content-Type等。例如:Host: example.com
|
|
154
|
+
返回头:包含了HTTP响应的相关信息,也是以键值对的形式出现,每个键值对占据一行。常见的返回头包括Content-Type、Content-Length、Server等。例如:Content-Type: text/html
|
|
155
|
+
空行:用于分隔请求头和请求体,由两个连续的回车换行符组成
|
|
156
|
+
请求体/返回体:
|
|
157
|
+
请求体:包含了HTTP请求中的实际数据部分,通常在POST请求中使用。请求体可以是表单数据、JSON数据等,格式取决于请求头中的Content-Type字段
|
|
158
|
+
返回体:包含了HTTP响应中的实际数据部分,通常是服务器返回给客户端的内容。返回体的格式和内容取决于具体的请求和服务器的处理逻辑
|
|
159
|
+
|
|
160
|
+
Returns
|
|
161
|
+
-------
|
|
162
|
+
bytes
|
|
163
|
+
HTTP的bytes类型,如果有Gzip数据会自动解压缩
|
|
164
|
+
"""
|
|
165
|
+
full_request = bytes.fromhex(full_request)
|
|
166
|
+
num = full_request.find(b"\r\n\r\n")
|
|
167
|
+
header = full_request[:num]
|
|
168
|
+
|
|
169
|
+
if full_request.endswith(b"\r\n\r\n"):
|
|
170
|
+
# 判断是否有file_data,没有的话就为b""空字符串
|
|
171
|
+
# 由于是多个tcp所以需要去除应该是长度的字节 不确定是不是4个字节 后期可能出现bug
|
|
172
|
+
ret = re.findall(b'^\r\n\r\n[0-9a-f]{1,}\r\n(.*)\r\n\x30\r\n\r\n$', full_request[num:], flags=re.DOTALL)
|
|
173
|
+
file_data = re.sub(b"\r\n[0-9a-f]{1,}\r\n", b"", ret[0]) if ret != [] else b""
|
|
174
|
+
else:
|
|
175
|
+
file_data = full_request[num+4:]
|
|
176
|
+
|
|
177
|
+
with contextlib.suppress(Exception):
|
|
178
|
+
file_data = gzip.decompress(file_data)
|
|
179
|
+
return header + b"\r\n\r\n" + file_data if preserve_http_headers else file_data
|
|
@@ -6,7 +6,7 @@ with open(os.path.join(os.path.dirname(__file__), "README.md"), encoding="utf-8"
|
|
|
6
6
|
|
|
7
7
|
setup(
|
|
8
8
|
name="FlowAnalyzer",
|
|
9
|
-
version="0.1.
|
|
9
|
+
version="0.1.6",
|
|
10
10
|
description="FlowAnalyzer是一个流量分析器,用于解析和处理tshark导出的JSON数据文件",
|
|
11
11
|
author="Byxs20",
|
|
12
12
|
author_email="97766819@qq.com",
|
|
@@ -1,162 +0,0 @@
|
|
|
1
|
-
import os
|
|
2
|
-
import re
|
|
3
|
-
import json
|
|
4
|
-
import gzip
|
|
5
|
-
import contextlib
|
|
6
|
-
import subprocess
|
|
7
|
-
from typing import Tuple
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
class FlowAnalyzer:
|
|
11
|
-
"""FlowAnalyzer是一个流量分析器,用于解析和处理tshark导出的JSON数据文件"""
|
|
12
|
-
|
|
13
|
-
def __init__(self, jsonPath: str):
|
|
14
|
-
"""初始化FlowAnalyzer对象
|
|
15
|
-
|
|
16
|
-
Parameters
|
|
17
|
-
----------
|
|
18
|
-
jsonPath : str
|
|
19
|
-
tshark导出的JSON文件路径
|
|
20
|
-
"""
|
|
21
|
-
self.jsonPath = jsonPath
|
|
22
|
-
self.check_json_file()
|
|
23
|
-
|
|
24
|
-
def check_json_file(self):
|
|
25
|
-
# sourcery skip: remove-redundant-fstring, replace-interpolation-with-fstring
|
|
26
|
-
"""检查JSON文件是否存在并非空
|
|
27
|
-
|
|
28
|
-
Raises
|
|
29
|
-
------
|
|
30
|
-
FileNotFoundError
|
|
31
|
-
当JSON文件不存在时抛出异常
|
|
32
|
-
ValueError
|
|
33
|
-
当JSON文件内容为空时抛出异常
|
|
34
|
-
"""
|
|
35
|
-
if not os.path.exists(self.jsonPath):
|
|
36
|
-
raise FileNotFoundError(
|
|
37
|
-
f"您的tshark导出的JSON文件没有找到!JSON路径:%s" % self.jsonPath)
|
|
38
|
-
|
|
39
|
-
if os.path.getsize(self.jsonPath) == 0:
|
|
40
|
-
raise ValueError("您的tshark导出的JSON文件内容为空!JSON路径:%s" % self.jsonPath)
|
|
41
|
-
|
|
42
|
-
def parse_http_json(self) -> Tuple[dict, list]:
|
|
43
|
-
"""解析JSON数据文件中的HTTP请求和响应信息
|
|
44
|
-
|
|
45
|
-
Returns
|
|
46
|
-
-------
|
|
47
|
-
tuple
|
|
48
|
-
包含请求字典和响应列表的元组
|
|
49
|
-
"""
|
|
50
|
-
with open(self.jsonPath, "r") as f:
|
|
51
|
-
data = json.load(f)
|
|
52
|
-
|
|
53
|
-
request, response = {}, []
|
|
54
|
-
for packet in data:
|
|
55
|
-
packet = packet["_source"]["layers"]
|
|
56
|
-
time_epoch = packet.get("frame.time_epoch")
|
|
57
|
-
full_request = packet.get(
|
|
58
|
-
"tcp.reassembled.data") or packet["tcp.payload"]
|
|
59
|
-
|
|
60
|
-
if packet.get("http.request_in"):
|
|
61
|
-
response.append({"response_num": packet["frame.number"][0], "request_in": packet["http.request_in"]
|
|
62
|
-
[0], "full_request": full_request[0], "time_epoch": float(time_epoch[0])})
|
|
63
|
-
else:
|
|
64
|
-
request[packet["frame.number"][0]] = {
|
|
65
|
-
"full_request": full_request[0], "time_epoch": float(time_epoch[0])}
|
|
66
|
-
return request, response
|
|
67
|
-
|
|
68
|
-
def generate_http_dict_pairs(self, http_header=False, time_epoch=False):
|
|
69
|
-
"""生成HTTP请求和响应信息的字典对
|
|
70
|
-
|
|
71
|
-
Parameters
|
|
72
|
-
----------
|
|
73
|
-
http_header : bool, optional
|
|
74
|
-
指是否提取HTTP请求的全部,默认为False,表示只提取请求体,否则返回整个HTTP请求,如下结构:
|
|
75
|
-
请求行:请求方法、请求目标和 HTTP 协议版本组成,例如 POST /login.php HTTP/1.1。
|
|
76
|
-
请求头:包含了各种请求的元数据,比如 Host、Content-Length、User-Agent 等。每个请求头都由一个字段名和对应的值组成,以冒号分隔,例如 Host: 192.168.52.176。
|
|
77
|
-
空行:用于分隔请求头和请求体,由两个连续的回车换行符组成。
|
|
78
|
-
请求体:包含了请求的实际数据,对于 POST 请求来说,请求体通常包含表单数据或其他数据,以便发送给服务器进行处理。
|
|
79
|
-
time_epoch : bool, optional
|
|
80
|
-
指是否提取HTTP请求的时间,指的是开始时间
|
|
81
|
-
|
|
82
|
-
Yields
|
|
83
|
-
------
|
|
84
|
-
Iterator[dict]
|
|
85
|
-
包含请求和响应信息的字典迭代器
|
|
86
|
-
"""
|
|
87
|
-
request, response = self.parse_http_json()
|
|
88
|
-
for resp in response:
|
|
89
|
-
frame_num = resp["response_num"]
|
|
90
|
-
request_num = resp["request_in"]
|
|
91
|
-
requ = request.get(request_num)
|
|
92
|
-
|
|
93
|
-
dic = {"response": [frame_num, self.extract_http_file_data(resp['full_request'], http_header)]}
|
|
94
|
-
dic["request"] = [request_num, self.extract_http_file_data(requ["full_request"], http_header)] if requ else None
|
|
95
|
-
|
|
96
|
-
if time_epoch:
|
|
97
|
-
dic["response"].append(resp["time_epoch"])
|
|
98
|
-
dic["request"].append(requ["time_epoch"]) if requ else None
|
|
99
|
-
yield dic
|
|
100
|
-
|
|
101
|
-
@staticmethod
|
|
102
|
-
def get_json_data(filePath, display_filter):
|
|
103
|
-
"""获取JSON数据并保存至文件,保存目录是当前工作目录,也就是您运行脚本所在目录
|
|
104
|
-
|
|
105
|
-
Parameters
|
|
106
|
-
----------
|
|
107
|
-
filePath : str
|
|
108
|
-
待处理的数据文件路径
|
|
109
|
-
display_filter : str
|
|
110
|
-
Tshark的过滤器表达式
|
|
111
|
-
|
|
112
|
-
Returns
|
|
113
|
-
-------
|
|
114
|
-
str
|
|
115
|
-
保存JSON数据的文件路径
|
|
116
|
-
"""
|
|
117
|
-
# sourcery skip: use-fstring-for-formatting
|
|
118
|
-
# jsonPath = os.path.join(os.path.dirname(filePath), "output.json")
|
|
119
|
-
jsonPath = os.path.join(os.getcwd(), "output.json")
|
|
120
|
-
command = 'tshark -r {} -Y "{}" -T json -e http.request_in -e tcp.reassembled.data -e frame.number -e tcp.payload -e frame.time_epoch > {}'.format(
|
|
121
|
-
filePath, display_filter, jsonPath)
|
|
122
|
-
proc = subprocess.Popen(command, shell=True,
|
|
123
|
-
stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
|
124
|
-
proc.communicate()
|
|
125
|
-
return jsonPath
|
|
126
|
-
|
|
127
|
-
@staticmethod
|
|
128
|
-
def extract_http_file_data(full_request, http_header=False):
|
|
129
|
-
# sourcery skip: merge-else-if-into-elif, swap-if-else-branches
|
|
130
|
-
"""提取HTTP请求或响应中的文件数据
|
|
131
|
-
|
|
132
|
-
Parameters
|
|
133
|
-
----------
|
|
134
|
-
full_request : str
|
|
135
|
-
HTTP请求或响应的消息体
|
|
136
|
-
http_header : bool, optional
|
|
137
|
-
指是否提取HTTP请求的全部,默认为False,表示只提取请求体,否则返回整个HTTP请求,如下结构:
|
|
138
|
-
请求行:请求方法、请求目标和 HTTP 协议版本组成,例如 POST /login.php HTTP/1.1。
|
|
139
|
-
请求头:包含了各种请求的元数据,比如 Host、Content-Length、User-Agent 等。每个请求头都由一个字段名和对应的值组成,以冒号分隔,例如 Host: 192.168.52.176。
|
|
140
|
-
空行:用于分隔请求头和请求体,由两个连续的回车换行符组成。
|
|
141
|
-
请求体:包含了请求的实际数据,对于 POST 请求来说,请求体通常包含表单数据或其他数据,以便发送给服务器进行处理。
|
|
142
|
-
|
|
143
|
-
Returns
|
|
144
|
-
-------
|
|
145
|
-
bytes
|
|
146
|
-
HTTP的bytes类型,如果有Gzip数据会自动解压缩
|
|
147
|
-
"""
|
|
148
|
-
full_request = bytes.fromhex(full_request)
|
|
149
|
-
num = full_request.find(b"\r\n\r\n")
|
|
150
|
-
header = full_request[:num]
|
|
151
|
-
|
|
152
|
-
if full_request.endswith(b"\r\n\r\n"):
|
|
153
|
-
ret = re.findall(b'^\r\n\r\n.*?\r\n(.*)\r\n.*?\r\n\r\n$', full_request[num:], flags=re.DOTALL)
|
|
154
|
-
# 判断是否有file_data,没有的话就为b""空字符串
|
|
155
|
-
# 由于是多个tcp所以需要去除应该是长度的字节 不确定是不是4个字节 后期可能出现bug
|
|
156
|
-
file_data = re.sub(b"\r\n.{4}\r\n", b"", ret[0]) if ret != [] else b""
|
|
157
|
-
else:
|
|
158
|
-
file_data = full_request[num+4:]
|
|
159
|
-
|
|
160
|
-
with contextlib.suppress(Exception):
|
|
161
|
-
file_data = gzip.decompress(file_data)
|
|
162
|
-
return header + b"\r\n\r\n" + file_data if http_header else file_data
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|