This commit is contained in:
wanghongen
2023-06-08 20:49:07 +08:00
parent 099175fa27
commit e0bc38b1c3
9 changed files with 379 additions and 283 deletions

View File

@@ -2,13 +2,13 @@ import 'dart:collection';
import 'package:flutter/material.dart';
import 'package:network/network/bin/server.dart';
import 'package:network/ui/widgets.dart';
import 'package:network/ui/left.dart';
import 'package:network/ui/panel.dart';
import 'network/channel.dart';
import 'network/handler.dart';
import 'network/http/http.dart';
import 'network/util/AttributeKeys.dart';
import 'ui/components.dart';
void main() {
runApp(const MyApp());
@@ -39,33 +39,109 @@ class NetworkHomePage extends StatefulWidget {
State<NetworkHomePage> createState() => _NetworkHomePagePageState();
}
class _NetworkHomePagePageState extends State<NetworkHomePage> implements EventListener {
LinkedHashMap<HostAndPort, List<HttpRequest>> containerMap = LinkedHashMap<HostAndPort, List<HttpRequest>>();
class DomainWidget extends StatefulWidget {
final _DomainWidgetState _state = _DomainWidgetState();
final NetworkTabController panel;
Set<String> expandedHosts = <String>{};
DomainWidget({super.key, required this.panel});
@override
void onRequest(Channel channel, HttpRequest request) {
HostAndPort hostAndPort = channel.getAttribute(AttributeKeys.HOST_KEY);
var list = containerMap[hostAndPort];
if (list == null) {
list = [request];
setState(() {
containerMap;
});
containerMap[hostAndPort] = list;
} else {
list.add(request);
}
void add(Channel channel, HttpRequest request) {
_state.add(channel, request);
}
void addResponse(Channel channel, HttpResponse response) {
_state.addResponse(channel, response);
}
void clean() {
_state.clean();
}
@override
void onResponse(Channel channel, HttpResponse response) {}
State<StatefulWidget> createState() {
return _state;
}
}
class _DomainWidgetState extends State<DomainWidget> {
LinkedHashMap<HostAndPort, ValueNotifier<HeaderBody>> containerMap =
LinkedHashMap<HostAndPort, ValueNotifier<HeaderBody>>();
@override
Widget build(BuildContext context) {
var map = containerMap.values.map((e) => ValueListenableBuilder<HeaderBody>(
valueListenable: e,
builder: (context, value, child) {
return _show(value);
}));
return ListView(children: map.toList());
}
///添加请求
void add(Channel channel, HttpRequest request) {
HostAndPort hostAndPort = channel.getAttribute(AttributeKeys.HOST_KEY);
ValueNotifier<HeaderBody>? valueNotifier = containerMap[hostAndPort];
var listURI = RowURI(request, widget.panel);
if (valueNotifier != null) {
valueNotifier.value.addBody(channel.id, listURI);
valueNotifier.value = valueNotifier.value.copy();
return;
}
var headerBody = HeaderBody(hostAndPort.url);
valueNotifier = ValueNotifier<HeaderBody>(headerBody);
headerBody.addBody(channel.id, listURI);
setState(() {
containerMap[hostAndPort] = valueNotifier!;
});
}
///添加响应
void addResponse(Channel channel, HttpResponse response) {
HostAndPort hostAndPort = channel.getAttribute(AttributeKeys.HOST_KEY);
ValueNotifier<HeaderBody>? valueNotifier = containerMap[hostAndPort];
if (valueNotifier != null && valueNotifier.value.getBody(channel.id) != null) {
var body = valueNotifier.value.getBody(channel.id);
body?.add(response);
return;
}
}
void clean() {
setState(() {
containerMap.forEach((key, value) {
value.dispose();
});
containerMap.clear();
});
}
Widget _show(Widget widget) {
return AnimatedOpacity(opacity: 1.0, duration: const Duration(seconds: 2), child: widget);
}
}
class _NetworkHomePagePageState extends State<NetworkHomePage> implements EventListener {
late DomainWidget domainWidget;
final NetworkTabController panel = NetworkTabController();
@override
void onRequest(Channel channel, HttpRequest request) {
domainWidget.add(channel, request);
}
@override
void onResponse(Channel channel, HttpResponse response) {
domainWidget.addResponse(channel, response);
}
@override
void initState() {
super.initState();
print("initState");
super.initState();
domainWidget = DomainWidget(panel: panel);
start(listener: this);
}
@@ -73,102 +149,14 @@ class _NetworkHomePagePageState extends State<NetworkHomePage> implements EventL
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
leading: IconButton(onPressed: () => domainWidget.clean(), icon: const Icon(Icons.cleaning_services)),
backgroundColor: Theme.of(context).colorScheme.inversePrimary,
title: Text(widget.title),
),
body: Row(children: [
SizedBox(width: 420, child: ListView(children: _buildHosts())),
SizedBox(width: 420, child: domainWidget),
const Spacer(),
Expanded(flex: 100, child: NetworkTabController()),
Expanded(flex: 100, child: Visibility(visible: true, child: domainWidget.panel)),
]));
}
Widget _row(HostAndPort host) {
bool selected = expandedHosts.contains(host.url);
return ListTile(
leading: Icon(selected ? Icons.arrow_drop_down : Icons.arrow_right, size: 16),
dense: true,
selected: selected,
horizontalTitleGap: 0,
visualDensity: const VisualDensity(vertical: -3.6),
title: Text(host.url, textAlign: TextAlign.left),
onTap: () {
if (!expandedHosts.remove(host.url)) {
expandedHosts.add(host.url);
}
setState(() {
expandedHosts;
});
});
}
List<Widget> _buildHosts() {
print(containerMap.keys);
List<Widget> list = [];
for (var host in containerMap.keys) {
list.add(_row(host));
if (expandedHosts.contains(host.url)) {
containerMap[host]?.forEach((element) {
print(element);
list.add(ListURI(leading: Icons.html, text: '${element.method.name} ${Uri.parse(element.uri).path}'));
});
}
}
return list;
// return [
// ListTile(
// leading: const Icon(Icons.arrow_drop_down),
// dense: true,
// title: const Text('https://dmall.com', textAlign: TextAlign.left),
// selected: true,
// onTap: () {}),
// const ListURI(leading: Icons.image_outlined, text: 'POST /private/browser/stats/haha'),
// const ListURI(leading: Icons.javascript_sharp, text: 'GET /private/browser/stats/haha'),
// const ListURI(leading: Icons.css, text: 'GET /private/browser/stats/haha'),
// ListTile(
// leading: const Icon(
// size: 15,
// Icons.document_scanner,
// color: Colors.red,
// ),
// title: const Text('POST /private/browser/stats', textAlign: TextAlign.left),
// textColor: Colors.red,
// trailing: const Icon(Icons.chevron_right),
// visualDensity: const VisualDensity(vertical: -4),
// dense: true,
// contentPadding: const EdgeInsets.symmetric(vertical: 0, horizontal: 50.0),
// onTap: () {}),
// const Divider(),
// ListTile(
// leading: const Icon(Icons.expand_more),
// dense: true,
// title: const Text('https://baidu.com', textAlign: TextAlign.left),
// onTap: () {}),
// _buildRow(),
// const Divider(),
// ];
}
Widget _buildRow() {
final ScrollController scrollController = ScrollController();
return Scrollbar(
controller: scrollController,
thumbVisibility: true,
trackVisibility: true,
child: SingleChildScrollView(
controller: scrollController,
scrollDirection: Axis.horizontal,
child: Container(
margin: const EdgeInsets.only(left: 16),
child: const Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
RowURI(leading: Icons.image, text: "POST /private/browser/stats/private/browser/stats"),
RowURI(leading: Icons.javascript_sharp, text: "GET /private/browser/stats.js"),
RowURI(leading: Icons.css, text: "GET /openapi/documents/oristartguid.css"),
],
)),
));
}
}

