import 'dart:async'; import 'package:localsend_app/gen/strings.g.dart'; import 'package:localsend_app/model/cross_file.dart'; import 'package:localsend_app/model/state/send/web/web_send_state.dart'; import 'package:localsend_app/model/state/server/server_state.dart'; import 'package:localsend_app/provider/network/server/controller/receive_controller.dart'; import 'package:localsend_app/provider/network/server/controller/send_controller.dart'; import 'package:localsend_app/provider/network/server/server_utils.dart'; import 'package:localsend_app/provider/settings_provider.dart'; import 'package:localsend_app/util/alias_generator.dart'; import 'package:localsend_isolates/constants.dart'; import 'package:localsend_isolates/isolate.dart'; import 'package:localsend_isolates/model/dto/multicast_dto.dart'; import 'package:localsend_isolates/rust/api/server.dart' show WebI18n, WebParams, WebSendParams; import 'package:localsend_isolates/util/rust.dart'; import 'package:logging/logging.dart'; import 'package:refena_flutter/refena_flutter.dart'; final _logger = Logger('Server'); /// This provider runs the server and provides the current server state. /// It is a singleton provider, so only one server can be running at a time. /// The server state is null if the server is not running. /// The server can receive files (since v1) and send files (since v2). /// /// The HTTP server itself runs in Rust (inside the server isolate); this /// provider starts it, listens to its events and holds the resulting state. final serverProvider = NotifierProvider( (ref) { return ServerService(); }, onChanged: (_, next, ref) { final settings = ref.read(settingsProvider); final syncState = ref.read(parentIsolateProvider).syncState; final syncStatePrev = (syncState.alias, syncState.port, syncState.protocol, syncState.serverRunning, syncState.download); final syncStateNext = ( next?.alias ?? settings.alias, next?.port ?? settings.port, (next?.https ?? settings.https) ? ProtocolType.https : ProtocolType.http, next != null, next?.webSendState != null, ); if (syncStatePrev == syncStateNext) { return; } ref .redux(parentIsolateProvider) .dispatch( IsolateSyncServerStateAction( alias: syncStateNext.$1, port: syncStateNext.$2, protocol: syncStateNext.$3, serverRunning: syncStateNext.$4, download: syncStateNext.$5, ), ); }, ); class ServerService extends Notifier { late final _serverUtils = ServerUtils( refFunc: () => ref, getState: () => state!, getStateOrNull: () => state, setState: (builder) => state = builder(state), ); late final _receiveController = ReceiveController(_serverUtils); late final _sendController = SendController(_serverUtils); StreamSubscription? _subscription; ServerService(); @override ServerState? init() { return null; } /// Starts the server from user settings. Future startServerFromSettings() async { final settings = ref.read(settingsProvider); return startServer( alias: settings.alias, port: settings.port, https: settings.https, ); } /// Starts the server. /// Passing a [webSendState] additionally serves the web send (download) API. /// Passing [webUpload] serves the upload page so web browsers can upload files. Future startServer({ required String alias, required int port, required bool https, WebSendState? webSendState, bool webUpload = false, }) async { if (state != null) { _logger.info('Server already running.'); return null; } alias = alias.trim(); if (alias.isEmpty) { alias = generateRandomAlias(); } if (port < 0 || port > 65535) { port = defaultPort; } _logger.info('Starting server...'); // The server isolate derives its configuration from the sync state, // so it must be published before the start task. _syncServerState(alias: alias, port: port, https: https, serverRunning: true, download: webSendState != null); final settings = ref.read(settingsProvider); final events = ref .redux(parentIsolateProvider) .dispatchTakeResult( IsolateHttpServerStartAction( pin: settings.receivePin, verifyChecksums: settings.verifyChecksums, web: webSendState != null || webUpload ? WebParams( send: webSendState != null ? WebSendParams( files: { for (final entry in webSendState.files.entries) entry.key: entry.value.file.toRust(), }, pin: webSendState.pin, ) : null, upload: webUpload, i18N: WebI18n( waiting: t.web.waiting, enterPin: t.web.enterPin, invalidPin: t.web.invalidPin, tooManyAttempts: t.web.tooManyAttempts, rejected: t.web.rejected, uploadRejected: t.sendPage.rejected, busy: t.sendPage.busy, files: t.web.files, fileName: t.web.fileName, size: t.web.size, ), ) : null, showToken: settings.showToken, ), ); final started = Completer(); final subscription = events.listen( (event) { if (event is HttpServerStartedEvent) { if (!started.isCompleted) { started.complete(); } return; } _handleEvent(event); }, onError: (Object error) { if (!started.isCompleted) { started.completeError(error); } else { _logger.severe('HTTP server error: $error'); } }, ); try { await started.future; } catch (e) { await subscription.cancel(); _syncServerState(alias: alias, port: port, https: https, serverRunning: false, download: false); _logger.warning('Failed to start server', e); rethrow; } _subscription = subscription; final newServerState = ServerState( alias: alias, port: port, https: https, session: null, webSendState: webSendState, webUpload: webUpload, ); state = newServerState; _logger.info('Server started. (Port: $port, ${https ? 'HTTPS' : 'HTTP'} only)'); return newServerState; } Future stopServer() async { _logger.info('Stopping server...'); await _subscription?.cancel(); _subscription = null; await ref.redux(parentIsolateProvider).dispatchAsync(IsolateHttpServerStopAction()); state = null; _logger.info('Server stopped.'); } Future restartServerFromSettings() async { await stopServer(); return await startServerFromSettings(); } Future restartServer({ required String alias, required int port, required bool https, WebSendState? webSendState, bool webUpload = false, }) async { await stopServer(); return await startServer(alias: alias, port: port, https: https, webSendState: webSendState, webUpload: webUpload); } Future acceptFileRequest(Map fileNameMap) async { await _receiveController.acceptFileRequest(fileNameMap); } void declineFileRequest() { _receiveController.declineFileRequest(); } /// Updates the destination directory for the current session. void setSessionDestinationDir(String destinationDirectory) { _receiveController.setSessionDestinationDir(destinationDirectory); } /// Updates the save to gallery setting for the current session. void setSessionSaveToGallery(bool saveToGallery) { _receiveController.setSessionSaveToGallery(saveToGallery); } /// In addition to [closeSession], this method also cancels incoming requests. void cancelSession() { _receiveController.cancelSession(); } /// Clears the session. void closeSession() { _receiveController.closeSession(); } /// Restarts the server with web send (the download API) enabled for [files]. /// The auto accept setting and the pin of a previous web send state are kept. Future restartServerWithWebSend({ required String alias, required int port, required bool https, required List files, }) async { final webSendState = await _sendController.buildWebSendState(files: files); await restartServer(alias: alias, port: port, https: https, webSendState: webSendState); } /// Updates the web send pin. /// The pin is enforced by the Rust server, so the server is restarted. Future setWebSendPin(String? pin) async { final current = state; final webSendState = current?.webSendState; if (current == null || webSendState == null || webSendState.pin == pin) { return; } await restartServer( alias: current.alias, port: current.port, https: current.https, webSendState: webSendState.copyWith(sessions: {}, pin: pin), ); } /// Updates the auto accept setting for web send. void setWebSendAutoAccept(bool autoAccept) { state = state?.copyWith( webSendState: state?.webSendState?.copyWith( autoAccept: autoAccept, ), ); } /// Accepts the web send request. void acceptWebSendRequest(String sessionId) { _sendController.acceptRequest(sessionId); } /// Declines the web send request. void declineWebSendRequest(String sessionId) { _sendController.declineRequest(sessionId); } void _handleEvent(HttpServerEvent event) { switch (event) { case HttpServerStartedEvent(): break; case HttpServerRegisterEvent(): // ignore: discarded_futures _receiveController.onRegister(event); case HttpServerPrepareUploadEvent(): // ignore: discarded_futures _receiveController.onPrepareUpload(event); case HttpServerFileUploadEvent(): _receiveController.onFileUpload(event); case HttpServerFileUploadProgressEvent(): _receiveController.onFileUploadProgress(event); case HttpServerFileUploadResultEvent(): // ignore: discarded_futures _receiveController.onFileUploadResult(event); case HttpServerSessionEndEvent(): _receiveController.onSessionEnd(event); case HttpServerPrepareUploadAbortedEvent(): _receiveController.onPrepareUploadAborted(event); case HttpServerCancelReceivedEvent(): _receiveController.onCancelReceived(event); case HttpServerShowEvent(): _receiveController.onShow(event); case HttpServerWebPrepareDownloadEvent(): _sendController.onPrepareDownload(event); case HttpServerWebFileDownloadEvent(): // ignore: discarded_futures _sendController.onFileDownload(event); } } void _syncServerState({ required String alias, required int port, required bool https, required bool serverRunning, required bool download, }) { ref .redux(parentIsolateProvider) .dispatch( IsolateSyncServerStateAction( alias: alias, port: port, protocol: https ? ProtocolType.https : ProtocolType.http, serverRunning: serverRunning, download: download, ), ); } }