// SPDX-License-Identifier: Apache-2.0 #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include namespace { constexpr auto* TAG = "DevelopmentServer"; constexpr auto* SETTINGS_FILE = "server.properties"; constexpr auto* KEY_ENABLE_ON_BOOT = "enableOnBoot"; constexpr int LISTEN_BACKLOG = 2; constexpr uint32_t ACCEPT_POLL_TIMEOUT_MS = 200; constexpr uint32_t SOCKET_POLL_TIMEOUT_MS = 200; constexpr uint32_t HANDSHAKE_TIMEOUT_MS = 10000; constexpr size_t LISTENER_STACK_SIZE = 8192; constexpr MemoryPolicy MEMORY_POLICY = { .required = 0, .desired = MEMORY_CAPABILITY_EXTERNAL, .alignment = 0 }; struct SessionSlot { bool active; bool shell; /** Set until the session app takes the session */ DevelopmentSession* pending; AppInstanceId appId; }; struct ServerData { char directory[FILE_MAX_PATH_STRING_LENGTH] = {}; DevelopmentCredentials credentials {}; bool credentialsLoaded = false; mbedtls_ssl_config config {}; Thread* listener = nullptr; int listenFd = -1; uint16_t boundPort = 0; std::atomic stopRequested = false; // Guarded by stateMutex char fingerprint[DEVELOPMENT_SERVER_FINGERPRINT_LENGTH + 1] = {}; bool listening = false; SessionSlot slots[DEVELOPMENT_SERVER_MAX_SESSIONS] = {}; }; struct GlobalMutex { Mutex handle {}; GlobalMutex() { mutex_construct(&handle); } ~GlobalMutex() { mutex_destruct(&handle); } }; // Serializes starting and stopping. Held while waiting for the listener and session app to stop, // so those must never take it. GlobalMutex controlMutex; // Guards the server pointer and the fields of ServerData that the listener and session app share. GlobalMutex stateMutex; ServerData* server = nullptr; uint16_t configuredPort = DEVELOPMENT_SERVER_DEFAULT_PORT; void lockState() { mutex_lock(&stateMutex.handle); } void unlockState() { mutex_unlock(&stateMutex.handle); } // region Settings bool getSettingsPath(const ServerData* data, char* outPath, size_t outSize) { const int written = snprintf(outPath, outSize, "%s/%s", data->directory, SETTINGS_FILE); return written > 0 && static_cast(written) < outSize; } bool loadEnableOnBoot(const ServerData* data) { char path[FILE_MAX_PATH_STRING_LENGTH]; if (!getSettingsPath(data, path, sizeof(path))) { return false; } if (!path_exists(path)) { return false; } PropertiesFile* file = properties_file_open(path); if (file == nullptr) { return false; } char value[8] = {}; const bool enabled = properties_file_get(file, KEY_ENABLE_ON_BOOT, value, sizeof(value)) == ERROR_NONE && strcmp(value, "true") == 0; properties_file_close(file); return enabled; } void saveEnableOnBoot(const ServerData* data, bool enabled) { char path[FILE_MAX_PATH_STRING_LENGTH]; const error_t directoryError = directory_make(data->directory, true); if ((directoryError != ERROR_NONE && directoryError != ERROR_ALREADY_EXISTS) || !getSettingsPath(data, path, sizeof(path))) { LOG_E(TAG, "Failed to save settings"); return; } PropertiesFile* file = properties_file_open(path); if (file == nullptr) { LOG_E(TAG, "Failed to open %s", path); return; } properties_file_set(file, KEY_ENABLE_ON_BOOT, enabled ? "true" : "false"); if (properties_file_close(file) != ERROR_NONE) { LOG_E(TAG, "Failed to save %s", path); } } // endregion // region Connections int verifyClientCertificate(void* context, mbedtls_x509_crt* certificate, int depth, uint32_t* flags) { // Clients use self-signed certificates: the user decides whether to trust them if (depth == 0) { auto* session = static_cast(context); if (development_fingerprint(certificate->raw.p, certificate->raw.len, session->fingerprint) != ERROR_NONE) { session->fingerprint[0] = '\0'; } } *flags = 0; return 0; } bool isExpired(TickType_t start, uint32_t timeoutMs) { return xTaskGetTickCount() - start > pdMS_TO_TICKS(timeoutMs); } bool handshake(ServerData* data, DevelopmentSession* session) { const TickType_t start = xTaskGetTickCount(); int result; while ((result = mbedtls_ssl_handshake(&session->ssl)) != 0) { if (result != MBEDTLS_ERR_SSL_WANT_READ && result != MBEDTLS_ERR_SSL_WANT_WRITE) { LOG_W(TAG, "Handshake with %s failed: -0x%04x", session->peer_address, static_cast(-result)); return false; } if (data->stopRequested || isExpired(start, HANDSHAKE_TIMEOUT_MS)) { LOG_W(TAG, "Handshake with %s timed out", session->peer_address); return false; } } return true; } // Reads byte by byte, so nothing after the newline is consumed bool readLine(ServerData* data, DevelopmentSession* session, char* outLine, size_t outSize, size_t* outLength) { const TickType_t start = xTaskGetTickCount(); size_t length = 0; while (true) { unsigned char character; const int result = mbedtls_ssl_read(&session->ssl, &character, 1); if (result == 1) { if (character == '\n') { outLine[length] = '\0'; *outLength = length; return true; } if (length + 1 >= outSize) { return false; } outLine[length++] = static_cast(character); } else if (result != MBEDTLS_ERR_SSL_WANT_READ && result != MBEDTLS_ERR_SSL_WANT_WRITE) { return false; } else if (data->stopRequested || isExpired(start, HANDSHAKE_TIMEOUT_MS)) { return false; } } } bool readHello(ServerData* data, DevelopmentSession* session) { auto* line = static_cast(memory_alloc_with_policy(DEVELOPMENT_PROTOCOL_LINE_MAX, &MEMORY_POLICY)); if (line == nullptr) { return false; } size_t length = 0; bool valid = false; if (readLine(data, session, line, DEVELOPMENT_PROTOCOL_LINE_MAX, &length)) { const char* message = nullptr; if (development_protocol_parse_hello(line, length, &session->hello, &message) == DevelopmentHelloError::None) { valid = true; } else { LOG_W(TAG, "Invalid hello from %s: %s", session->peer_address, message); development_session_send_status(session, "error", message); } } memory_free(line); return valid; } /** @return the reason why the session can't start now, or nullptr after reserving a slot for it */ const char* reserveSlot(ServerData* data, DevelopmentSession* session) { const bool shell = strcmp(session->hello.mode, "shell") == 0; lockState(); int freeSlot = -1; bool shellBusy = false; for (int i = 0; i < DEVELOPMENT_SERVER_MAX_SESSIONS; i++) { const SessionSlot& slot = data->slots[i]; if (!slot.active && freeSlot < 0) { freeSlot = i; } shellBusy = shellBusy || (shell && slot.active && slot.shell); } const char* error = shellBusy ? "shell busy" : (freeSlot < 0 ? "busy" : nullptr); if (error == nullptr) { data->slots[freeSlot] = { .active = true, .shell = shell, .pending = session, .appId = 0 }; session->slot = freeSlot; } unlockState(); return error; } void releaseSlot(ServerData* data, int slot) { lockState(); data->slots[slot] = {}; unlockState(); } bool startSessionApp(ServerData* data, int slot) { char slotArgument[4]; snprintf(slotArgument, sizeof(slotArgument), "%d", slot); const char* arguments[] = { slotArgument }; AppStartContext context = app_start_context_for_manifest(&development_session_app_manifest); app_start_context_set_arguments_ext(&context, 1, arguments); AppInstanceId appId = 0; if (app_start_with_context(&context, &appId) != ERROR_NONE) { return false; } lockState(); // Only this thread reserves slots, so an inactive slot means that the app already finished its session if (data->slots[slot].active) { data->slots[slot].appId = appId; } unlockState(); return true; } void handleConnection(ServerData* data, int socket, const sockaddr_in& address) { const timeval timeout { .tv_sec = 0, .tv_usec = SOCKET_POLL_TIMEOUT_MS * 1000 }; setsockopt(socket, SOL_SOCKET, SO_RCVTIMEO, &timeout, sizeof(timeout)); DevelopmentSession* session = development_session_alloc(socket, &data->config); if (session == nullptr) { LOG_E(TAG, "Out of memory"); close(socket); return; } inet_ntop(AF_INET, &address.sin_addr, session->peer_address, sizeof(session->peer_address)); mbedtls_ssl_set_verify(&session->ssl, verifyClientCertificate, session); if (!handshake(data, session) || !readHello(data, session)) { development_session_free(session); return; } if (session->fingerprint[0] == '\0') { LOG_W(TAG, "Client %s has no certificate", session->peer_address); development_session_send_status(session, "error", "client certificate required"); development_session_free(session); return; } session->trusted = development_trust_contains(data->directory, session->fingerprint); const char* busy = reserveSlot(data, session); if (busy != nullptr) { development_session_send_status(session, "error", busy); development_session_free(session); return; } const int flags = fcntl(socket, F_GETFL, 0); fcntl(socket, F_SETFL, flags | O_NONBLOCK); LOG_I(TAG, "Connection from %s (%s)", session->peer_address, session->hello.client); if (!startSessionApp(data, session->slot)) { LOG_E(TAG, "Failed to start session"); releaseSlot(data, session->slot); development_session_send_status(session, "error", "internal error"); development_session_free(session); } } int32_t listenerMain(void* context) { auto* data = static_cast(context); while (!data->stopRequested) { fd_set readFds; FD_ZERO(&readFds); FD_SET(data->listenFd, &readFds); timeval timeout { .tv_sec = 0, .tv_usec = ACCEPT_POLL_TIMEOUT_MS * 1000 }; if (select(data->listenFd + 1, &readFds, nullptr, nullptr, &timeout) <= 0) { continue; } sockaddr_in address {}; socklen_t addressLength = sizeof(address); const int socket = accept(data->listenFd, reinterpret_cast(&address), &addressLength); if (socket >= 0) { handleConnection(data, socket, address); } } return 0; } // endregion // region Lifecycle error_t loadCredentials(ServerData* data) { if (data->credentialsLoaded) { return ERROR_NONE; } const error_t error = development_credentials_load(data->directory, &data->credentials); if (error != ERROR_NONE) { development_credentials_free(&data->credentials); return error; } mbedtls_ssl_config_init(&data->config); int result = mbedtls_ssl_config_defaults(&data->config, MBEDTLS_SSL_IS_SERVER, MBEDTLS_SSL_TRANSPORT_STREAM, MBEDTLS_SSL_PRESET_DEFAULT); if (result == 0) { mbedtls_ssl_conf_authmode(&data->config, MBEDTLS_SSL_VERIFY_OPTIONAL); mbedtls_ssl_conf_min_tls_version(&data->config, MBEDTLS_SSL_VERSION_TLS1_2); mbedtls_ssl_conf_max_tls_version(&data->config, MBEDTLS_SSL_VERSION_TLS1_2); result = mbedtls_ssl_conf_own_cert(&data->config, &data->credentials.certificate, &data->credentials.key); } if (result != 0) { LOG_E(TAG, "Failed to configure TLS: -0x%04x", static_cast(-result)); mbedtls_ssl_config_free(&data->config); development_credentials_free(&data->credentials); return ERROR_RESOURCE; } data->credentialsLoaded = true; lockState(); memcpy(data->fingerprint, data->credentials.fingerprint, sizeof(data->fingerprint)); unlockState(); LOG_I(TAG, "Device fingerprint %s", data->credentials.fingerprint); return ERROR_NONE; } error_t startListening(ServerData* data) { if (data->listener != nullptr) { return ERROR_NONE; } error_t error = loadCredentials(data); if (error != ERROR_NONE) { return error; } const int fd = socket(AF_INET, SOCK_STREAM, 0); if (fd < 0) { return ERROR_RESOURCE; } int reuse = 1; setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &reuse, sizeof(reuse)); sockaddr_in address {}; address.sin_family = AF_INET; address.sin_port = htons(configuredPort); address.sin_addr.s_addr = htonl(INADDR_ANY); if (bind(fd, reinterpret_cast(&address), sizeof(address)) != 0 || listen(fd, LISTEN_BACKLOG) != 0) { LOG_E(TAG, "Failed to listen on port %u", static_cast(configuredPort)); close(fd); return ERROR_RESOURCE; } socklen_t addressLength = sizeof(address); getsockname(fd, reinterpret_cast(&address), &addressLength); data->listenFd = fd; data->boundPort = ntohs(address.sin_port); data->stopRequested = false; data->listener = thread_alloc_full("development-server", LISTENER_STACK_SIZE, listenerMain, data, -1); if (data->listener != nullptr) { // Socket calls block outside the scheduler on POSIX, so a higher priority would starve other tasks there thread_set_priority(data->listener, THREAD_PRIORITY_IDLE); } if (data->listener == nullptr || thread_start(data->listener) != ERROR_NONE) { if (data->listener != nullptr) { thread_free(data->listener); data->listener = nullptr; } close(fd); data->listenFd = -1; return ERROR_RESOURCE; } lockState(); data->listening = true; unlockState(); LOG_I(TAG, "Listening on port %u", static_cast(data->boundPort)); return ERROR_NONE; } void stopListening(ServerData* data) { if (data->listener != nullptr) { data->stopRequested = true; thread_join(data->listener, portMAX_DELAY, pdMS_TO_TICKS(10)); thread_free(data->listener); data->listener = nullptr; close(data->listenFd); data->listenFd = -1; } AppInstanceId appIds[DEVELOPMENT_SERVER_MAX_SESSIONS]; lockState(); data->listening = false; for (int i = 0; i < DEVELOPMENT_SERVER_MAX_SESSIONS; i++) { appIds[i] = data->slots[i].appId; } unlockState(); for (const AppInstanceId appId : appIds) { if (appId != 0 && app_manager_get_state(appId) != APP_INSTANCE_STATE_STOPPED) { app_manager_stop(appId); } } // Sessions that no app took yet for (int i = 0; i < DEVELOPMENT_SERVER_MAX_SESSIONS; i++) { lockState(); DevelopmentSession* pending = data->slots[i].pending; if (pending != nullptr) { data->slots[i] = {}; } unlockState(); if (pending != nullptr) { development_session_free(pending); } } } void* createService(const ServiceManifest* /*manifest*/) { return tt::newObject(); } void destroyService(const ServiceManifest* /*manifest*/, void* data) { tt::deleteObject(static_cast(data)); } error_t onStart(ServiceInstance* /*instance*/, void* context) { auto* data = static_cast(context); if (data == nullptr) { return ERROR_OUT_OF_MEMORY; } const error_t error = service_paths_get_user_data_directory(DEVELOPMENT_SERVER_SERVICE_ID, data->directory, sizeof(data->directory)); if (error != ERROR_NONE) { return error; } mutex_lock(&controlMutex.handle); lockState(); server = data; unlockState(); if (loadEnableOnBoot(data) && startListening(data) != ERROR_NONE) { LOG_E(TAG, "Failed to start"); } mutex_unlock(&controlMutex.handle); return ERROR_NONE; } void onStop(ServiceInstance* /*instance*/, void* context) { auto* data = static_cast(context); mutex_lock(&controlMutex.handle); stopListening(data); lockState(); server = nullptr; unlockState(); lockState(); data->fingerprint[0] = '\0'; unlockState(); if (data->credentialsLoaded) { mbedtls_ssl_config_free(&data->config); development_credentials_free(&data->credentials); data->credentialsLoaded = false; } mutex_unlock(&controlMutex.handle); } // endregion } // namespace extern "C" const ServiceManifest development_server_service_manifest = { .id = DEVELOPMENT_SERVER_SERVICE_ID, .create_service = createService, .destroy_service = destroyService, .on_start = onStart, .on_stop = onStop, }; extern "C" { error_t development_server_set_enabled(bool enabled) { mutex_lock(&controlMutex.handle); lockState(); ServerData* data = server; unlockState(); if (data == nullptr) { mutex_unlock(&controlMutex.handle); return ERROR_INVALID_STATE; } error_t error = ERROR_NONE; if (enabled) { error = startListening(data); } else { stopListening(data); } saveEnableOnBoot(data, enabled); mutex_unlock(&controlMutex.handle); return error; } bool development_server_is_enabled() { lockState(); const bool listening = server != nullptr && server->listening; unlockState(); return listening; } error_t development_server_get_fingerprint(char* out_fingerprint, size_t out_size) { if (out_size < DEVELOPMENT_SERVER_FINGERPRINT_LENGTH + 1) { return ERROR_BUFFER_OVERFLOW; } // Avoids waiting for the control mutex once the credentials are loaded lockState(); const bool loaded = server != nullptr && server->fingerprint[0] != '\0'; if (loaded) { memcpy(out_fingerprint, server->fingerprint, sizeof(server->fingerprint)); } unlockState(); if (loaded) { return ERROR_NONE; } mutex_lock(&controlMutex.handle); lockState(); ServerData* data = server; unlockState(); error_t error = data != nullptr ? loadCredentials(data) : ERROR_INVALID_STATE; if (error == ERROR_NONE) { memcpy(out_fingerprint, data->credentials.fingerprint, DEVELOPMENT_SERVER_FINGERPRINT_LENGTH + 1); } mutex_unlock(&controlMutex.handle); return error; } unsigned short development_server_get_port() { lockState(); const uint16_t port = (server != nullptr && server->listening) ? server->boundPort : configuredPort; unlockState(); return port; } } // extern "C" DevelopmentSession* development_server_take_session(int slot) { if (slot < 0 || slot >= DEVELOPMENT_SERVER_MAX_SESSIONS) { return nullptr; } lockState(); DevelopmentSession* session = nullptr; if (server != nullptr) { session = server->slots[slot].pending; server->slots[slot].pending = nullptr; } unlockState(); return session; } void development_server_finish_session(DevelopmentSession* session) { const int slot = session->slot; development_session_free(session); lockState(); if (server != nullptr) { server->slots[slot] = {}; } unlockState(); } namespace { bool getDirectory(char* outDirectory) { lockState(); const bool running = server != nullptr; if (running) { strcpy(outDirectory, server->directory); } unlockState(); return running; } } // namespace error_t development_server_trust(const char* fingerprint, const char* name) { char directory[FILE_MAX_PATH_STRING_LENGTH]; return getDirectory(directory) ? development_trust_add(directory, fingerprint, name) : ERROR_INVALID_STATE; } extern "C" error_t development_server_for_each_trusted_client(DevelopmentTrustedClientVisitor visitor, void* context) { char directory[FILE_MAX_PATH_STRING_LENGTH]; return getDirectory(directory) ? development_trust_for_each(directory, visitor, context) : ERROR_INVALID_STATE; } extern "C" error_t development_server_remove_trusted_client(const char* fingerprint) { char directory[FILE_MAX_PATH_STRING_LENGTH]; return getDirectory(directory) ? development_trust_remove(directory, fingerprint) : ERROR_INVALID_STATE; } void development_server_set_port(uint16_t port) { lockState(); configuredPort = port; unlockState(); }