Repository navigation
Expand file tree
/
Copy pathclient.py
More file actions
400 lines (332 loc) · 13.2 KB
/
Copy pathclient.py
File metadata and controls
400 lines (332 loc) · 13.2 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
HTTP客户端
发送HTTP请求,支持指定case-id header
"""
import json
import requests
import argparse
import os
from typing import Optional, Dict, Any
from case_manager import CaseManager
class HTTPClient:
"""HTTP客户端类"""
def __init__(self, base_url: str = 'http://localhost:32941'):
self.base_url = base_url
self.case_id_header = 'X-Case-ID'
def send_request(
self,
case_id: str,
method: str = 'GET',
path: str = '/',
headers: Optional[Dict[str, str]] = None,
data: Optional[Any] = None,
json_data: Optional[Dict] = None
) -> requests.Response:
"""
发送HTTP请求
Args:
case_id: 用例ID,会添加到header中
method: HTTP方法 (GET, POST, PUT, DELETE等)
path: 请求路径
headers: 额外的请求头
data: 请求体(字符串或字节)
json_data: JSON格式的请求体
Returns:
requests.Response对象
"""
url = f"{self.base_url.rstrip('/')}/{path.lstrip('/')}"
# 构建请求头
request_headers = headers.copy() if headers else {}
request_headers[self.case_id_header] = case_id
# 发送请求
response = requests.request(
method=method.upper(),
url=url,
headers=request_headers,
data=data,
json=json_data,
timeout=30
)
return response
def send_from_case(self, case_id: str, request_data: Dict[str, Any]) -> requests.Response:
"""
根据用例中的请求数据发送请求
Args:
case_id: 用例ID
request_data: 包含请求信息的字典,格式:
{
'method': 'GET',
'path': '/api/users',
'headers': {...},
'body': '...' 或 {...}
}
Returns:
requests.Response对象
"""
method = request_data.get('method', 'GET')
path = request_data.get('path', '/')
headers = request_data.get('headers', {})
body = request_data.get('body', None)
# 处理body
json_data = None
data = None
if body:
if isinstance(body, dict):
json_data = body
elif isinstance(body, str):
# 尝试解析JSON字符串
try:
json_data = json.loads(body)
except json.JSONDecodeError:
data = body
return self.send_request(
case_id=case_id,
method=method,
path=path,
headers=headers,
data=data,
json_data=json_data
)
def main():
"""命令行入口"""
parser = argparse.ArgumentParser(description='HTTP客户端工具')
parser.add_argument('case_id', nargs='?', help='用例ID(可选,不指定时重放所有用例)')
parser.add_argument('--method', '-m', help='HTTP方法(仅当指定单个用例时有效)')
parser.add_argument('--path', '-p', help='请求路径(仅当指定单个用例时有效)')
parser.add_argument('--header', '-H', action='append', help='请求头,格式: Key:Value(仅当指定单个用例时有效)')
parser.add_argument('--data', '-d', help='请求体数据(仅当指定单个用例时有效)')
parser.add_argument('--json', '-j', help='JSON格式的请求体(仅当指定单个用例时有效)')
parser.add_argument('--url', '-u', help='服务器完整URL (例如: http://localhost:23941)')
parser.add_argument('--host', default='localhost', help='服务器IP地址 (默认: localhost)')
parser.add_argument('--port', type=int, default=23941, help='服务器端口 (默认: 23941)')
parser.add_argument('--pretty', action='store_true', help='美化输出JSON响应')
parser.add_argument('--cases', '-c', help='用例文件或文件夹路径(用于加载所有用例,默认: cases.yaml和cases/)')
args = parser.parse_args()
# 构建服务器URL
if args.url:
# 如果指定了完整URL,使用它
server_url = args.url
else:
# 否则使用host和port构建URL
server_url = f'http://{args.host}:{args.port}'
# 创建客户端
client = HTTPClient(base_url=server_url)
# 如果没有指定case_id,重放所有用例
if not args.case_id:
return replay_all_cases(client, server_url, args)
# 指定了单个用例,执行单个请求
return send_single_request(client, server_url, args)
def send_single_request(client, server_url, args):
"""发送单个用例请求"""
# 解析请求头
headers = {}
if args.header:
for h in args.header:
if ':' in h:
key, value = h.split(':', 1)
headers[key.strip()] = value.strip()
# 解析JSON数据
json_data = None
if args.json:
try:
json_data = json.loads(args.json)
except json.JSONDecodeError as e:
print(f"Error parsing JSON: {e}")
return 1
# 使用命令行参数或从用例文件加载请求信息
method = args.method or 'GET'
path = args.path or '/'
# 尝试从用例文件加载请求信息
case_manager = CaseManager()
if args.cases:
case_manager.load_cases(args.cases)
else:
# 默认加载cases.yaml和cases/文件夹
case_manager.load_cases('cases.yaml')
if os.path.exists('cases') and os.path.isdir('cases'):
case_manager.load_cases('cases')
case = case_manager.get_case(args.case_id)
if case and case.get('request'):
# 使用用例文件中的请求信息
request_data = case['request']
method = request_data.get('method', method)
path = request_data.get('path', path)
# 合并用例中的headers和命令行指定的headers
case_headers = request_data.get('headers', {})
case_headers.update(headers)
headers = case_headers
# 使用用例中的body
case_body = request_data.get('body')
if case_body is not None:
if isinstance(case_body, dict):
json_data = case_body
elif isinstance(case_body, str):
try:
json_data = json.loads(case_body)
except json.JSONDecodeError:
json_data = None
args.data = case_body
# 构建完整的请求信息
url = f"{server_url.rstrip('/')}/{path.lstrip('/')}"
request_headers = headers.copy() if headers else {}
request_headers[client.case_id_header] = args.case_id
# 解析URL获取host和path
from urllib.parse import urlparse
parsed_url = urlparse(server_url)
host = parsed_url.netloc or 'localhost'
path = path if path.startswith('/') else '/' + path
# 构建请求体
request_body_str = None
if json_data:
request_body_str = json.dumps(json_data, ensure_ascii=False) if not args.pretty else json.dumps(json_data, indent=2, ensure_ascii=False)
elif args.data:
request_body_str = str(args.data)
# 获取用例描述
case = case_manager.get_case(args.case_id)
case_description = case_manager.get_case_description(args.case_id) if case else args.case_id
# 显示请求信息(BurpSuite风格)
print("\n" + "=" * 80)
print(f"REQUEST [{case_description}]")
print("=" * 80)
# 请求行
print(f"{method.upper()} {path} HTTP/1.1")
# 请求头(按BurpSuite顺序)
# 先显示Host
if 'Host' in request_headers:
print(f"Host: {request_headers['Host']}")
else:
print(f"Host: {host}")
# 显示其他请求头
for key, value in request_headers.items():
if key.lower() != 'host':
print(f"{key}: {value}")
# 空行分隔header和body
print()
# 请求体
if request_body_str:
print(request_body_str)
print("=" * 80)
print("RESPONSE")
print("=" * 80)
try:
# 发送请求
response = client.send_request(
case_id=args.case_id,
method=method,
path=path,
headers=headers,
data=args.data,
json_data=json_data
)
# 状态行
status_text_map = {
200: 'OK', 201: 'Created', 202: 'Accepted', 204: 'No Content',
301: 'Moved Permanently', 302: 'Found', 304: 'Not Modified',
400: 'Bad Request', 401: 'Unauthorized', 403: 'Forbidden',
404: 'Not Found', 405: 'Method Not Allowed', 500: 'Internal Server Error',
502: 'Bad Gateway', 503: 'Service Unavailable'
}
status_text = status_text_map.get(response.status_code, 'OK' if 200 <= response.status_code < 300 else 'Error')
print(f"HTTP/1.1 {response.status_code} {status_text}")
# 响应头
for key, value in response.headers.items():
print(f"{key}: {value}")
# 空行分隔header和body
print()
# 响应体
try:
response_json = response.json()
if args.pretty:
print(json.dumps(response_json, indent=2, ensure_ascii=False))
else:
print(json.dumps(response_json, ensure_ascii=False))
except (ValueError, json.JSONDecodeError):
# 不是JSON格式,直接输出文本
print(response.text)
print("=" * 80 + "\n")
return 0
except requests.exceptions.RequestException as e:
print(f"HTTP/1.1 000 Connection Failed")
print()
print(f"Request failed: {e}")
print("=" * 80 + "\n")
return 1
def replay_all_cases(client, server_url, args):
"""重放所有用例"""
# 加载用例
case_manager = CaseManager()
if args.cases:
case_manager.load_cases(args.cases)
else:
# 默认加载cases.yaml和cases/文件夹
case_manager.load_cases('cases.yaml')
if os.path.exists('cases') and os.path.isdir('cases'):
case_manager.load_cases('cases')
case_ids = case_manager.list_cases()
if not case_ids:
print("No cases found. Please check your case files.")
return 1
print(f"\nFound {len(case_ids)} case(s). Starting replay...\n")
print("=" * 80)
success_count = 0
fail_count = 0
for case_id in sorted(case_ids):
case = case_manager.get_case(case_id)
if not case or not case.get('request'):
case_description = case_manager.get_case_description(case_id)
print(f"\n[SKIP] {case_description}: No request data found")
continue
request_data = case['request']
method = request_data.get('method', 'GET')
path = request_data.get('path', '/')
headers = request_data.get('headers', {})
body = request_data.get('body')
# 获取用例描述
case_description = case_manager.get_case_description(case_id)
# 准备请求数据
json_data = None
data = None
if body is not None:
if isinstance(body, dict):
json_data = body
elif isinstance(body, str):
try:
json_data = json.loads(body)
except json.JSONDecodeError:
data = body
# 发送请求
try:
response = client.send_request(
case_id=case_id,
method=method,
path=path,
headers=headers,
data=data,
json_data=json_data
)
# 显示结果
status_text_map = {
200: 'OK', 201: 'Created', 202: 'Accepted', 204: 'No Content',
301: 'Moved Permanently', 302: 'Found', 304: 'Not Modified',
400: 'Bad Request', 401: 'Unauthorized', 403: 'Forbidden',
404: 'Not Found', 405: 'Method Not Allowed', 500: 'Internal Server Error',
502: 'Bad Gateway', 503: 'Service Unavailable'
}
status_text = status_text_map.get(response.status_code, 'OK' if 200 <= response.status_code < 300 else 'Error')
status_icon = "✓" if 200 <= response.status_code < 300 else "✗"
print(f"[{status_icon}] {case_description}: {method.upper()} {path} -> {response.status_code} {status_text}")
if 200 <= response.status_code < 300:
success_count += 1
else:
fail_count += 1
except requests.exceptions.RequestException as e:
print(f"[✗] {case_description}: {method.upper()} {path} -> Failed: {e}")
fail_count += 1
print("=" * 80)
print(f"\nSummary: {success_count} succeeded, {fail_count} failed, {len(case_ids)} total")
print()
return 0 if fail_count == 0 else 1
if __name__ == '__main__':
exit(main())