// // WQSSHServer.m // FilzaSlop // // In-process SSH server built on libssh (weakly linked; the public build // does not ship libssh). Authentication is intentionally simple: // mobile/alpine and root/alpine. Each session runs the same in-process shell // engine used by Filza's terminal, starting in the worse_query hub. // #import "WQSSHServer.h" #import "WQTerminal.h" #import "WQFilzaIntegration.h" #import #import #import #import #import #import #import #import #import #import #import #import #include #include static ssh_bind gSSHBind; static ssh_key gSSHHostKey; static pthread_t gSSHListenerThread; static BOOL gSSHRunning; static NSInteger gSSHPort = 2222; static NSString *gSSHLastError; static NSObject *gSSHLock; static void WQSSHLog(NSString *message) { NSLog(@"[WQSSH] %@", message); } static BOOL WQSSHLibraryAvailable(void) { return dlsym(RTLD_DEFAULT, "ssh_bind_new") != NULL && dlsym(RTLD_DEFAULT, "ssh_handle_key_exchange") != NULL; } #pragma mark - Session shell static const char *WQSSHAuthorizedPassword(const char *user) { if (!user) return NULL; if (strcmp(user, "mobile") == 0 || strcmp(user, "root") == 0) return "alpine"; return NULL; } static void *WQSSHClientThread(void *argument) { ssh_session session = (ssh_session)argument; @autoreleasepool { if (!session) return NULL; if (ssh_handle_key_exchange(session) != SSH_OK) { WQSSHLog([NSString stringWithFormat:@"key exchange failed: %s", ssh_get_error(session)]); ssh_disconnect(session); ssh_free(session); return NULL; } ssh_channel channel = NULL; char authenticatedUser[64] = {0}; BOOL authenticated = NO; while (gSSHRunning) { ssh_message message = ssh_message_get(session); if (!message) break; int type = ssh_message_type(message); int subtype = ssh_message_subtype(message); if (type == SSH_REQUEST_AUTH && !authenticated) { if (subtype == SSH_AUTH_METHOD_PASSWORD) { const char *user = ssh_message_auth_user(message); const char *password = ssh_message_auth_password(message); const char *expected = WQSSHAuthorizedPassword(user); if (expected && password && strcmp(password, expected) == 0) { ssh_message_auth_reply_success(message, 0); authenticated = YES; strncpy(authenticatedUser, user, sizeof(authenticatedUser) - 1); WQSSHLog([NSString stringWithFormat:@"login user=%s", user]); ssh_message_free(message); continue; } } ssh_message_auth_set_methods(message, SSH_AUTH_METHOD_PASSWORD); ssh_message_reply_default(message); } else if (authenticated && type == SSH_REQUEST_CHANNEL_OPEN && subtype == SSH_CHANNEL_SESSION) { channel = ssh_message_channel_request_open_reply_accept(message); } else if (authenticated && type == SSH_REQUEST_CHANNEL && channel) { if (subtype == SSH_CHANNEL_REQUEST_PTY) { ssh_message_channel_request_reply_success(message); } else if (subtype == SSH_CHANNEL_REQUEST_SHELL) { ssh_message_channel_request_reply_success(message); ssh_message_free(message); break; } else { ssh_message_reply_default(message); } } else { ssh_message_reply_default(message); } ssh_message_free(message); } if (!authenticated || !channel) { ssh_disconnect(session); ssh_free(session); return NULL; } setenv("USER", authenticatedUser, 1); setenv("LOGNAME", authenticatedUser, 1); setenv("HOME", NSHomeDirectory().UTF8String, 1); setenv("SHELL", "fish", 1); int pid = -1; int master = WQTerminalSpawnShell(&pid, WQVirtualRootFolder()); if (master < 0) { const char *failure = "Unable to start the in-process shell.\r\n"; ssh_channel_write(channel, failure, (uint32_t)strlen(failure)); ssh_channel_send_eof(channel); ssh_channel_close(channel); ssh_channel_free(channel); ssh_disconnect(session); ssh_free(session); return NULL; } fcntl(master, F_SETFL, O_NONBLOCK); char buffer[4096]; while (gSSHRunning && ssh_channel_is_open(channel)) { int incoming = ssh_channel_read_nonblocking(channel, buffer, sizeof(buffer), 0); if (incoming > 0) { ssize_t written = 0; while (written < incoming) { ssize_t step = write(master, buffer + written, (size_t)(incoming - written)); if (step <= 0) break; written += step; } } else if (incoming == SSH_ERROR) { // Client may have half-closed stdin after piping a command. // Keep pumping shell output until the shell exits. } ssize_t outgoing = read(master, buffer, sizeof(buffer)); if (outgoing > 0) { int offset = 0; while (offset < outgoing) { int sent = ssh_channel_write(channel, buffer + offset, (uint32_t)(outgoing - offset)); if (sent <= 0) break; offset += sent; } } else if (outgoing < 0 && errno != EAGAIN && errno != EWOULDBLOCK) { break; } usleep(5000); } close(master); ssh_channel_send_eof(channel); ssh_channel_close(channel); ssh_channel_free(channel); ssh_disconnect(session); ssh_free(session); } return NULL; } #pragma mark - Listener static void *WQSSHListener(void *argument) { @autoreleasepool { while (gSSHRunning) { ssh_session session = ssh_new(); if (!session) break; if (ssh_bind_accept(gSSHBind, session) != SSH_OK) { ssh_free(session); if (!gSSHRunning) break; usleep(20000); continue; } pthread_t thread; if (pthread_create(&thread, NULL, WQSSHClientThread, session) == 0) { pthread_detach(thread); } else { ssh_disconnect(session); ssh_free(session); } } } return NULL; } static BOOL WQSSHTryPort(NSInteger port, NSString **error) { gSSHBind = ssh_bind_new(); if (!gSSHBind) { if (error) *error = @"ssh_bind_new failed"; return NO; } unsigned int bindPort = (unsigned int)port; ssh_bind_options_set(gSSHBind, SSH_BIND_OPTIONS_BINDADDR, "0.0.0.0"); ssh_bind_options_set(gSSHBind, SSH_BIND_OPTIONS_BINDPORT, &bindPort); if (gSSHHostKey) ssh_bind_options_set(gSSHBind, SSH_BIND_OPTIONS_IMPORT_KEY, gSSHHostKey); if (ssh_bind_listen(gSSHBind) != SSH_OK) { if (error) *error = [NSString stringWithFormat:@"listen failed: %s", ssh_get_error(gSSHBind)]; ssh_bind_free(gSSHBind); gSSHBind = NULL; return NO; } gSSHPort = port; return YES; } BOOL WQSSHServerStart(NSString **error) { @synchronized (gSSHLock ?: (gSSHLock = [NSObject new])) { if (gSSHRunning) return YES; if (!WQSSHLibraryAvailable()) { if (error) *error = @"libssh is not bundled in this build"; return NO; } ssh_init(); if (!gSSHHostKey) { if (ssh_pki_generate(SSH_KEYTYPE_RSA, 2048, &gSSHHostKey) != SSH_OK) { if (error) *error = @"failed to generate SSH host key"; ssh_finalize(); return NO; } } gSSHRunning = YES; BOOL bound = NO; NSString *lastError = nil; NSInteger requested = WQSSHServerPort(); for (NSInteger port = requested; port < requested + 8; port++) { if (WQSSHTryPort(port, &lastError)) { bound = YES; break; } } if (!bound) { gSSHRunning = NO; ssh_finalize(); if (error) *error = lastError ?: @"unable to bind SSH port"; WQSSHLog([NSString stringWithFormat:@"start failed: %@", lastError]); return NO; } if (pthread_create(&gSSHListenerThread, NULL, WQSSHListener, NULL) != 0) { gSSHRunning = NO; ssh_bind_free(gSSHBind); gSSHBind = NULL; if (error) *error = @"failed to create SSH listener thread"; return NO; } pthread_detach(gSSHListenerThread); gSSHLastError = nil; WQSSHLog([NSString stringWithFormat:@"started port=%ld url=%@", (long)gSSHPort, WQSSHServerURL()]); return YES; } } void WQSSHServerStop(void) { @synchronized (gSSHLock ?: (gSSHLock = [NSObject new])) { if (!gSSHRunning && !gSSHBind) return; gSSHRunning = NO; if (gSSHBind) { ssh_bind_free(gSSHBind); gSSHBind = NULL; } if (gSSHHostKey) { ssh_key_free(gSSHHostKey); gSSHHostKey = NULL; } ssh_finalize(); WQSSHLog(@"stopped"); } } BOOL WQSSHServerIsRunning(void) { return gSSHRunning; } NSInteger WQSSHServerPort(void) { id value = [NSUserDefaults.standardUserDefaults objectForKey:@"WQSSHPort"]; NSInteger port = [value respondsToSelector:@selector(integerValue)] ? [value integerValue] : 2222; if (port < 1024 || port > 65535) port = 2222; return port; } NSString *WQSSHServerURL(void) { struct ifaddrs *interfaces = NULL; NSString *ip = nil; if (getifaddrs(&interfaces) == 0) { for (struct ifaddrs *cursor = interfaces; cursor; cursor = cursor->ifa_next) { if (!cursor->ifa_addr || cursor->ifa_addr->sa_family != AF_INET) continue; NSString *name = [NSString stringWithUTF8String:cursor->ifa_name ?: ""]; if ([name isEqualToString:@"lo0"]) continue; char address[INET_ADDRSTRLEN] = {0}; struct sockaddr_in *sin = (struct sockaddr_in *)cursor->ifa_addr; if (!inet_ntop(AF_INET, &sin->sin_addr, address, sizeof(address))) continue; NSString *candidate = [NSString stringWithUTF8String:address]; if ([name isEqualToString:@"en0"] || [name isEqualToString:@"en1"]) { ip = candidate; break; } if (!ip) ip = candidate; } freeifaddrs(interfaces); } return [NSString stringWithFormat:@"ssh -p %ld mobile@%@", (long)gSSHPort, ip ?: @"127.0.0.1"]; }