184 lines
5.2 KiB
Dart
184 lines
5.2 KiB
Dart
import 'dart:convert';
|
|
import 'dart:io';
|
|
|
|
import 'package:sqlite3/sqlite3.dart';
|
|
|
|
const seedLinks = [
|
|
'vless://adbb9513-991a-4d64-9b30-1bf2283e7ed8@93.77.185.114:444?security=reality&encryption=none&pbk=M_VX89rLtCxGh45cRzXITGBgV3HTxW5c2zOEvqHFbSs&headerType=none&fp=random&spx=%2F&type=tcp&sni=www.apple.com&sid=f33f#6uuknvqv',
|
|
'vless://833a8f71-2b99-4e69-b39a-6d242c82fabb@147.45.145.102:444?security=reality&encryption=none&pbk=jQ7nhZoFKsFAwl8lhR4g5rBl_PT_-BA_lQmt1kG3EAs&headerType=&fp=random&spx=%2F&type=tcp&sni=ya.ru&sid=5a#vless_1-zxdrfypi4',
|
|
];
|
|
|
|
Future<void> main() async {
|
|
final db = _initDatabase();
|
|
|
|
ProcessSignal.sigint.watch().listen((_) {
|
|
db.dispose();
|
|
exit(0);
|
|
});
|
|
|
|
final server = await HttpServer.bind(InternetAddress.anyIPv4, 8080);
|
|
print('Server started on http://${server.address.address}:${server.port}');
|
|
|
|
await for (final request in server) {
|
|
final isConnectionsRoute = request.uri.path == '/connections';
|
|
|
|
if (!isConnectionsRoute) {
|
|
request.response
|
|
..statusCode = HttpStatus.notFound
|
|
..write('Not Found');
|
|
await request.response.close();
|
|
continue;
|
|
}
|
|
|
|
if (request.method == 'GET') {
|
|
final result = db.select('SELECT url FROM connections ORDER BY id ASC');
|
|
final links = result.map((row) => row['url'] as String).toList();
|
|
|
|
request.response
|
|
..statusCode = HttpStatus.ok
|
|
..headers.contentType = ContentType.json
|
|
..write(jsonEncode({'links': links}));
|
|
await request.response.close();
|
|
continue;
|
|
}
|
|
|
|
if (request.method == 'POST') {
|
|
await _handleCreateConnection(request, db);
|
|
continue;
|
|
}
|
|
|
|
if (request.method == 'DELETE') {
|
|
await _handleDeleteConnection(request, db);
|
|
continue;
|
|
}
|
|
|
|
request.response
|
|
..statusCode = HttpStatus.methodNotAllowed
|
|
..headers.set(HttpHeaders.allowHeader, 'GET, POST, DELETE')
|
|
..headers.contentType = ContentType.json
|
|
..write(jsonEncode({'error': 'Method Not Allowed'}));
|
|
await request.response.close();
|
|
}
|
|
}
|
|
|
|
Database _initDatabase() {
|
|
final dataDir = Directory('data');
|
|
if (!dataDir.existsSync()) {
|
|
dataDir.createSync(recursive: true);
|
|
}
|
|
|
|
final db = sqlite3.open('data/vpn_links.db');
|
|
db.execute('''
|
|
CREATE TABLE IF NOT EXISTS connections (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
url TEXT NOT NULL UNIQUE,
|
|
created_at TEXT NOT NULL
|
|
)
|
|
''');
|
|
|
|
final insert = db.prepare(
|
|
'INSERT OR IGNORE INTO connections(url, created_at) VALUES (?, ?)',
|
|
);
|
|
final createdAt = DateTime.now().toUtc().toIso8601String();
|
|
for (final link in seedLinks) {
|
|
insert.execute([link, createdAt]);
|
|
}
|
|
insert.dispose();
|
|
|
|
return db;
|
|
}
|
|
|
|
Future<void> _handleCreateConnection(HttpRequest request, Database db) async {
|
|
final body = await utf8.decoder.bind(request).join();
|
|
final url = _extractUrl(body);
|
|
|
|
if (url == null) {
|
|
request.response
|
|
..statusCode = HttpStatus.badRequest
|
|
..headers.contentType = ContentType.json
|
|
..write(jsonEncode({'error': 'Body must be JSON: {"url": "..."}'}));
|
|
await request.response.close();
|
|
return;
|
|
}
|
|
|
|
try {
|
|
db.execute(
|
|
'INSERT INTO connections(url, created_at) VALUES (?, ?)',
|
|
[url, DateTime.now().toUtc().toIso8601String()],
|
|
);
|
|
|
|
request.response
|
|
..statusCode = HttpStatus.created
|
|
..headers.contentType = ContentType.json
|
|
..write(jsonEncode({'message': 'Link added'}));
|
|
await request.response.close();
|
|
} on SqliteException catch (e) {
|
|
if (e.extendedResultCode == 2067) {
|
|
request.response
|
|
..statusCode = HttpStatus.conflict
|
|
..headers.contentType = ContentType.json
|
|
..write(jsonEncode({'error': 'Link already exists'}));
|
|
await request.response.close();
|
|
return;
|
|
}
|
|
|
|
request.response
|
|
..statusCode = HttpStatus.internalServerError
|
|
..headers.contentType = ContentType.json
|
|
..write(jsonEncode({'error': 'Database error'}));
|
|
await request.response.close();
|
|
}
|
|
}
|
|
|
|
Future<void> _handleDeleteConnection(HttpRequest request, Database db) async {
|
|
final body = await utf8.decoder.bind(request).join();
|
|
final url = _extractUrl(body);
|
|
|
|
if (url == null) {
|
|
request.response
|
|
..statusCode = HttpStatus.badRequest
|
|
..headers.contentType = ContentType.json
|
|
..write(jsonEncode({'error': 'Body must be JSON: {"url": "..."}'}));
|
|
await request.response.close();
|
|
return;
|
|
}
|
|
|
|
final delete = db.prepare('DELETE FROM connections WHERE url = ?');
|
|
delete.execute([url]);
|
|
final changes = db.updatedRows;
|
|
delete.dispose();
|
|
|
|
if (changes == 0) {
|
|
request.response
|
|
..statusCode = HttpStatus.notFound
|
|
..headers.contentType = ContentType.json
|
|
..write(jsonEncode({'error': 'Link not found'}));
|
|
await request.response.close();
|
|
return;
|
|
}
|
|
|
|
request.response
|
|
..statusCode = HttpStatus.ok
|
|
..headers.contentType = ContentType.json
|
|
..write(jsonEncode({'message': 'Link deleted'}));
|
|
await request.response.close();
|
|
}
|
|
|
|
String? _extractUrl(String body) {
|
|
try {
|
|
final json = jsonDecode(body);
|
|
if (json is! Map<String, dynamic>) {
|
|
return null;
|
|
}
|
|
|
|
final url = json['url'];
|
|
if (url is! String || url.trim().isEmpty) {
|
|
return null;
|
|
}
|
|
|
|
return url.trim();
|
|
} catch (_) {
|
|
return null;
|
|
}
|
|
}
|