Implement authentication middleware and rolling codes service for secure API access
This commit is contained in:
132
lib/services/rolling_codes_service.dart
Normal file
132
lib/services/rolling_codes_service.dart
Normal file
@@ -0,0 +1,132 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:typed_data';
|
||||
import 'dart:io';
|
||||
import 'package:crypto/crypto.dart';
|
||||
import 'package:waylume_server/core/utils.dart';
|
||||
|
||||
import '../config/supabase_config.dart';
|
||||
|
||||
class RollingCodesService {
|
||||
// Shared constants (these should match edge function constants)
|
||||
static const String registrationSeed = String.fromEnvironment('REGISTRATION_SEED', defaultValue: 'default_registration_seed_change_in_production');
|
||||
static const String secretSalt = String.fromEnvironment('SECRET_SALT', defaultValue: 'default_secret_salt_change_in_production');
|
||||
static const int timeWindowSeconds = 300; // 5 minutes
|
||||
|
||||
// Server configuration file path
|
||||
static const String configFile = '/tmp/waylume_server_config.json';
|
||||
|
||||
static String? _operationalSeed;
|
||||
static String? _serverId;
|
||||
|
||||
/// Initialize service and load/generate configuration
|
||||
static Future<void> initialize() async {
|
||||
_serverId = fromEnivronment('SERVER_ID');
|
||||
if (_serverId == null) {
|
||||
throw Exception('SERVER_ID environment variable is required');
|
||||
}
|
||||
|
||||
await _loadConfiguration();
|
||||
}
|
||||
|
||||
/// Generate registration rolling code
|
||||
static String generateRegistrationCode([DateTime? timestamp]) {
|
||||
timestamp ??= DateTime.now();
|
||||
final timeWindow = timestamp.millisecondsSinceEpoch ~/ 1000 ~/ timeWindowSeconds;
|
||||
return _generateHmacSha256(registrationSeed, timeWindow.toString());
|
||||
}
|
||||
|
||||
/// Generate operational rolling code
|
||||
static String generateOperationalCode([DateTime? timestamp]) {
|
||||
if (_operationalSeed == null) {
|
||||
throw Exception('Operational seed not set. Server needs to be registered first.');
|
||||
}
|
||||
|
||||
timestamp ??= DateTime.now();
|
||||
final timeWindow = timestamp.millisecondsSinceEpoch ~/ 1000 ~/ timeWindowSeconds;
|
||||
return _generateHmacSha256(_operationalSeed!, timeWindow.toString() + secretSalt);
|
||||
}
|
||||
|
||||
/// Validate rolling code (checks current and previous window)
|
||||
static bool validateOperationalCode(String receivedCode, [DateTime? timestamp]) {
|
||||
if (_operationalSeed == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
timestamp ??= DateTime.now();
|
||||
final currentWindow = timestamp.millisecondsSinceEpoch ~/ 1000 ~/ timeWindowSeconds;
|
||||
|
||||
// Check current window
|
||||
final currentExpected = _generateHmacSha256(_operationalSeed!, currentWindow.toString() + secretSalt);
|
||||
if (receivedCode == currentExpected) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check previous window (10-minute tolerance total)
|
||||
final previousExpected = _generateHmacSha256(_operationalSeed!, (currentWindow - 1).toString() + secretSalt);
|
||||
return receivedCode == previousExpected;
|
||||
}
|
||||
|
||||
/// Set operational seed after successful registration
|
||||
static Future<void> setOperationalSeed(String seed) async {
|
||||
_operationalSeed = seed;
|
||||
await _saveConfiguration();
|
||||
}
|
||||
|
||||
/// Get current operational seed
|
||||
static String? get operationalSeed => _operationalSeed;
|
||||
|
||||
/// Get server ID
|
||||
static String? get serverId => _serverId;
|
||||
|
||||
/// Check if server is registered (has operational seed)
|
||||
static bool get isRegistered => _operationalSeed != null;
|
||||
|
||||
/// Generate HMAC-SHA256 hash
|
||||
static String _generateHmacSha256(String key, String message) {
|
||||
final keyBytes = utf8.encode(key);
|
||||
final messageBytes = utf8.encode(key + message);
|
||||
|
||||
final hmac = Hmac(sha256, keyBytes);
|
||||
final digest = hmac.convert(messageBytes);
|
||||
|
||||
return digest.toString();
|
||||
}
|
||||
|
||||
/// Load configuration from persistent storage
|
||||
static Future<void> _loadConfiguration() async {
|
||||
try {
|
||||
final file = File(configFile);
|
||||
if (await file.exists()) {
|
||||
final content = await file.readAsString();
|
||||
final config = jsonDecode(content) as Map<String, dynamic>;
|
||||
|
||||
if (config['server_id'] == _serverId) {
|
||||
_operationalSeed = config['operational_seed'];
|
||||
print('Loaded operational seed for server $_serverId');
|
||||
} else {
|
||||
print('Configuration file exists but server_id does not match. Need to re-register.');
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
print('Error loading configuration: $e');
|
||||
}
|
||||
}
|
||||
|
||||
/// Save configuration to persistent storage
|
||||
static Future<void> _saveConfiguration() async {
|
||||
try {
|
||||
final config = {
|
||||
'server_id': _serverId,
|
||||
'operational_seed': _operationalSeed,
|
||||
'last_updated': DateTime.now().toIso8601String(),
|
||||
};
|
||||
|
||||
final file = File(configFile);
|
||||
await file.writeAsString(jsonEncode(config));
|
||||
print('Configuration saved for server $_serverId');
|
||||
} catch (e) {
|
||||
print('Error saving configuration: $e');
|
||||
throw Exception('Failed to save server configuration');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,37 +1,67 @@
|
||||
import 'dart:io';
|
||||
import 'dart:convert';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:waylume_server/config/supabase_config.dart';
|
||||
import 'package:waylume_server/core/utils.dart';
|
||||
import 'package:waylume_server/services/rolling_codes_service.dart';
|
||||
|
||||
class ServerService {
|
||||
static Future<void> registerServer() async {
|
||||
|
||||
await RollingCodesService.initialize();
|
||||
|
||||
// If already registered and has operational seed, skip registration
|
||||
if (RollingCodesService.isRegistered) {
|
||||
print('Server already registered with operational seed');
|
||||
return;
|
||||
}
|
||||
|
||||
GeolocationData geolocationData = await getGeolocationData();
|
||||
|
||||
String ip = "${geolocationData.ip}:${fromEnivronment("EXTERNAL_PORT") ?? "3000"}";
|
||||
|
||||
var existsCheck = await SUPABASE_CLIENT
|
||||
.from("waylume_servers")
|
||||
.select()
|
||||
.eq("id", fromEnivronment('SERVER_ID'))
|
||||
.eq("host_ip", ip);
|
||||
|
||||
if (existsCheck.isEmpty) {
|
||||
await SUPABASE_CLIENT
|
||||
.from("waylume_servers")
|
||||
.insert({
|
||||
"id": fromEnivronment('SERVER_ID')!,
|
||||
"last_heartbeat": DateTime.now().toUtc().toIso8601String(),
|
||||
"host_ip": ip,
|
||||
"geolocation": {
|
||||
"country": geolocationData.countryName,
|
||||
"country_code": geolocationData.countryCode,
|
||||
"city": geolocationData.city,
|
||||
"coords": [
|
||||
geolocationData.latitude,
|
||||
geolocationData.longitude
|
||||
],
|
||||
|
||||
// Generate registration rolling code
|
||||
String registrationAuth = RollingCodesService.generateRegistrationCode();
|
||||
|
||||
// Call server-manager registration endpoint
|
||||
String serverManagerUrl = '${fromEnivronment("SUPABASE_URL")}/functions/v1/server-manager/register';
|
||||
|
||||
Map<String, dynamic> requestBody = {
|
||||
'server_id': fromEnivronment('SERVER_ID')!,
|
||||
'registration_auth': registrationAuth,
|
||||
'geolocation_data': {
|
||||
"country": geolocationData.countryName,
|
||||
"country_code": geolocationData.countryCode,
|
||||
"city": geolocationData.city,
|
||||
"coords": [
|
||||
geolocationData.latitude,
|
||||
geolocationData.longitude
|
||||
],
|
||||
}
|
||||
};
|
||||
|
||||
try {
|
||||
final response = await http.post(
|
||||
Uri.parse(serverManagerUrl),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'apikey': fromEnivronment('SUPABASE_ANON_KEY')!,
|
||||
},
|
||||
body: jsonEncode(requestBody),
|
||||
);
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
final responseData = jsonDecode(response.body);
|
||||
if (responseData['success']) {
|
||||
// Store operational seed
|
||||
await RollingCodesService.setOperationalSeed(responseData['operational_seed']);
|
||||
print('Server registered successfully with server-manager');
|
||||
} else {
|
||||
throw Exception('Registration failed: ${responseData['error']}');
|
||||
}
|
||||
});
|
||||
} else {
|
||||
throw Exception('Registration request failed: ${response.statusCode}');
|
||||
}
|
||||
} catch (e) {
|
||||
print('Error registering server: $e');
|
||||
throw Exception('Failed to register server: $e');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -4,9 +4,10 @@
|
||||
import 'dart:io';
|
||||
import 'dart:isolate';
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:supabase/supabase.dart';
|
||||
import 'dart:convert';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:waylume_server/config/supabase_config.dart';
|
||||
import 'package:waylume_server/services/rolling_codes_service.dart';
|
||||
|
||||
void initHeartbeat() {
|
||||
|
||||
@@ -18,18 +19,122 @@ void initHeartbeat() {
|
||||
|
||||
while (true) {
|
||||
DateTime now = DateTime.now().toUtc();
|
||||
|
||||
try {
|
||||
// Wait until rolling codes service is initialized and registered
|
||||
if (!RollingCodesService.isRegistered) {
|
||||
print("Server not registered yet, skipping heartbeat");
|
||||
await Future.delayed(Duration(seconds: 30));
|
||||
continue;
|
||||
}
|
||||
|
||||
// Generate operational rolling code for heartbeat authentication
|
||||
String authCode = RollingCodesService.generateOperationalCode();
|
||||
|
||||
// Get server status (CPU, memory, active connections)
|
||||
Map<String, dynamic> serverStatus = await _getServerStatus();
|
||||
|
||||
// Send heartbeat to server-manager endpoint
|
||||
String serverManagerUrl = '${fromEnivronment("SUPABASE_URL")}/functions/v1/server-manager/heartbeat';
|
||||
|
||||
Map<String, dynamic> heartbeatData = {
|
||||
'server_id': fromEnivronment("SERVER_ID")!,
|
||||
'timestamp': now.toIso8601String(),
|
||||
'auth_code': authCode,
|
||||
'status': serverStatus
|
||||
};
|
||||
|
||||
final response = await http.post(
|
||||
Uri.parse(serverManagerUrl),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'apikey': fromEnivronment('SUPABASE_ANON_KEY')!,
|
||||
},
|
||||
body: jsonEncode(heartbeatData),
|
||||
);
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
final responseData = jsonDecode(response.body);
|
||||
if (responseData['success']) {
|
||||
print("Heartbeat sent successfully at ${now.toIso8601String()}");
|
||||
} else {
|
||||
print("Heartbeat failed: ${responseData['error']}");
|
||||
}
|
||||
} else {
|
||||
print("Heartbeat request failed: ${response.statusCode}");
|
||||
}
|
||||
|
||||
} catch (e) {
|
||||
print("Error sending heartbeat: $e");
|
||||
}
|
||||
|
||||
await SUPABASE_CLIENT
|
||||
.from("waylume_servers")
|
||||
.update({ "last_heartbeat": now.toIso8601String() })
|
||||
.eq("id", fromEnivronment("SERVER_ID")!);
|
||||
|
||||
print("Heartbeat sent to Supabase at ${now.toIso8601String()}");
|
||||
|
||||
// Wait 30 seconds before sending the next heartbeat
|
||||
await Future.delayed(Duration(seconds: 30));
|
||||
// Wait 120 seconds (2 minutes) before sending the next heartbeat
|
||||
await Future.delayed(Duration(seconds: 120));
|
||||
}
|
||||
|
||||
}, null);
|
||||
|
||||
}
|
||||
|
||||
Future<Map<String, dynamic>> _getServerStatus() async {
|
||||
// Get basic server status information
|
||||
// This is a simplified implementation - could be enhanced with actual metrics
|
||||
|
||||
int activeConnections = 0;
|
||||
double cpuUsage = 0.0;
|
||||
double memoryUsage = 0.0;
|
||||
|
||||
try {
|
||||
// Get active WireGuard connections count
|
||||
ProcessResult wgShowResult = await Process.run('wg', ['show', 'wg0']);
|
||||
if (wgShowResult.exitCode == 0) {
|
||||
// Count peer entries (simplified - each peer has multiple lines)
|
||||
String output = wgShowResult.stdout as String;
|
||||
activeConnections = 'peer:'.allMatches(output).length;
|
||||
}
|
||||
} catch (e) {
|
||||
print("Error getting WireGuard status: $e");
|
||||
}
|
||||
|
||||
try {
|
||||
// Get basic system info (simplified)
|
||||
ProcessResult uptimeResult = await Process.run('uptime', []);
|
||||
if (uptimeResult.exitCode == 0) {
|
||||
String output = uptimeResult.stdout as String;
|
||||
// Parse load average as a rough CPU usage indicator
|
||||
RegExp loadRegex = RegExp(r'load average: (\d+\.?\d*),');
|
||||
Match? match = loadRegex.firstMatch(output);
|
||||
if (match != null) {
|
||||
cpuUsage = double.tryParse(match.group(1) ?? '0') ?? 0.0;
|
||||
cpuUsage = (cpuUsage * 100).clamp(0, 100); // Convert to percentage
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
print("Error getting CPU usage: $e");
|
||||
}
|
||||
|
||||
try {
|
||||
// Get memory usage (simplified)
|
||||
ProcessResult freeResult = await Process.run('free', ['-m']);
|
||||
if (freeResult.exitCode == 0) {
|
||||
String output = freeResult.stdout as String;
|
||||
List<String> lines = output.split('\n');
|
||||
if (lines.length > 1) {
|
||||
List<String> memLine = lines[1].split(RegExp(r'\s+'));
|
||||
if (memLine.length > 2) {
|
||||
int total = int.tryParse(memLine[1]) ?? 1;
|
||||
int used = int.tryParse(memLine[2]) ?? 0;
|
||||
memoryUsage = (used / total * 100).clamp(0, 100);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
print("Error getting memory usage: $e");
|
||||
}
|
||||
|
||||
return {
|
||||
'active_connections': activeConnections,
|
||||
'cpu_usage': cpuUsage,
|
||||
'memory_usage': memoryUsage,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user