http body charset && script add rawBody byte array (#304)

This commit is contained in:
wanghongenpin
2024-09-14 00:47:31 +08:00
parent 5979bd5c7e
commit eef651d286
16 changed files with 321 additions and 60 deletions

View File

@@ -388,12 +388,13 @@ class RequestRewrites {
//修改消息
_updateMessage(HttpMessage message, RewriteItem item) {
if (item.type == RewriteType.updateBody && message.body != null) {
message.body = utf8.encode(message.bodyAsString.replaceAllMapped(RegExp(item.key!), (match) {
String body = message.bodyAsString.replaceAllMapped(RegExp(item.key!), (match) {
if (match.groupCount > 0 && item.value?.contains("\$1") == true) {
return item.value!.replaceAll("\$1", match.group(1)!);
}
return item.value ?? '';
}));
});
message.body = message.charset == 'utf-8' ? utf8.encode(body) : body.codeUnits;
message.headers.remove(HttpHeaders.CONTENT_ENCODING);
message.headers.contentLength = message.body!.length;
@@ -453,7 +454,7 @@ class RequestRewrites {
}
if (item.body != null) {
message.body = utf8.encode(item.body!);
message.body = message.charset == 'utf-8' ? utf8.encode(item.body!) : item.body?.codeUnits;
message.headers.contentLength = message.body!.length;
}
return;

View File

@@ -310,7 +310,8 @@ async function onResponse(context, request, response) {
'queries': requestUri?.queryParameters,
'headers': request.headers.toMap(),
'method': request.method.name,
'body': request.bodyAsString
'body': request.bodyAsString,
'rawBody': request.body
};
}
@@ -320,8 +321,12 @@ async function onResponse(context, request, response) {
if (response.contentType.isBinary) {
body = response.body;
}
return {'headers': response.headers.toMap(), 'statusCode': response.status.code, 'body': body};
return {
'headers': response.headers.toMap(),
'statusCode': response.status.code,
'body': body,
'rawBody': response.body
};
}
//http request
@@ -348,8 +353,11 @@ async function onResponse(context, request, response) {
}
request.headers.add(key, value);
});
request.body = map['body'] == null ? null : utf8.encode(map['body'].toString());
request.body = map['body']?.toString().codeUnits;
if (request.body != null && request.charset == 'utf-8') {
request.body = utf8.encode(map['body'].toString());
}
return request;
}
@@ -365,6 +373,7 @@ async function onResponse(context, request, response) {
response.headers.add(key, value);
});
response.headers.remove(HttpHeaders.CONTENT_ENCODING);
//判断是否是二进制
@@ -373,7 +382,11 @@ async function onResponse(context, request, response) {
return response;
}
response.body = map['body'] == null ? null : utf8.encode(map['body'].toString());
response.body = map['body']?.toString().codeUnits;
if (response.body != null && response.charset == 'utf-8') {
response.body = utf8.encode(map['body'].toString());
}
return response;
}
}

View File

