2023-09-01 15:00:41 +03:30
|
|
|
import 'dart:convert';
|
2024-01-15 01:31:20 +03:30
|
|
|
import 'dart:io';
|
2023-09-01 15:00:41 +03:30
|
|
|
|
2023-08-19 22:27:23 +03:30
|
|
|
import 'package:flutter/services.dart';
|
|
|
|
|
import 'package:fpdart/fpdart.dart';
|
2023-12-01 12:56:24 +03:30
|
|
|
import 'package:hiddify/core/model/directories.dart';
|
|
|
|
|
import 'package:hiddify/singbox/model/singbox_config_option.dart';
|
|
|
|
|
import 'package:hiddify/singbox/model/singbox_outbound.dart';
|
|
|
|
|
import 'package:hiddify/singbox/model/singbox_stats.dart';
|
|
|
|
|
import 'package:hiddify/singbox/model/singbox_status.dart';
|
2024-02-18 12:35:11 +03:30
|
|
|
import 'package:hiddify/singbox/model/warp_account.dart';
|
2023-12-01 12:56:24 +03:30
|
|
|
import 'package:hiddify/singbox/service/singbox_service.dart';
|
|
|
|
|
import 'package:hiddify/utils/custom_loggers.dart';
|
2023-09-10 20:25:04 +03:30
|
|
|
import 'package:rxdart/rxdart.dart';
|
2023-08-19 22:27:23 +03:30
|
|
|
|
2024-03-17 19:20:59 +01:00
|
|
|
class PlatformSingboxService with InfraLogger implements SingboxService {
|
2024-02-13 17:02:10 +03:30
|
|
|
static const channelPrefix = "com.hiddify.app";
|
|
|
|
|
|
|
|
|
|
static const methodChannel = MethodChannel("$channelPrefix/method");
|
2024-07-29 13:11:51 +02:00
|
|
|
static const statusChannel = EventChannel("$channelPrefix/service.status", JSONMethodCodec());
|
|
|
|
|
static const alertsChannel = EventChannel("$channelPrefix/service.alerts", JSONMethodCodec());
|
|
|
|
|
static const statsChannel = EventChannel("$channelPrefix/stats", JSONMethodCodec());
|
2024-02-13 17:02:10 +03:30
|
|
|
static const groupsChannel = EventChannel("$channelPrefix/groups");
|
2024-07-29 13:11:51 +02:00
|
|
|
static const activeGroupsChannel = EventChannel("$channelPrefix/active-groups");
|
2024-02-13 17:02:10 +03:30
|
|
|
static const logsChannel = EventChannel("$channelPrefix/service.logs");
|
2023-09-10 20:25:04 +03:30
|
|
|
|
2023-12-01 12:56:24 +03:30
|
|
|
late final ValueStream<SingboxStatus> _status;
|
2023-09-10 20:25:04 +03:30
|
|
|
|
|
|
|
|
@override
|
|
|
|
|
Future<void> init() async {
|
|
|
|
|
loggy.debug("initializing");
|
2024-07-29 13:11:51 +02:00
|
|
|
final status = statusChannel.receiveBroadcastStream().map(SingboxStatus.fromEvent);
|
|
|
|
|
final alerts = alertsChannel.receiveBroadcastStream().map(SingboxStatus.fromEvent);
|
2023-12-26 22:26:06 +03:30
|
|
|
|
2023-12-01 12:56:24 +03:30
|
|
|
_status = ValueConnectableStream(Rx.merge([status, alerts])).autoConnect();
|
|
|
|
|
await _status.first;
|
2023-09-10 20:25:04 +03:30
|
|
|
}
|
2023-08-19 22:27:23 +03:30
|
|
|
|
|
|
|
|
@override
|
2024-02-13 17:02:10 +03:30
|
|
|
TaskEither<String, Unit> setup(Directories directories, bool debug) {
|
2024-01-15 01:31:20 +03:30
|
|
|
return TaskEither(
|
|
|
|
|
() async {
|
2024-03-17 19:20:59 +01:00
|
|
|
if (!Platform.isIOS) {
|
|
|
|
|
return right(unit);
|
|
|
|
|
}
|
2023-08-19 22:27:23 +03:30
|
|
|
|
2024-03-17 19:20:59 +01:00
|
|
|
await methodChannel.invokeMethod("setup");
|
2024-03-15 11:43:13 +03:30
|
|
|
return right(unit);
|
2023-08-19 22:27:23 +03:30
|
|
|
},
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2024-03-17 19:20:59 +01:00
|
|
|
@override
|
|
|
|
|
TaskEither<String, Unit> validateConfigByPath(
|
|
|
|
|
String path,
|
|
|
|
|
String tempPath,
|
|
|
|
|
bool debug,
|
|
|
|
|
) {
|
|
|
|
|
return TaskEither(
|
|
|
|
|
() async {
|
|
|
|
|
final message = await methodChannel.invokeMethod<String>(
|
|
|
|
|
"parse_config",
|
|
|
|
|
{"path": path, "tempPath": tempPath, "debug": debug},
|
|
|
|
|
);
|
|
|
|
|
if (message == null || message.isEmpty) return right(unit);
|
|
|
|
|
return left(message);
|
|
|
|
|
},
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2023-09-01 15:00:41 +03:30
|
|
|
@override
|
2023-12-01 12:56:24 +03:30
|
|
|
TaskEither<String, Unit> changeOptions(SingboxConfigOption options) {
|
2023-09-01 15:00:41 +03:30
|
|
|
return TaskEither(
|
|
|
|
|
() async {
|
2024-02-13 17:02:10 +03:30
|
|
|
loggy.debug("changing options");
|
|
|
|
|
await methodChannel.invokeMethod(
|
2023-09-01 15:00:41 +03:30
|
|
|
"change_config_options",
|
2024-02-17 11:58:34 +03:30
|
|
|
jsonEncode(options.toJson()),
|
2023-09-01 15:00:41 +03:30
|
|
|
);
|
|
|
|
|
return right(unit);
|
|
|
|
|
},
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2024-03-17 19:20:59 +01:00
|
|
|
@override
|
|
|
|
|
TaskEither<String, String> generateFullConfigByPath(String path) {
|
|
|
|
|
return TaskEither(
|
|
|
|
|
() async {
|
|
|
|
|
loggy.debug("generating full config by path");
|
|
|
|
|
final configJson = await methodChannel.invokeMethod<String>(
|
|
|
|
|
"generate_config",
|
|
|
|
|
{"path": path},
|
|
|
|
|
);
|
|
|
|
|
if (configJson == null || configJson.isEmpty) {
|
|
|
|
|
return left("null response");
|
|
|
|
|
}
|
|
|
|
|
return right(configJson);
|
|
|
|
|
},
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2023-08-19 22:27:23 +03:30
|
|
|
@override
|
2023-12-14 14:50:10 +03:30
|
|
|
TaskEither<String, Unit> start(
|
|
|
|
|
String path,
|
|
|
|
|
String name,
|
|
|
|
|
bool disableMemoryLimit,
|
|
|
|
|
) {
|
2023-08-19 22:27:23 +03:30
|
|
|
return TaskEither(
|
|
|
|
|
() async {
|
2023-09-10 20:25:04 +03:30
|
|
|
loggy.debug("starting");
|
2024-02-13 17:02:10 +03:30
|
|
|
await methodChannel.invokeMethod(
|
2023-09-10 20:25:04 +03:30
|
|
|
"start",
|
2023-12-14 14:50:10 +03:30
|
|
|
{"path": path, "name": name},
|
2023-08-19 22:27:23 +03:30
|
|
|
);
|
|
|
|
|
return right(unit);
|
|
|
|
|
},
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@override
|
2023-09-10 20:25:04 +03:30
|
|
|
TaskEither<String, Unit> stop() {
|
2023-08-19 22:27:23 +03:30
|
|
|
return TaskEither(
|
|
|
|
|
() async {
|
2023-09-10 20:25:04 +03:30
|
|
|
loggy.debug("stopping");
|
2024-02-13 17:02:10 +03:30
|
|
|
await methodChannel.invokeMethod("stop");
|
2023-08-19 22:27:23 +03:30
|
|
|
return right(unit);
|
|
|
|
|
},
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@override
|
2023-12-14 14:50:10 +03:30
|
|
|
TaskEither<String, Unit> restart(
|
|
|
|
|
String path,
|
|
|
|
|
String name,
|
|
|
|
|
bool disableMemoryLimit,
|
|
|
|
|
) {
|
2023-08-19 22:27:23 +03:30
|
|
|
return TaskEither(
|
|
|
|
|
() async {
|
2023-09-10 20:25:04 +03:30
|
|
|
loggy.debug("restarting");
|
2024-02-13 17:02:10 +03:30
|
|
|
await methodChannel.invokeMethod(
|
2023-09-10 20:25:04 +03:30
|
|
|
"restart",
|
2023-12-14 14:50:10 +03:30
|
|
|
{"path": path, "name": name},
|
2023-09-10 20:25:04 +03:30
|
|
|
);
|
2023-08-19 22:27:23 +03:30
|
|
|
return right(unit);
|
|
|
|
|
},
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2024-01-18 22:53:17 +03:30
|
|
|
@override
|
|
|
|
|
TaskEither<String, Unit> resetTunnel() {
|
|
|
|
|
return TaskEither(
|
|
|
|
|
() async {
|
|
|
|
|
// only available on iOS (and macOS later)
|
|
|
|
|
if (!Platform.isIOS) {
|
|
|
|
|
throw UnimplementedError(
|
|
|
|
|
"reset tunnel function unavailable on platform",
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
loggy.debug("resetting tunnel");
|
2024-02-13 17:02:10 +03:30
|
|
|
await methodChannel.invokeMethod("reset");
|
2024-01-18 22:53:17 +03:30
|
|
|
return right(unit);
|
|
|
|
|
},
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2023-08-29 19:32:31 +03:30
|
|
|
@override
|
2024-02-13 17:02:10 +03:30
|
|
|
Stream<List<SingboxOutboundGroup>> watchGroups() {
|
|
|
|
|
loggy.debug("watching groups");
|
|
|
|
|
return groupsChannel.receiveBroadcastStream().map(
|
2023-08-29 19:32:31 +03:30
|
|
|
(event) {
|
|
|
|
|
if (event case String _) {
|
2023-12-01 12:56:24 +03:30
|
|
|
return (jsonDecode(event) as List).map((e) {
|
|
|
|
|
return SingboxOutboundGroup.fromJson(e as Map<String, dynamic>);
|
|
|
|
|
}).toList();
|
2023-08-29 19:32:31 +03:30
|
|
|
}
|
2023-10-05 22:47:24 +03:30
|
|
|
loggy.error("[group client] unexpected type, msg: $event");
|
2023-08-29 19:32:31 +03:30
|
|
|
throw "invalid type";
|
|
|
|
|
},
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2024-02-09 12:02:52 +03:30
|
|
|
@override
|
2024-02-13 17:02:10 +03:30
|
|
|
Stream<List<SingboxOutboundGroup>> watchActiveGroups() {
|
|
|
|
|
loggy.debug("watching active groups");
|
|
|
|
|
return activeGroupsChannel.receiveBroadcastStream().map(
|
2024-02-09 12:02:52 +03:30
|
|
|
(event) {
|
|
|
|
|
if (event case String _) {
|
|
|
|
|
return (jsonDecode(event) as List).map((e) {
|
|
|
|
|
return SingboxOutboundGroup.fromJson(e as Map<String, dynamic>);
|
|
|
|
|
}).toList();
|
|
|
|
|
}
|
|
|
|
|
loggy.error("[active group client] unexpected type, msg: $event");
|
|
|
|
|
throw "invalid type";
|
|
|
|
|
},
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2023-08-28 13:15:57 +03:30
|
|
|
@override
|
2023-12-01 12:56:24 +03:30
|
|
|
Stream<SingboxStatus> watchStatus() => _status;
|
2023-09-10 20:25:04 +03:30
|
|
|
|
|
|
|
|
@override
|
2023-12-01 12:56:24 +03:30
|
|
|
Stream<SingboxStats> watchStats() {
|
2023-12-14 12:12:12 +03:30
|
|
|
loggy.debug("watching stats");
|
2024-02-13 17:02:10 +03:30
|
|
|
return statsChannel.receiveBroadcastStream().map(
|
2023-12-14 12:12:12 +03:30
|
|
|
(event) {
|
|
|
|
|
if (event case Map<String, dynamic> _) {
|
|
|
|
|
return SingboxStats.fromJson(event);
|
|
|
|
|
}
|
2024-01-18 23:07:29 +03:30
|
|
|
loggy.error(
|
|
|
|
|
"[stats client] unexpected type(${event.runtimeType}), msg: $event",
|
|
|
|
|
);
|
2023-12-14 12:12:12 +03:30
|
|
|
throw "invalid type";
|
|
|
|
|
},
|
|
|
|
|
);
|
2023-08-28 13:15:57 +03:30
|
|
|
}
|
|
|
|
|
|
2023-08-29 19:32:31 +03:30
|
|
|
@override
|
|
|
|
|
TaskEither<String, Unit> selectOutbound(String groupTag, String outboundTag) {
|
|
|
|
|
return TaskEither(
|
|
|
|
|
() async {
|
|
|
|
|
loggy.debug("selecting outbound");
|
2024-02-13 17:02:10 +03:30
|
|
|
await methodChannel.invokeMethod(
|
2023-08-29 19:32:31 +03:30
|
|
|
"select_outbound",
|
|
|
|
|
{"groupTag": groupTag, "outboundTag": outboundTag},
|
|
|
|
|
);
|
|
|
|
|
return right(unit);
|
|
|
|
|
},
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@override
|
|
|
|
|
TaskEither<String, Unit> urlTest(String groupTag) {
|
|
|
|
|
return TaskEither(
|
|
|
|
|
() async {
|
2024-02-13 17:02:10 +03:30
|
|
|
await methodChannel.invokeMethod(
|
2023-08-29 19:32:31 +03:30
|
|
|
"url_test",
|
|
|
|
|
{"groupTag": groupTag},
|
|
|
|
|
);
|
|
|
|
|
return right(unit);
|
|
|
|
|
},
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2023-08-19 22:27:23 +03:30
|
|
|
@override
|
2023-10-24 11:26:57 +02:00
|
|
|
Stream<List<String>> watchLogs(String path) async* {
|
2024-07-29 13:11:51 +02:00
|
|
|
yield* logsChannel.receiveBroadcastStream().map((event) => (event as List).map((e) => e as String).toList());
|
2023-10-24 11:26:57 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@override
|
|
|
|
|
TaskEither<String, Unit> clearLogs() {
|
|
|
|
|
return TaskEither(
|
|
|
|
|
() async {
|
2024-02-13 17:02:10 +03:30
|
|
|
await methodChannel.invokeMethod("clear_logs");
|
2023-10-24 11:26:57 +02:00
|
|
|
return right(unit);
|
2023-08-19 22:27:23 +03:30
|
|
|
},
|
|
|
|
|
);
|
|
|
|
|
}
|
2024-02-18 12:35:11 +03:30
|
|
|
|
|
|
|
|
@override
|
2024-02-20 22:16:47 +03:30
|
|
|
TaskEither<String, WarpResponse> generateWarpConfig({
|
2024-02-18 12:35:11 +03:30
|
|
|
required String licenseKey,
|
|
|
|
|
required String previousAccountId,
|
|
|
|
|
required String previousAccessToken,
|
|
|
|
|
}) {
|
|
|
|
|
return TaskEither(
|
|
|
|
|
() async {
|
|
|
|
|
loggy.debug("generating warp config");
|
|
|
|
|
final warpConfig = await methodChannel.invokeMethod(
|
|
|
|
|
"generate_warp_config",
|
|
|
|
|
{
|
|
|
|
|
"license-key": licenseKey,
|
|
|
|
|
"previous-account-id": previousAccountId,
|
|
|
|
|
"previous-access-token": previousAccessToken,
|
|
|
|
|
},
|
|
|
|
|
);
|
2024-02-20 22:16:47 +03:30
|
|
|
return right(warpFromJson(jsonDecode(warpConfig as String)));
|
2024-02-18 12:35:11 +03:30
|
|
|
},
|
|
|
|
|
);
|
|
|
|
|
}
|
2023-08-19 22:27:23 +03:30
|
|
|
}
|