add moat service
This commit is contained in:
@@ -1 +1,2 @@
|
|||||||
|
export 'src/data/service/moat_service.dart';
|
||||||
export 'src/pigeons/proxy.g.dart' show IPtProxyController, ProxyType;
|
export 'src/pigeons/proxy.g.dart' show IPtProxyController, ProxyType;
|
||||||
|
|||||||
@@ -0,0 +1,151 @@
|
|||||||
|
import 'package:json_annotation/json_annotation.dart';
|
||||||
|
|
||||||
|
part 'moat.g.dart';
|
||||||
|
|
||||||
|
enum TransportType {
|
||||||
|
obfs4('obfs4'),
|
||||||
|
snowflake('snowflake'),
|
||||||
|
meek('meek'),
|
||||||
|
meekAzure('meek-azure'),
|
||||||
|
webtunnel('webtunnel');
|
||||||
|
|
||||||
|
const TransportType(this.value);
|
||||||
|
final String value;
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() => value;
|
||||||
|
|
||||||
|
static TransportType? fromString(String value) {
|
||||||
|
for (final transport in TransportType.values) {
|
||||||
|
if (transport.value == value) return transport;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@JsonSerializable()
|
||||||
|
class SettingsRequest {
|
||||||
|
final String? country;
|
||||||
|
@JsonKey(toJson: _transportsToJson, fromJson: _transportsFromJson)
|
||||||
|
final List<TransportType> transports;
|
||||||
|
|
||||||
|
const SettingsRequest({
|
||||||
|
this.country,
|
||||||
|
this.transports = const [
|
||||||
|
TransportType.obfs4,
|
||||||
|
TransportType.snowflake,
|
||||||
|
TransportType.webtunnel,
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
static List<String> _transportsToJson(List<TransportType> transports) =>
|
||||||
|
transports.map((t) => t.value).toList();
|
||||||
|
|
||||||
|
static List<TransportType> _transportsFromJson(List<dynamic> json) => json
|
||||||
|
.cast<String>()
|
||||||
|
.map((s) => TransportType.fromString(s))
|
||||||
|
.where((t) => t != null)
|
||||||
|
.cast<TransportType>()
|
||||||
|
.toList();
|
||||||
|
|
||||||
|
factory SettingsRequest.fromJson(Map<String, dynamic> json) =>
|
||||||
|
_$SettingsRequestFromJson(json);
|
||||||
|
|
||||||
|
Map<String, dynamic> toJson() => _$SettingsRequestToJson(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
@JsonSerializable()
|
||||||
|
class SettingsResponse {
|
||||||
|
final List<Setting>? settings;
|
||||||
|
final String? country;
|
||||||
|
final List<MoatError>? errors;
|
||||||
|
|
||||||
|
const SettingsResponse({this.settings, this.country, this.errors});
|
||||||
|
|
||||||
|
factory SettingsResponse.fromJson(Map<String, dynamic> json) =>
|
||||||
|
_$SettingsResponseFromJson(json);
|
||||||
|
|
||||||
|
Map<String, dynamic> toJson() => _$SettingsResponseToJson(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
@JsonSerializable()
|
||||||
|
class Setting {
|
||||||
|
@JsonKey(name: 'bridges')
|
||||||
|
final Bridge bridge;
|
||||||
|
|
||||||
|
const Setting({required this.bridge});
|
||||||
|
|
||||||
|
factory Setting.fromJson(Map<String, dynamic> json) =>
|
||||||
|
_$SettingFromJson(json);
|
||||||
|
|
||||||
|
Map<String, dynamic> toJson() => _$SettingToJson(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
@JsonSerializable()
|
||||||
|
class Bridge {
|
||||||
|
final String type;
|
||||||
|
final String source;
|
||||||
|
@JsonKey(name: 'bridge_strings')
|
||||||
|
final List<String>? bridges;
|
||||||
|
|
||||||
|
const Bridge({required this.type, required this.source, this.bridges});
|
||||||
|
|
||||||
|
factory Bridge.fromJson(Map<String, dynamic> json) => _$BridgeFromJson(json);
|
||||||
|
|
||||||
|
Map<String, dynamic> toJson() => _$BridgeToJson(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
@JsonSerializable()
|
||||||
|
class MoatError implements Exception {
|
||||||
|
final String? id;
|
||||||
|
final String? type;
|
||||||
|
final String? version;
|
||||||
|
final int? code;
|
||||||
|
final String? status;
|
||||||
|
final String? detail;
|
||||||
|
|
||||||
|
const MoatError({
|
||||||
|
this.id,
|
||||||
|
this.type,
|
||||||
|
this.version,
|
||||||
|
this.code,
|
||||||
|
this.status,
|
||||||
|
this.detail,
|
||||||
|
});
|
||||||
|
|
||||||
|
factory MoatError.fromJson(Map<String, dynamic> json) =>
|
||||||
|
_$MoatErrorFromJson(json);
|
||||||
|
|
||||||
|
Map<String, dynamic> toJson() => _$MoatErrorToJson(this);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() {
|
||||||
|
if (detail != null && detail!.isNotEmpty) {
|
||||||
|
return detail!;
|
||||||
|
}
|
||||||
|
return '$code $status';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@JsonSerializable()
|
||||||
|
class BuiltInBridges {
|
||||||
|
final List<String> meek;
|
||||||
|
|
||||||
|
@JsonKey(name: 'meek-azure')
|
||||||
|
final List<String> meekAzure;
|
||||||
|
|
||||||
|
final List<String> obfs4;
|
||||||
|
final List<String> snowflake;
|
||||||
|
|
||||||
|
const BuiltInBridges({
|
||||||
|
required this.meek,
|
||||||
|
required this.meekAzure,
|
||||||
|
required this.obfs4,
|
||||||
|
required this.snowflake,
|
||||||
|
});
|
||||||
|
|
||||||
|
factory BuiltInBridges.fromJson(Map<String, dynamic> json) =>
|
||||||
|
_$BuiltInBridgesFromJson(json);
|
||||||
|
|
||||||
|
Map<String, dynamic> toJson() => _$BuiltInBridgesToJson(this);
|
||||||
|
}
|
||||||
@@ -0,0 +1,102 @@
|
|||||||
|
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||||
|
|
||||||
|
part of 'moat.dart';
|
||||||
|
|
||||||
|
// **************************************************************************
|
||||||
|
// JsonSerializableGenerator
|
||||||
|
// **************************************************************************
|
||||||
|
|
||||||
|
SettingsRequest _$SettingsRequestFromJson(Map<String, dynamic> json) =>
|
||||||
|
SettingsRequest(
|
||||||
|
country: json['country'] as String?,
|
||||||
|
transports: json['transports'] == null
|
||||||
|
? const [
|
||||||
|
TransportType.obfs4,
|
||||||
|
TransportType.snowflake,
|
||||||
|
TransportType.webtunnel,
|
||||||
|
]
|
||||||
|
: SettingsRequest._transportsFromJson(json['transports'] as List),
|
||||||
|
);
|
||||||
|
|
||||||
|
Map<String, dynamic> _$SettingsRequestToJson(SettingsRequest instance) =>
|
||||||
|
<String, dynamic>{
|
||||||
|
'country': instance.country,
|
||||||
|
'transports': SettingsRequest._transportsToJson(instance.transports),
|
||||||
|
};
|
||||||
|
|
||||||
|
SettingsResponse _$SettingsResponseFromJson(Map<String, dynamic> json) =>
|
||||||
|
SettingsResponse(
|
||||||
|
settings: (json['settings'] as List<dynamic>?)
|
||||||
|
?.map((e) => Setting.fromJson(e as Map<String, dynamic>))
|
||||||
|
.toList(),
|
||||||
|
country: json['country'] as String?,
|
||||||
|
errors: (json['errors'] as List<dynamic>?)
|
||||||
|
?.map((e) => MoatError.fromJson(e as Map<String, dynamic>))
|
||||||
|
.toList(),
|
||||||
|
);
|
||||||
|
|
||||||
|
Map<String, dynamic> _$SettingsResponseToJson(SettingsResponse instance) =>
|
||||||
|
<String, dynamic>{
|
||||||
|
'settings': instance.settings,
|
||||||
|
'country': instance.country,
|
||||||
|
'errors': instance.errors,
|
||||||
|
};
|
||||||
|
|
||||||
|
Setting _$SettingFromJson(Map<String, dynamic> json) =>
|
||||||
|
Setting(bridge: Bridge.fromJson(json['bridges'] as Map<String, dynamic>));
|
||||||
|
|
||||||
|
Map<String, dynamic> _$SettingToJson(Setting instance) => <String, dynamic>{
|
||||||
|
'bridges': instance.bridge,
|
||||||
|
};
|
||||||
|
|
||||||
|
Bridge _$BridgeFromJson(Map<String, dynamic> json) => Bridge(
|
||||||
|
type: json['type'] as String,
|
||||||
|
source: json['source'] as String,
|
||||||
|
bridges: (json['bridge_strings'] as List<dynamic>?)
|
||||||
|
?.map((e) => e as String)
|
||||||
|
.toList(),
|
||||||
|
);
|
||||||
|
|
||||||
|
Map<String, dynamic> _$BridgeToJson(Bridge instance) => <String, dynamic>{
|
||||||
|
'type': instance.type,
|
||||||
|
'source': instance.source,
|
||||||
|
'bridge_strings': instance.bridges,
|
||||||
|
};
|
||||||
|
|
||||||
|
MoatError _$MoatErrorFromJson(Map<String, dynamic> json) => MoatError(
|
||||||
|
id: json['id'] as String?,
|
||||||
|
type: json['type'] as String?,
|
||||||
|
version: json['version'] as String?,
|
||||||
|
code: (json['code'] as num?)?.toInt(),
|
||||||
|
status: json['status'] as String?,
|
||||||
|
detail: json['detail'] as String?,
|
||||||
|
);
|
||||||
|
|
||||||
|
Map<String, dynamic> _$MoatErrorToJson(MoatError instance) => <String, dynamic>{
|
||||||
|
'id': instance.id,
|
||||||
|
'type': instance.type,
|
||||||
|
'version': instance.version,
|
||||||
|
'code': instance.code,
|
||||||
|
'status': instance.status,
|
||||||
|
'detail': instance.detail,
|
||||||
|
};
|
||||||
|
|
||||||
|
BuiltInBridges _$BuiltInBridgesFromJson(Map<String, dynamic> json) =>
|
||||||
|
BuiltInBridges(
|
||||||
|
meek: (json['meek'] as List<dynamic>).map((e) => e as String).toList(),
|
||||||
|
meekAzure: (json['meek-azure'] as List<dynamic>)
|
||||||
|
.map((e) => e as String)
|
||||||
|
.toList(),
|
||||||
|
obfs4: (json['obfs4'] as List<dynamic>).map((e) => e as String).toList(),
|
||||||
|
snowflake: (json['snowflake'] as List<dynamic>)
|
||||||
|
.map((e) => e as String)
|
||||||
|
.toList(),
|
||||||
|
);
|
||||||
|
|
||||||
|
Map<String, dynamic> _$BuiltInBridgesToJson(BuiltInBridges instance) =>
|
||||||
|
<String, dynamic>{
|
||||||
|
'meek': instance.meek,
|
||||||
|
'meek-azure': instance.meekAzure,
|
||||||
|
'obfs4': instance.obfs4,
|
||||||
|
'snowflake': instance.snowflake,
|
||||||
|
};
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
import 'dart:convert';
|
||||||
|
|
||||||
|
import 'package:http/http.dart' as http;
|
||||||
|
import 'package:pluggable_transports_proxy/src/data/models/moat.dart';
|
||||||
|
|
||||||
|
class MoatApi {
|
||||||
|
static const String _baseUrl =
|
||||||
|
'https://bridges.torproject.org/moat/circumvention/';
|
||||||
|
|
||||||
|
final http.Client _client;
|
||||||
|
|
||||||
|
MoatApi(this._client);
|
||||||
|
|
||||||
|
Future<SettingsResponse> settings([SettingsRequest? request]) async {
|
||||||
|
final response = await _post('settings', request ?? SettingsRequest());
|
||||||
|
return SettingsResponse.fromJson(
|
||||||
|
jsonDecode(response.body) as Map<String, dynamic>,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<SettingsResponse> defaults([SettingsRequest? request]) async {
|
||||||
|
final response = await _post('defaults', request ?? SettingsRequest());
|
||||||
|
return SettingsResponse.fromJson(
|
||||||
|
jsonDecode(response.body) as Map<String, dynamic>,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<Map<String, SettingsResponse>> map() async {
|
||||||
|
final response = await _get('map');
|
||||||
|
final Map<String, dynamic> data =
|
||||||
|
jsonDecode(response.body) as Map<String, dynamic>;
|
||||||
|
return data.map(
|
||||||
|
(key, value) => MapEntry(
|
||||||
|
key,
|
||||||
|
SettingsResponse.fromJson(value as Map<String, dynamic>),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<BuiltInBridges> builtin() async {
|
||||||
|
final response = await _get('builtin');
|
||||||
|
return BuiltInBridges.fromJson(
|
||||||
|
jsonDecode(response.body) as Map<String, dynamic>,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<List<String>> countries() async {
|
||||||
|
final response = await _get('countries');
|
||||||
|
return List<String>.from(jsonDecode(response.body) as List);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<http.Response> _get(String endpoint) async {
|
||||||
|
final uri = Uri.parse('$_baseUrl$endpoint');
|
||||||
|
return await _client.get(uri, headers: _headers);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<http.Response> _post(String endpoint, Object body) async {
|
||||||
|
final uri = Uri.parse('$_baseUrl$endpoint');
|
||||||
|
return await _client.post(uri, headers: _headers, body: jsonEncode(body));
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, String> get _headers => {
|
||||||
|
'Content-Type': 'application/vnd.api+json',
|
||||||
|
'Accept': 'application/vnd.api+json',
|
||||||
|
};
|
||||||
|
|
||||||
|
void dispose() {
|
||||||
|
_client.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,160 @@
|
|||||||
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:http/io_client.dart';
|
||||||
|
import 'package:pluggable_transports_proxy/pluggable_transports_proxy.dart';
|
||||||
|
import 'package:pluggable_transports_proxy/src/data/models/moat.dart';
|
||||||
|
import 'package:pluggable_transports_proxy/src/data/service/moat_api.dart';
|
||||||
|
import 'package:socks5_proxy/socks_client.dart';
|
||||||
|
|
||||||
|
const String _meekParameters =
|
||||||
|
'url=https://1723079976.rsc.cdn77.org;front=www.phpmyadmin.net';
|
||||||
|
|
||||||
|
/// Manages MOAT API connections with persistent proxy setup.
|
||||||
|
class MoatService {
|
||||||
|
MoatApi? _api;
|
||||||
|
|
||||||
|
/// Initializes the MeekLite proxy and MOAT API connection.
|
||||||
|
/// Must be called before using other methods.
|
||||||
|
Future<void> initialize() async {
|
||||||
|
if (_api != null) return;
|
||||||
|
|
||||||
|
final port = await IPtProxyController().start(ProxyType.meekLite, "");
|
||||||
|
|
||||||
|
final httpClient = HttpClient();
|
||||||
|
SocksTCPClient.assignToHttpClient(httpClient, [
|
||||||
|
ProxySettings(
|
||||||
|
InternetAddress.loopbackIPv4,
|
||||||
|
port,
|
||||||
|
username: _meekParameters,
|
||||||
|
password: '\u0000',
|
||||||
|
),
|
||||||
|
]);
|
||||||
|
|
||||||
|
_api = MoatApi(IOClient(httpClient));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Disposes the API connection and stops the MeekLite proxy.
|
||||||
|
/// Should be called when done using the client.
|
||||||
|
Future<void> dispose() async {
|
||||||
|
if (_api == null) return;
|
||||||
|
|
||||||
|
_api!.dispose();
|
||||||
|
await IPtProxyController().stop(ProxyType.meekLite);
|
||||||
|
_api = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Ensures the client is initialized before API calls.
|
||||||
|
void _ensureInitialized() {
|
||||||
|
if (_api == null) {
|
||||||
|
throw StateError(
|
||||||
|
'MoatClient must be initialized before use. Call initialize() first.',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Handles MOAT API errors and determines if PT is required.
|
||||||
|
bool _handleMoatErrors(List<MoatError>? errors) {
|
||||||
|
if (errors == null || errors.isEmpty) return false;
|
||||||
|
|
||||||
|
final error = errors.first;
|
||||||
|
if (error.code == 404 || error.code == 406) {
|
||||||
|
// 404: Needs transport, but not the available ones
|
||||||
|
// 406: No country from IP address
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Converts BuiltInBridges to Settings for requested transports.
|
||||||
|
List<Setting> _convertBuiltinToSettings(
|
||||||
|
BuiltInBridges builtinBridges,
|
||||||
|
List<TransportType> transports,
|
||||||
|
) {
|
||||||
|
final settings = <Setting>[];
|
||||||
|
|
||||||
|
for (final transport in transports) {
|
||||||
|
final bridgeStrings = switch (transport) {
|
||||||
|
TransportType.obfs4 => builtinBridges.obfs4,
|
||||||
|
TransportType.snowflake => builtinBridges.snowflake,
|
||||||
|
TransportType.meek => builtinBridges.meek,
|
||||||
|
TransportType.meekAzure => builtinBridges.meekAzure,
|
||||||
|
TransportType.webtunnel => <String>[], // Not available in builtin
|
||||||
|
};
|
||||||
|
|
||||||
|
if (bridgeStrings.isNotEmpty) {
|
||||||
|
settings.add(
|
||||||
|
Setting(
|
||||||
|
bridge: Bridge(
|
||||||
|
type: transport.value,
|
||||||
|
source: 'builtin',
|
||||||
|
bridges: bridgeStrings,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return settings;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Gets built-in bridges from the MOAT service endpoint.
|
||||||
|
Future<List<Setting>?> getBuiltinBridges({
|
||||||
|
List<TransportType> transports = const [
|
||||||
|
TransportType.obfs4,
|
||||||
|
TransportType.snowflake,
|
||||||
|
TransportType.meek,
|
||||||
|
],
|
||||||
|
}) async {
|
||||||
|
_ensureInitialized();
|
||||||
|
|
||||||
|
final builtinBridges = await _api!.builtin();
|
||||||
|
final settings = _convertBuiltinToSettings(builtinBridges, transports);
|
||||||
|
return settings.isEmpty ? null : settings;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Tries to automatically configure Pluggable Transports.
|
||||||
|
Future<List<Setting>?> autoConf({
|
||||||
|
String? country,
|
||||||
|
bool cannotConnectWithoutPt = false,
|
||||||
|
List<TransportType> transports = const [
|
||||||
|
TransportType.obfs4,
|
||||||
|
TransportType.snowflake,
|
||||||
|
TransportType.webtunnel,
|
||||||
|
],
|
||||||
|
}) async {
|
||||||
|
_ensureInitialized();
|
||||||
|
|
||||||
|
bool localCannotConnectWithoutPt = cannotConnectWithoutPt;
|
||||||
|
final request = SettingsRequest(country: country, transports: transports);
|
||||||
|
|
||||||
|
var response = await _api!.settings(request);
|
||||||
|
|
||||||
|
if (_handleMoatErrors(response.errors)) {
|
||||||
|
localCannotConnectWithoutPt = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
final hasSettings = response.settings?.isNotEmpty ?? false;
|
||||||
|
if (!hasSettings && !localCannotConnectWithoutPt) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasSettings) {
|
||||||
|
return response.settings;
|
||||||
|
}
|
||||||
|
|
||||||
|
response = await _api!.defaults(SettingsRequest(transports: transports));
|
||||||
|
return response.settings;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Gets the list of supported countries from the MOAT service.
|
||||||
|
Future<List<String>> getCountries() async {
|
||||||
|
_ensureInitialized();
|
||||||
|
return await _api!.countries();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Gets the country-to-settings map from the MOAT service.
|
||||||
|
Future<Map<String, SettingsResponse>> getMap() async {
|
||||||
|
_ensureInitialized();
|
||||||
|
return await _api!.map();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -11,10 +11,18 @@ environment:
|
|||||||
dependencies:
|
dependencies:
|
||||||
flutter:
|
flutter:
|
||||||
sdk: flutter
|
sdk: flutter
|
||||||
|
http: ^1.5.0
|
||||||
|
json_annotation: ^4.9.0
|
||||||
|
socks5_proxy:
|
||||||
|
git:
|
||||||
|
url: https://github.com/sylvieon/socks_dart.git
|
||||||
|
ref: 04550bc08f42cc1a35f1e870f651b93a4f9aceab
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
|
build_runner: ^2.4.15
|
||||||
flutter_test:
|
flutter_test:
|
||||||
sdk: flutter
|
sdk: flutter
|
||||||
|
json_serializable: ^6.9.5
|
||||||
lint: ^2.8.0
|
lint: ^2.8.0
|
||||||
pigeon: ^26.0.1
|
pigeon: ^26.0.1
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user