@@ -0,0 +1,184 @@
/*
* Copyright Copyright 2024 WangHongEn.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import 'package:network_proxy/network/util/cache.dart';
///content type
///@author WangHongEn
enum ContentType {
json,
formUrl,
formData,
js,
html,
text,
css,
font,
image,
video,
http;
static ContentType valueOf(String name) {
return ContentType.values.firstWhere((element) => element.name == name.toLowerCase(), orElse: () => http);
}
//是否是二进制
bool get isBinary {
return this == image || this == font || this == video;
}
}
class MediaType {
static const String wildcardType = "*/*";
static LruCache<String, MediaType> cachedMediaTypes = LruCache(64);
///默认编码类型
static List<MediaType> defaultCharsetMediaTypes = [
MediaType("text", "plain", charset: "utf-8"),
MediaType("text", "html", charset: "utf-8"),
MediaType("application", "json", charset: "utf-8"),
MediaType("application", "problem+json", charset: "utf-8"),
MediaType("application", "xml", charset: "utf-8"),
MediaType("application", "xhtml+xml", charset: "utf-8"),
MediaType("application", "octet-stream", charset: "utf-16"),
MediaType("image", "*", charset: "utf-16"),
];
final String type;
final String subtype;
final Map<String, String> parameters;
MediaType(this.type, this.subtype, {this.parameters = const {}, String? charset}) {
if (charset != null) {
parameters["charset"] = charset;
}
}
factory MediaType.valueOf(String mediaType) {
if (mediaType.isEmpty) {
throw InvalidMediaTypeException(mediaType, "'mediaType' must not be empty");
}
// do not cache multipart mime types with random boundaries
if (mediaType.startsWith("multipart")) {
return _parseMediaTypeInternal(mediaType);
}
return cachedMediaTypes.pubIfAbsent(mediaType, () => _parseMediaTypeInternal(mediaType));
}
///编码
String? get charset {
return parameters["charset"];
}
///获取默认编码
static String? defaultCharset(MediaType mediaType) {
for (var defaultMediaType in defaultCharsetMediaTypes) {
if (defaultMediaType.equalsTypeAndSubtype(mediaType)) {
return defaultMediaType.charset;
}
}
return null;
}
static MediaType _parseMediaTypeInternal(String mediaType) {
int index = mediaType.indexOf(';');
String fullType = (index >= 0 ? mediaType.substring(0, index) : mediaType).trim();
if (fullType.isEmpty) {
throw InvalidMediaTypeException(mediaType, "'mediaType' must not be empty");
}
if (MediaType.wildcardType == fullType) {
fullType = "*/*";
}
int subIndex = fullType.indexOf('/');
if (subIndex == -1) {
throw InvalidMediaTypeException(mediaType, "does not contain '/'");
}
if (subIndex == fullType.length - 1) {
throw InvalidMediaTypeException(mediaType, "does not contain subtype after '/'");
}
String type = fullType.substring(0, subIndex);
String subtype = fullType.substring(subIndex + 1);
if (MediaType.wildcardType == type && MediaType.wildcardType != subtype) {
throw InvalidMediaTypeException(mediaType, "wildcard type is legal only in '*/*' (all mime types)");
}
Map<String, String> parameters = {};
do {
int nextIndex = index + 1;
bool quoted = false;
while (nextIndex < mediaType.length) {
var ch = mediaType[0];
if (ch == ';') {
if (!quoted) {
break;
}
} else if (ch == '"') {
quoted = !quoted;
}
nextIndex++;
}
String parameter = mediaType.substring(index + 1, nextIndex).trim();
if (parameter.isNotEmpty) {
int eqIndex = parameter.indexOf('=');
if (eqIndex >= 0) {
String attribute = parameter.substring(0, eqIndex).trim();
String value = parameter.substring(eqIndex + 1).trim();
parameters[attribute] = value;
}
}
index = nextIndex;
} while (index < mediaType.length);
try {
return MediaType(type, subtype, parameters: parameters);
} catch (e) {
throw InvalidMediaTypeException(mediaType, e.toString());
}
}
///类似于equalsObject但仅基于类型和子类型即忽略参数。
bool equalsTypeAndSubtype(MediaType other) {
return type.toLowerCase() == other.type.toLowerCase() && subtype.toLowerCase() == other.subtype.toLowerCase();
}
@override
bool operator ==(Object other) {
if (identical(this, other)) {
return true;
}
if (other is MediaType) {
return type == other.type && subtype == other.subtype && parameters == other.parameters;
}
return false;
}
@override
int get hashCode => type.hashCode ^ subtype.hashCode ^ parameters.hashCode;
}
class InvalidMediaTypeException implements Exception {
final String mediaType;
final String message;
InvalidMediaTypeException(this.mediaType, this.message);
@override
String toString() {
return "InvalidMediaTypeException: $message (mediaType: $mediaType)";
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2023 the original author or authors.
* Copyright Copyright 2023 WangHongEn.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -18,6 +18,7 @@ import 'dart:convert';
import 'dart:math';
import 'package:network_proxy/network/host_port.dart';
import 'package:network_proxy/network/http/content_type.dart';
import 'package:network_proxy/network/http/websocket.dart';
import 'package:network_proxy/network/util/logger.dart';
import 'package:network_proxy/network/util/process_info.dart';
@@ -26,6 +27,7 @@ import 'package:network_proxy/utils/compress.dart';
import 'http_headers.dart';
///定义HTTP消息的接口为HttpRequest和HttpResponse提供公共属性。
///@author WangHongEn
abstract class HttpMessage {
///内容类型
static final Map<String, ContentType> contentTypes = {
@@ -76,15 +78,31 @@ abstract class HttpMessage {
orElse: () => const MapEntry("unknown", ContentType.http))
.value;
///获取消息体编码
String? get charset {
var contentType = headers.contentType;
if (contentType.isEmpty) {
return null;
}
MediaType mediaType = MediaType.valueOf(contentType);
return mediaType.charset ?? MediaType.defaultCharset(mediaType);
}
String get bodyAsString {
if (body == null || body?.isEmpty == true) {
return "";
}
try {
List<int> rawBody = body!;
if (headers.contentEncoding == 'br') {
return utf8.decode(brDecode(body!));
rawBody = brDecode(body!);
}
return utf8.decode(body!);
if (charset == 'utf-8') {
return utf8.decode(rawBody);
}
return String.fromCharCodes(rawBody);
} catch (e) {
return String.fromCharCodes(body!);
}
@@ -148,6 +166,12 @@ class HttpRequest extends HttpMessage {
}
}
///获取消息体编码
@override
String? get charset {
return super.charset ?? 'utf-8';
}
///复制请求
HttpRequest copy({String? uri}) {
var request = HttpRequest(method, uri ?? this.uri, protocolVersion: protocolVersion);
@@ -189,29 +213,6 @@ class HttpRequest extends HttpMessage {
}
}
enum ContentType {
json,
formUrl,
formData,
js,
html,
text,
css,
font,
image,
video,
http;
static ContentType valueOf(String name) {
return ContentType.values.firstWhere((element) => element.name == name.toLowerCase(), orElse: () => http);
}
//是否是二进制
bool get isBinary {
return this == image || this == font || this == video;
}
}
///HTTP响应。
class HttpResponse extends HttpMessage {
HttpStatus status;

View File

@@ -15,7 +15,11 @@
*/
import 'dart:async';
import 'dart:collection';
/// A cache that expires entries after a given duration.
/// The cache uses a timer to remove entries after the specified duration.
/// @author WangHongEn
class ExpiringCache<K, V> {
final Duration duration;
final _cache = <K, V>{};
@@ -48,4 +52,53 @@ class ExpiringCache<K, V> {
_cache.remove(key);
}
}
class LruCache<K, V> {
final int capacity;
final _cache = LinkedHashMap<K, V>();
LruCache(this.capacity);
V? get(K key) {
if (!_cache.containsKey(key)) {
return null;
}
// Move the accessed key to the end to show that it was recently used
final value = _cache.remove(key);
_cache[key] = value as V;
return value;
}
V pubIfAbsent(K key, V Function() ifAbsent) {
if (_cache.containsKey(key)) {
return _cache[key]!;
}
final value = ifAbsent();
set(key, value);
return value;
}
void set(K key, V value) {
if (_cache.containsKey(key)) {
// Remove the old value
_cache.remove(key);
} else if (_cache.length == capacity) {
// Remove the first key (least recently used)
_cache.remove(_cache.keys.first);
}
_cache[key] = value;
}
void remove(K key) {
_cache.remove(key);
}
int get length => _cache.length;
void clear() {
_cache.clear();
}
}

View File

@@ -4,6 +4,7 @@ import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:flutter_toastr/flutter_toastr.dart';
import 'package:network_proxy/network/http/content_type.dart';
import 'package:network_proxy/network/http/http.dart';
const contentMap = {

View File

@@ -22,6 +22,7 @@ import 'package:flutter/services.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:flutter_toastr/flutter_toastr.dart';
import 'package:network_proxy/network/components/request_rewrite_manager.dart';
import 'package:network_proxy/network/http/content_type.dart';
import 'package:network_proxy/network/http/http.dart';
import 'package:network_proxy/network/util/logger.dart';
import 'package:network_proxy/ui/component/encoder.dart';

View File

@@ -13,6 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import 'package:network_proxy/network/http/content_type.dart';
import 'package:network_proxy/network/http/http.dart';
/// @author wanghongen

View File

@@ -256,7 +256,7 @@ class _HttpState extends State<_HttpWidget> {
message = widget.message;
body = TextEditingController(text: widget.message?.bodyAsString);
if (widget.message?.headers == null && !widget.readOnly) {
initHeader["User-Agent"] = ["ProxyPin/1.1.1"];
initHeader["User-Agent"] = ["ProxyPin/1.1.3"];
initHeader["Accept"] = ["*/*"];
return;
}

View File

@@ -15,7 +15,7 @@
*/
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:network_proxy/network/http/http.dart';
import 'package:network_proxy/network/http/content_type.dart';
import 'package:network_proxy/ui/desktop/request/model/search_model.dart';
import 'package:network_proxy/ui/desktop/request/search_condition.dart';

View File

@@ -16,6 +16,7 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:network_proxy/network/http/content_type.dart';
import 'package:network_proxy/network/http/http.dart';
import 'package:network_proxy/ui/desktop/request/model/search_model.dart';
import 'package:network_proxy/utils/lang.dart';

View File

@@ -104,15 +104,17 @@ class _ScriptWidgetState extends State<ScriptWidget> {
children: [
Row(children: [
SizedBox(
width: 300,
child: SwitchWidget(
title: localizations.enableScript,
subtitle: localizations.scriptUseDescribe,
value: data.enabled,
onChanged: (value) {
data.enabled = value;
_refreshScript();
})),
width: 350,
child: ListTile(
title: Text(localizations.enableScript),
subtitle: Text(localizations.scriptUseDescribe),
trailing: SwitchWidget(
value: data.enabled,
scale: 0.9,
onChanged: (value) {
data.enabled = value;
_refreshScript();
}))),
Expanded(
child: Row(
mainAxisAlignment: MainAxisAlignment.end,

View File

@@ -244,7 +244,7 @@ class _HttpState extends State<_HttpWidget> with AutomaticKeepAliveClientMixin {
message = widget.message;
body = widget.message?.bodyAsString;
if (widget.message?.headers == null && !widget.readOnly) {
initHeader["User-Agent"] = ["ProxyPin/1.1.1"];
initHeader["User-Agent"] = ["ProxyPin/1.1.3"];
initHeader["Accept"] = ["*/*"];
return;
}

View File

@@ -72,19 +72,18 @@ class _MobileScriptState extends State<MobileScript> {
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.start,
children: [
Row(children: [
SizedBox(
width: 300,
child: SwitchWidget(
title: localizations.enableScript,
subtitle: localizations.scriptUseDescribe,
value: data.enabled,
onChanged: (value) {
data.enabled = value;
_refreshScript();
},
)),
]),
SizedBox(
child: ListTile(
title: Text(localizations.enableScript),
subtitle: Text(localizations.scriptUseDescribe),
trailing: SwitchWidget(
value: data.enabled,
onChanged: (value) {
data.enabled = value;
_refreshScript();
},
))),
const SizedBox(height: 8),
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [

View File

@@ -17,6 +17,7 @@ import 'dart:convert';
import 'dart:io';
import 'package:network_proxy/network/host_port.dart';
import 'package:network_proxy/network/http/content_type.dart';
import 'package:network_proxy/network/http/http.dart';
import 'package:network_proxy/network/http/http_headers.dart';
import 'package:network_proxy/network/util/process_info.dart';
@@ -80,7 +81,7 @@ class Har {
title = title.contains("ProxyPin") ? title : "[ProxyPin]$title";
har["log"] = {
"version": "1.2",
"creator": {"name": "ProxyPin", "version": "1.1.1"},
"creator": {"name": "ProxyPin", "version": "1.1.3"},
"pages": [
{
"title": title,

View File

@@ -1,6 +1,9 @@
import 'dart:io';
main() async {
var contentType = ContentType.parse("application/json");
print(contentType);
print(contentType.charset);
print(Uri.parse("https://www.v2ex.com").scheme);
// await socketTest();
await webTest();