View File

@@ -4,6 +4,7 @@ import 'dart:io';
import '../channel.dart';
import '../handler.dart';
import '../http/codec.dart';
import '../util/logger.dart';
Future<void> main() async {
start();
@@ -15,6 +16,7 @@ Future<void> start({EventListener? listener}) async {
..initChannel((channel) {
channel.pipeline.handle(HttpRequestCodec(), HttpResponseCodec(), HttpChannelHandler(listener: listener));
});
log.i("listen on $port");
await server.bind().then((value) => {setSystemProxy(port)});
}
@@ -27,8 +29,8 @@ void setSystemProxy(int port) {
// .then((ProcessResult results) {
// print(results.stdout);
// });
// Process.run('networksetup', ['-setwebproxy', 'Wi-Fi', '127.0.0.1', port.toString()]).then((ProcessResult results) {
// print(results.stdout);
// });
Process.run('networksetup', ['-setwebproxy', 'Wi-Fi', '127.0.0.1', port.toString()]).then((ProcessResult results) {
print(results.stdout);
});
}
}

View File

@@ -84,7 +84,7 @@ class HttpChannelHandler extends ChannelHandler<HttpRequest> {
var hostAndPort = getHostAndPort(httpRequest);
clientChannel.putAttribute(AttributeKeys.HOST_KEY, hostAndPort);
var proxyHandler = HttpResponseProxyHandler(clientChannel);
var proxyHandler = HttpResponseProxyHandler(clientChannel, listener: listener);
var proxyChannel = await HttpClients.connect(hostAndPort, proxyHandler);
//https代理新建连接请求
@@ -104,14 +104,17 @@ class HttpResponseProxyHandler extends ChannelHandler<HttpResponse> {
/// 排除的后缀 不打印日志
final Set<String> excludeContent = HashSet.from(["javascript", "text/css", "application/font-woff", "image"]);
HttpResponseProxyHandler(this.clientChannel);
EventListener? listener;
HttpResponseProxyHandler(this.clientChannel, {this.listener});
@override
void channelRead(Channel channel, HttpResponse msg) {
String contentType = msg.headers.contentType;
if (excludeContent.every((element) => !contentType.contains(element))) {
// log.i("[${clientChannel.id}] Response ${String.fromCharCodes(msg.body ?? [])}");
// log.i("[${clientChannel.id}] Response ${ String.fromCharCodes(msg.body ?? [])}");
}
listener?.onResponse(clientChannel, msg);
//发送给客户端
clientChannel.write(msg);
}

View File

@@ -1,3 +1,6 @@
import 'dart:convert';
import 'dart:io';
import 'http_headers.dart';
///定义HTTP消息的接口为HttpRequest和HttpResponse提供公共属性。
@@ -12,10 +15,17 @@ abstract class HttpMessage {
HttpMessage(this.protocolVersion);
String get bodyAsString {
if (body == null) {
if (body == null || body?.isEmpty == true) {
return "";
}
return String.fromCharCodes(body!);
try {
if (headers.isGzip) {
return utf8.decode(gzip.decode(body!));
}
return utf8.decode(body!);
} catch (e) {
return String.fromCharCodes(body!);
}
}
}
@@ -132,6 +142,10 @@ class HttpStatus {
HttpStatus(this.code, this.reasonPhrase);
bool isSuccessful() {
return code >= 200 && code < 300;
}
@override
String toString() {
return 'HttpResponseStatus{code: $code, reasonPhrase: $reasonPhrase}';

View File

@@ -2,6 +2,7 @@ import 'dart:core';
import 'dart:io';
import 'package:basic_utils/basic_utils.dart';
import 'package:flutter/services.dart';
Future<void> main() async {
var securityContext = await CertificateManager.getCertificateContext('www.baidu.com');
@@ -64,7 +65,7 @@ class CertificateManager {
var csr = X509Utils.generateRsaCsrPem(x509Subject, caPriKey, serverPubKey as RSAPublicKey, san: [host]);
Map<String, String> issuer = Map.from(_caCert.tbsCertificate!.subject);
var csrPem = X509Utils.generateSelfSignedCertificate(caPriKey, csr, 3650, sans: [host], issuer: issuer);
var csrPem = X509Utils.generateSelfSignedCertificate(caPriKey, csr, 365, sans: [host], issuer: issuer);
return csrPem;
}
@@ -73,13 +74,13 @@ class CertificateManager {
return;
}
//从项目目录加入ca根证书
var caPem = await File('assets/certs/ca.crt').readAsString();
var caPem = await rootBundle.loadString('assets/certs/ca.crt');
_caCert = X509Utils.x509CertificateFromPem(caPem);
//根据CA证书subject来动态生成目标服务器证书的issuer和subject
//从项目目录加入ca私钥
var privateBytes = await File('assets/certs/ca_private.der').readAsBytes();
_caPriKey = CryptoUtils.rsaPrivateKeyFromDERBytes(privateBytes);
var privateBytes = await rootBundle.load('assets/certs/ca_private.der');
_caPriKey = CryptoUtils.rsaPrivateKeyFromDERBytes(privateBytes.buffer.asUint8List());
_initialized = true;
}

View File

@@ -1,82 +0,0 @@
import 'package:flutter/material.dart';
class ListURI extends StatefulWidget {
final IconData? leading;
final String text;
final Color? color;
final IconData trailing;
const ListURI(
{Key? key, this.leading, required this.text, this.color = Colors.green, this.trailing = Icons.chevron_right})
: super(key: key);
@override
State<ListURI> createState() => _ListURIState();
}
class _ListURIState extends State<ListURI> {
@override
Widget build(BuildContext context) {
return ListTile(
leading: Icon(widget.leading, size: 15, color: widget.color),
title: Text(widget.text, overflow: TextOverflow.ellipsis, maxLines: 1),
trailing: Icon(widget.trailing),
dense: true,
contentPadding: const EdgeInsets.symmetric(vertical: 0, horizontal: 50.0),
onTap: () {});
}
}
class RowURI extends StatefulWidget {
final IconData? leading;
final String text;
final IconData trailing;
const RowURI({Key? key, this.leading, required this.text, this.trailing = Icons.arrow_right}) : super(key: key);
@override
State<StatefulWidget> createState() {
return _RowURIState();
}
}
class _RowURIState extends State<RowURI> {
@override
Widget build(BuildContext context) {
return Row(children: [
TextButton.icon(
icon: Icon(widget.leading, size: 16),
onPressed: () {
print("hello");
},
label: Text(widget.text, style: const TextStyle(color: Colors.black87)),
),
const Positioned(right: 10, child: Icon(Icons.chevron_right))
]);
}
}
class IconText extends StatefulWidget {
const IconText({Key? key, this.leading, required this.text, this.color, this.trailing}) : super(key: key);
final Widget? leading;
final String text;
final Color? color;
final Widget? trailing;
@override
State<IconText> createState() => _IconTextState();
}
class _IconTextState extends State<IconText> {
@override
Widget build(BuildContext context) {
return Row(
children: [
widget.leading ?? const SizedBox(),
Text(widget.text),
widget.trailing ?? const SizedBox(),
],
);
}
}

112
lib/ui/left.dart Normal file
View File

@@ -0,0 +1,112 @@
import 'dart:collection';
import 'package:flutter/material.dart';
import 'package:network/network/http/http.dart';
import 'package:network/ui/panel.dart';
///标题和内容布局 标题是域名 内容是域名下请求
class HeaderBody extends StatefulWidget {
final Map<String, RowURI> map = HashMap<String, RowURI>();
final String header;
final Queue<RowURI> _body = Queue<RowURI>();
final bool selected;
HeaderBody(this.header, {Key? key, this.selected = false}) : super(key: key);
///添加请求
void addBody(String key, RowURI widget) {
_body.addFirst(widget);
map[key] = widget;
}
RowURI? getBody(String key) {
return map[key];
}
//复制
HeaderBody copy() {
var headerBody = HeaderBody(header, selected: selected);
headerBody._body.addAll(_body);
headerBody.map.addAll(map);
return this;
}
@override
State<StatefulWidget> createState() {
return _HeaderBodyState();
}
}
class _HeaderBodyState extends State<HeaderBody> {
late bool selected;
@override
void initState() {
super.initState();
selected = widget.selected;
}
@override
Widget build(BuildContext context) {
return Column(children: [
_hostWidget(widget.header),
Visibility(visible: selected, child: Column(children: widget._body.toList()))
]);
}
Widget _hostWidget(String title) {
return ListTile(
leading: Icon(selected ? Icons.arrow_drop_down : Icons.arrow_right, size: 16),
dense: true,
selected: selected,
horizontalTitleGap: 0,
visualDensity: const VisualDensity(vertical: -3.6),
title: Text(title, textAlign: TextAlign.left),
onTap: () {
setState(() {
selected = !selected;
});
});
}
}
class RowURI extends StatefulWidget {
final Color? color;
final HttpRequest request;
HttpResponse? response;
final NetworkTabController panel;
final _RowURIState _state = _RowURIState();
RowURI(this.request, this.panel, {Key? key, this.color = Colors.green}) : super(key: key);
@override
State<RowURI> createState() => _state;
void add(HttpResponse response) {
this.response = response;
}
}
class _RowURIState extends State<RowURI> {
bool selected = false;
@override
Widget build(BuildContext context) {
var request = widget.request;
var leading = widget.response == null ? Icons.http : Icons.http;
var title = '${request.method.name} ${Uri.parse(request.uri).path}';
return ListTile(
leading: Icon(leading, size: 15, color: widget.color),
title: Text(title, overflow: TextOverflow.ellipsis, maxLines: 1),
trailing: const Icon(Icons.chevron_right),
dense: true,
contentPadding: const EdgeInsets.symmetric(vertical: 0, horizontal: 50.0),
onTap: () {
selected = !selected;
widget.panel.change(widget.request, widget.response);
});
}
}

136
lib/ui/panel.dart Normal file
View File

@@ -0,0 +1,136 @@
import 'package:flutter/material.dart';
import 'package:network/network/http/http.dart';
class NetworkTabController extends StatefulWidget {
final tabs = <Tab>[
const Tab(child: Text('General', style: TextStyle(fontSize: 18))),
const Tab(child: Text('Request', style: TextStyle(fontSize: 18))),
const Tab(child: Text('Response', style: TextStyle(fontSize: 18))),
const Tab(child: Text('Cookies', style: TextStyle(fontSize: 18))),
];
final _NetworkTabState _state = _NetworkTabState();
HttpRequest? request;
HttpResponse? response;
NetworkTabController({super.key});
void change(HttpRequest request, HttpResponse? response) {
_state.setState(() {
this.request = request;
this.response = response;
});
}
@override
State<StatefulWidget> createState() {
return _state;
}
}
class _NetworkTabState extends State<NetworkTabController> {
@override
Widget build(BuildContext context) {
if (widget.request == null) {
return const SizedBox();
}
return DefaultTabController(
length: widget.tabs.length,
child: Scaffold(
appBar: AppBar(title: TabBar(tabs: widget.tabs)),
body: TabBarView(
children: [
general(),
request(),
response(),
ListView(children: const [Text("Cookies")])
],
),
));
}
Widget general() {
return ExpansionTile(
title: const Text("General", style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16)),
initiallyExpanded: true,
childrenPadding: const EdgeInsets.all(20),
children: [
Row(children: [
const Expanded(flex: 1, child: SelectableText("Request URL:")),
Expanded(flex: 4, child: SelectableText(widget.request?.uri ?? ''))
]),
const SizedBox(height: 20),
Row(children: [
const Expanded(flex: 1, child: SelectableText("Status Code:")),
Expanded(flex: 4, child: SelectableText(widget.response?.status.code.toString() ?? ''))
])
]);
}
Widget request() {
var headers = <Widget>[];
widget.request?.headers.forEach((name, value) {
headers.add(Row(children: [
Expanded(flex: 2, child: SelectableText('$name:')),
Expanded(flex: 4, child: SelectableText(value)),
const SizedBox(height: 20),
]));
});
ExpansionTile? bodyWidgets;
if (widget.request?.body?.isNotEmpty == true) {
bodyWidgets = ExpansionTile(
title: const Text("Request Body", style: TextStyle(fontWeight: FontWeight.bold)),
initiallyExpanded: true,
shape: const Border(),
childrenPadding: const EdgeInsets.all(20),
children: [
SelectableText.rich(
TextSpan(text: widget.request?.bodyAsString, style: const TextStyle(color: Colors.black)))
]);
}
return ListView(children: [
ExpansionTile(
title: const Text("Request Headers", style: TextStyle(fontWeight: FontWeight.bold)),
initiallyExpanded: true,
shape: const Border(),
childrenPadding: const EdgeInsets.all(20),
children: headers),
const Divider(),
bodyWidgets ?? const SizedBox()
]);
}
Widget response() {
var headers = <Widget>[];
widget.response?.headers.forEach((name, value) {
headers.add(Row(children: [
Expanded(flex: 2, child: SelectableText('$name:')),
Expanded(flex: 4, child: SelectableText(value)),
const SizedBox(height: 20),
]));
});
return ListView(children: [
ExpansionTile(
title: const Text("Response Headers", style: TextStyle(fontWeight: FontWeight.bold)),
initiallyExpanded: true,
shape: const Border(),
childrenPadding: const EdgeInsets.all(20),
children: headers),
const Divider(),
ExpansionTile(
title: const Text("Response Body", style: TextStyle(fontWeight: FontWeight.bold)),
initiallyExpanded: true,
shape: const Border(),
childrenPadding: const EdgeInsets.all(20),
children: [
SelectableText.rich(
TextSpan(text: widget.response?.bodyAsString, style: const TextStyle(color: Colors.black))
)
])
]);
}
}

View File

@@ -1,78 +0,0 @@
import 'package:flutter/material.dart';
class NetworkTabController extends DefaultTabController {
static const myTabs = <Tab>[
Tab(child: Text('General', style: TextStyle(fontSize: 18))),
Tab(child: Text('Request', style: TextStyle(fontSize: 18))),
Tab(child: Text('Response', style: TextStyle(fontSize: 18))),
Tab(child: Text('Cookies', style: TextStyle(fontSize: 18))),
];
NetworkTabController({super.key})
: super(
length: 4,
child: Scaffold(
appBar: AppBar(
title: const TabBar(tabs: myTabs),
),
body: TabBarView(
children: [
ListView(children: const [
Text.rich(TextSpan(children: [
TextSpan(text: "Home: "),
TextSpan(text: "https://flutterchina.club", style: TextStyle(color: Colors.blue)),
]))
]),
Container(
padding: const EdgeInsets.all(20),
child: const Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
Text('General', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 18)),
Row(children: [
Expanded(flex: 1, child: Text("Request URL:")),
Expanded(
flex: 4,
child: Text(
"https://googleads.g.doubleclick.net/pagead/html/r20230601/r20190131/zrt_lookup.html"))
]),
SizedBox(height: 10), //保留间距
Row(children: [
Expanded(flex: 1, child: Text("Request Method:")),
Expanded(flex: 4, child: Text("GET"))
]),
SizedBox(height: 10), //保留间距
Row(children: [
Expanded(flex: 1, child: Text("Status Code:")),
Expanded(flex: 4, child: Text("200"))
]),
SizedBox(height: 10), //保留间距
Row(children: [
Expanded(flex: 1, child: Text("Remote Address:")),
Expanded(flex: 4, child: Text("127.0.0.1:8080"))
])
])),
const ExpansionTile(
title: Text("General", style: TextStyle(fontWeight: FontWeight.bold, fontSize: 18)),
initiallyExpanded: true,
childrenPadding: EdgeInsets.all(20),
expandedCrossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(children: [
Expanded(flex: 1, child: Text("Request URL:")),
Expanded(
flex: 4,
child: Text(
"https://googleads.g.doubleclick.net/pagead/html/r20230601/r20190131/zrt_lookup.html"))
]),
SizedBox(
height: 20,
),
Row(children: [
Expanded(flex: 1, child: Text("Status Code:")),
Expanded(flex: 4, child: Text("200"))
])
]),
ListView(children: const [Text("Cookies")])
],
),
));
}