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;
|
||||
|
||||
@@ -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:
|
||||
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:
|
||||
build_runner: ^2.4.15
|
||||
flutter_test:
|
||||
sdk: flutter
|
||||
json_serializable: ^6.9.5
|
||||
lint: ^2.8.0
|
||||
pigeon: ^26.0.1
|
||||
|
||||
|
||||
Reference in New Issue
Block a user