Files
xiaoli-cli/plugins/network_tools.py
T
shuiyu1123 78fea7ccdd xiaoli-cli 7.0.0: 重构底层架构 + 移除工具截断
- 重构 core/ 底层 (7文件): app/runner/engine/parser/history/safety/display
- FC原生调用为主,prompt-JSON保底
- 移除所有工具返回截断 (ai_search/git_tools/cmd_executor/browser_auto/network_tools/sub_agent/tool_search)
- 删除无用文件 (旧服务器/WebUI/彩蛋/启动器等21个文件)
- 清理 constants.py 死代码
- 版本号更新为 7.0.0
2026-07-05 13:17:27 +08:00

296 lines
11 KiB
Python

import requests
import time
import socket
import ssl
import json
from urllib.parse import urlparse
from colorama import Fore, Style
class Liugin:
"""网络工具插件"""
_UA = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36'
def __init__(self):
self.cli = None
self.timeout = 10
self.max_retries = 2
def set_cli(self, cli):
self.cli = cli
self.cli.register_liugin_command('net', self.command_handler)
self.cli.register_liugin_command('ping', lambda a: self.handle(f"ping {a}"))
self.cli.register_liugin_command('website', lambda a: self.handle(f"status {a}"))
def command_handler(self, args):
parts = args.strip().split(maxsplit=1)
if not parts:
return "用法: network_tools <操作> <参数>"
return self.handle(f"{parts[0].lower()} {parts[1] if len(parts) > 1 else ''}")
def get_tool_info(self):
return {
"name": "network_tools",
"description": "网络工具:ping/get/status/headers/ip/dns/ssl/port/post/put/delete",
"keywords": ["网络", "ping", "get", "post", "dns", "ssl", "port", "ip", "headers", "status"],
"usage": (
"network_tools <操作> <参数>\n\n"
"读取: get/status/headers <URL>\n"
"诊断: ping/dns/ssl <域名>\n"
"扫描: port <域名> [端口范围]\n"
"请求: post/put/delete <URL> [JSON体]\n"
"解析: ip <域名>"
),
}
def get_mcp_definition(self):
return {
"name": "network_tools",
"description": "网络工具:读取网页、诊断连接、发送HTTP请求、DNS/SSL/端口扫描",
"inputSchema": {
"type": "object",
"properties": {
"operation": {
"type": "string",
"enum": ["ping", "get", "status", "headers", "ip", "dns", "ssl", "port", "post", "put", "delete"],
},
"target": {"type": "string", "description": "URL、域名或IP"},
"extra": {"type": "string", "description": "附加参数:JSON body 或端口范围"}
},
"required": ["operation", "target"]
}
}
def convert_mcp_args(self, arguments):
op = arguments.get("operation", "")
target = arguments.get("target", "")
extra = arguments.get("extra", "")
return f"{op} {target} {extra}".strip()
def handle(self, args):
try:
parts = args.strip().split(maxsplit=2)
if not parts:
return "参数不足"
op = parts[0].lower()
target = parts[1].strip() if len(parts) > 1 else ""
extra = parts[2].strip() if len(parts) > 2 else ""
if op == "post":
return self._api(target, "POST", extra)
elif op == "put":
return self._api(target, "PUT", extra)
elif op == "delete":
return self._api(target, "DELETE", extra)
elif op == "ping":
return self._ping(target)
elif op == "get":
return self._get(target)
elif op == "status":
return self._status(target)
elif op == "headers":
return self._headers(target)
elif op == "ip":
return self._ip(target)
elif op == "dns":
return self._dns(target)
elif op == "ssl":
return self._ssl(target)
elif op == "port":
return self._port(target, extra)
else:
return f"不支持: {op}。支持: ping/get/status/headers/ip/dns/ssl/port/post/put/delete"
except Exception as e:
return f"错误: {e}"
def _url(self, t):
return t if t.startswith(('http://', 'https://')) else 'https://' + t
def _req(self, url, method='GET', data=None, ct=None):
headers = {'User-Agent': self._UA, 'Accept': '*/*'}
if ct:
headers['Content-Type'] = ct
for i in range(self.max_retries + 1):
try:
r = requests.request(method, url, timeout=self.timeout, headers=headers, allow_redirects=True,
json=data if ct == 'application/json' else None,
data=data if (ct and ct != 'application/json') else None)
if not r.encoding or r.encoding.lower() == 'iso-8859-1':
r.encoding = r.apparent_encoding or 'utf-8'
return r
except requests.exceptions.Timeout:
if i < self.max_retries:
time.sleep(1)
continue
return None
except Exception:
return None
return None
def _ping(self, t):
url = self._url(t)
start = time.time()
r = self._req(url)
ms = (time.time() - start) * 1000
if not r:
return f"Ping {t}: 连接失败"
status = "成功" if r.status_code < 400 else f"HTTP {r.status_code}"
extra = f"\n 重定向: {r.url}" if r.url != url else ""
return f"Ping {t}:\n 状态: {status}\n 延迟: {ms:.0f}ms\n HTTP: {r.status_code}{extra}"
def _get(self, t):
url = self._url(t)
r = self._req(url)
if not r:
return f"GET {t}: 失败"
content = r.text
return f"GET {url} [{r.status_code}]:\n{content}"
def _status(self, t):
url = self._url(t)
r = self._req(url)
if not r:
return f"STATUS {t}: 失败"
return (
f"STATUS {url}:\n"
f" 状态码: {r.status_code} {r.reason}\n"
f" 延迟: {r.elapsed.total_seconds()*1000:.0f}ms\n"
f" 大小: {len(r.content)} bytes\n"
f" 服务器: {r.headers.get('Server', '?')}\n"
f" 类型: {r.headers.get('Content-Type', '?')}\n"
f" 最终URL: {r.url}"
)
def _headers(self, t):
url = self._url(t)
r = self._req(url, method='HEAD')
if not r:
r = self._req(url)
if not r:
return f"HEADERS {t}: 失败"
lines = [f"HEADERS {url}:"]
for k, v in r.headers.items():
lines.append(f" {k}: {v}")
return "\n".join(lines)
def _ip(self, t):
if t.startswith(('http://', 'https://')):
t = urlparse(t).netloc
try:
ips = list(set(addr[4][0] for addr in socket.getaddrinfo(t, None)))
return f"IP {t}:\n" + "\n".join(f" {ip}" for ip in ips)
except socket.gaierror:
return f"IP {t}: 解析失败"
def _dns(self, t):
if t.startswith(('http://', 'https://')):
t = urlparse(t).netloc
lines = [f"DNS {t}:"]
try:
seen = set()
for item in socket.getaddrinfo(t, None):
ip = item[4][0]
fam = "IPv4" if item[0] == socket.AF_INET else "IPv6"
if (ip, fam) not in seen:
seen.add((ip, fam))
lines.append(f" {fam}: {ip}")
except socket.gaierror as e:
lines.append(f" 失败: {e}")
fqdn = socket.getfqdn(t)
if fqdn != t:
lines.append(f" FQDN: {fqdn}")
return "\n".join(lines)
def _ssl(self, t):
if t.startswith(('http://', 'https://')):
t = urlparse(t).netloc
host = t.split(':')[0]
lines = [f"SSL {t}:"]
try:
ctx = ssl.create_default_context()
with ctx.wrap_socket(socket.socket(), server_hostname=host) as s:
s.settimeout(self.timeout)
s.connect((host, 443))
cert = s.getpeercert()
subj = dict(x[0] for x in cert.get('subject', []))
iss = dict(x[0] for x in cert.get('issuer', []))
lines.append(f" 主题: {subj.get('commonName', '?')}")
lines.append(f" 颁发: {iss.get('organizationName', iss.get('commonName', '?'))}")
lines.append(f" 生效: {cert.get('notBefore', '?')}")
lines.append(f" 过期: {cert.get('notAfter', '?')}")
san = cert.get('subjectAltName', [])
if san:
doms = [v for _, v in san[:5]]
lines.append(f" SAN: {', '.join(doms)}{'...' if len(san) > 5 else ''}")
lines.append(f" 状态: 有效")
except ssl.SSLCertVerificationError as e:
lines.append(f" 状态: 验证失败 - {e}")
except socket.timeout:
lines.append(f" 状态: 超时")
except ConnectionRefusedError:
lines.append(f" 状态: 连接被拒绝")
except Exception as e:
lines.append(f" 状态: {e}")
return "\n".join(lines)
def _port(self, t, rng=''):
if t.startswith(('http://', 'https://')):
t = urlparse(t).netloc.split(':')[0]
if '-' in rng:
try:
s, e = rng.split('-', 1)
s, e = max(1, int(s)), min(65535, int(e))
except ValueError:
s, e = 1, 1024
else:
s, e = 1, 1024
try:
ip = socket.gethostbyname(t)
except socket.gaierror:
return f"PORT {t}: 解析失败"
common = {21:'FTP',22:'SSH',23:'Telnet',25:'SMTP',53:'DNS',80:'HTTP',110:'POP3',143:'IMAP',443:'HTTPS',993:'IMAPS',995:'POP3S',3306:'MySQL',3389:'RDP',5432:'PostgreSQL',6379:'Redis',8080:'HTTP-Alt',8443:'HTTPS-Alt',27017:'MongoDB'}
open_p = []
for p in range(s, e + 1):
try:
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.settimeout(0.3)
if sock.connect_ex((ip, p)) == 0:
open_p.append((p, common.get(p, '?')))
sock.close()
except Exception:
pass
lines = [f"PORT {t} ({s}-{e}), IP={ip}:"]
if open_p:
for p, svc in sorted(open_p):
lines.append(f" {p}/{svc}")
else:
lines.append(f" 无开放端口")
return "\n".join(lines)
def _api(self, url, method, body_str):
url = self._url(url)
data = None
ct = None
if body_str:
try:
data = json.loads(body_str)
ct = 'application/json'
except json.JSONDecodeError:
data = body_str
ct = 'text/plain'
r = self._req(url, method=method, data=data, ct=ct)
if not r:
return f"{method} {url}: 失败"
lines = [f"{method} {url} [{r.status_code}]"]
ctype = r.headers.get('Content-Type', '')
if 'json' in ctype:
try:
body = json.dumps(r.json(), ensure_ascii=False, indent=2)
lines.append(body)
except Exception:
lines.append(r.text)
else:
lines.append(r.text)
return "\n".join(lines)