mirror of
https://github.com/wanghongenpin/proxypin.git
synced 2026-03-26 06:29:46 +08:00
http body charset && script add rawBody byte array (#304)
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
184
lib/network/http/content_type.dart
Normal file
184
lib/network/http/content_type.dart
Normal 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());
|
||||
}
|
||||
}
|
||||
|
||||
///类似于equals(Object),但仅基于类型和子类型,即忽略参数。
|
||||
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)";
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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: [
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user