Files
vpn_server/bin/vpn_server.dart
2026-04-19 23:37:52 +03:00

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;
}
}