├── IMG_20240330_015103.jpg ├── readme.md ├── sshclient.ino └── wifissh.bin /IMG_20240330_015103.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SUB0PT1MAL/M5Cardputer_Interactive_SSH_Client/944336fb415fa607660cbf3a3f86f635ef8eb788/IMG_20240330_015103.jpg -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # Interactive SSH client with in-device WiFi and SSH connection setup 2 | 3 | ## Overview 4 | Forked a SSH client for the M5Cardputer so you can connect to WiFi and different SSH hosts dinamically from the cardputer itself with support for saving and loading the WiFi and SSH credentiasl from a file. 5 | 6 | Original project from https://github.com/fernandofatech/M5Cardputer-SSHClient 7 | 8 | ![IMG_3531](IMG_20240330_015103.jpg) 9 | 10 | ## Prerequisites 11 | 12 | Before using this application, make sure you have the following: 13 | 14 | - An M5Cardputer. 15 | - A microSD card formatted FAT32. 16 | - WiFi credentials (SSID and password) for the network you want to connect to. 17 | - SSH server credentials (hostname, username, and password) for the remote server you want to access. 18 | - (WIP) WireGuard configuration file (`wg.conf`) if you plan to use the VPN functionality. (WIP) 19 | 20 | ## Usage 21 | 22 | 2. The application will prompt you to choose whether to use saved WiFi and SSH credentials or input them manually. 23 | 3. If you choose to use saved credentials, the application will attempt to load them from the `/sshclient/session.wifi` and `/sshclient/session.ssh` files on the microSD card. !!CURRENTLY THE CREDENTIALS ARE SAVED IN PLAIN TEXT!! 24 | 4. If the credential files are not found or if you choose to input manually, you will be prompted to enter the WiFi SSID, password, SSH hostname, username, and password. 25 | 5. After entering the credentials, you will be asked if you want to save them to the respective files on the microSD card. 26 | 6. The application will attempt to connect to the WiFi network and the remote SSH server. 27 | 7. If you have a WireGuard configuration file (`/sshclient/wg.conf`) on the microSD card, you will be prompted to use the WireGuard VPN. Select "Y" to enable the VPN or "N" to continue without it. 28 | 8. Once connected, you can interact with the remote SSH server using the M5Cardputer's keyboard and display. 29 | 30 | ## Limitations 31 | 32 | - This application does not support file transfers or advanced SSH features beyond interactive terminal sessions. 33 | - The WireGuard VPN implementation is WIP. The code implementation is mostly done, but i'm having issues making the WireGuard-ESP32 library work, it keeps looping on attempting the handshake. Trying the official demo for the library I observe the same behaviour. 34 | 35 | ## FYI 36 | 37 | I just get tangled on those silly projects from time to time and i drop them immediately. Dont expect any future support. 38 | -------------------------------------------------------------------------------- /sshclient.ino: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include "libssh_esp32.h" 7 | #include 8 | 9 | #define BGCOLOR TFT_BLACK 10 | #define FGCOLOR TFT_WHITE 11 | 12 | // Saved credentials path 13 | const char* SSH_CRED_FILE = "/sshclient/session.ssh"; 14 | const char* WIFI_CRED_FILE = "/sshclient/session.wifi"; 15 | const char* WG_CONFIG_FILE = "/sshclient/wg.conf"; 16 | 17 | // WireGuard variables 18 | char private_key[45]; 19 | IPAddress local_ip; 20 | char public_key[45]; 21 | char endpoint_address[16]; 22 | int endpoint_port = 31337; 23 | static constexpr const uint32_t UPDATE_INTERVAL_MS = 5000; 24 | static WireGuard wg; 25 | bool useWireGuard = false; 26 | 27 | // SSH variables 28 | const char* ssid = ""; 29 | const char* password = ""; 30 | const char* ssh_host = ""; 31 | const char* ssh_user = ""; 32 | const char* ssh_password = ""; 33 | 34 | // M5Cardputer setup 35 | M5Canvas canvas(&M5Cardputer.Display); 36 | String commandBuffer = ""; 37 | int cursorY = 0; 38 | const int lineHeight = 32; 39 | unsigned long lastKeyPressMillis = 0; 40 | const unsigned long debounceDelay = 150; 41 | 42 | String readUserInput(bool isYesNoInput = false) { 43 | String input = ""; 44 | bool inputComplete = false; 45 | 46 | while (!inputComplete) { 47 | M5Cardputer.update(); 48 | if (M5Cardputer.Keyboard.isChange()) { 49 | if (M5Cardputer.Keyboard.isPressed()) { 50 | Keyboard_Class::KeysState status = M5Cardputer.Keyboard.keysState(); 51 | for (auto i : status.word) { 52 | input += i; 53 | M5Cardputer.Display.print(i); 54 | } 55 | 56 | if (status.del && !input.isEmpty()) { 57 | input.remove(input.length() - 1); 58 | M5Cardputer.Display.setCursor(M5Cardputer.Display.getCursorX() - 6, M5Cardputer.Display.getCursorY()); 59 | M5Cardputer.Display.print(" "); // Print a space to erase the last character 60 | M5Cardputer.Display.setCursor(M5Cardputer.Display.getCursorX() - 6, M5Cardputer.Display.getCursorY()); 61 | } 62 | 63 | if (status.enter || (isYesNoInput && (input == "Y" || input == "y" || input == "N" || input == "n"))) { 64 | inputComplete = true; 65 | } 66 | } 67 | } 68 | } 69 | return input; 70 | } 71 | 72 | void setup() { 73 | auto cfg = M5.config(); 74 | M5Cardputer.begin(cfg, true); 75 | M5Cardputer.Display.setRotation(1); 76 | M5Cardputer.Display.setTextSize(1); // Set text size 77 | 78 | M5Cardputer.Display.println("SSH Client v1.7 by SUB0PT1MAL"); 79 | 80 | if (!SD.begin(SS)) { 81 | M5Cardputer.Display.println("Failed to mount SD card file system."); 82 | // You might want to add some error handling or retry logic here 83 | return; 84 | } 85 | // Prompt WiFi setup 86 | M5Cardputer.Display.print("\nUse saved WiFi credentials? (Y/N): "); 87 | String useWiFiCredentials = readUserInput(true); 88 | 89 | String ssid, password; 90 | bool wifiCredentialsLoadedFromFile = false; 91 | if (useWiFiCredentials == "Y" || useWiFiCredentials == "y") { 92 | if (loadWiFiCredentials(ssid, password)) { 93 | M5Cardputer.Display.println("\nWiFi credentials loaded."); 94 | wifiCredentialsLoadedFromFile = true; 95 | } else { 96 | M5Cardputer.Display.println("\nFailed to load WiFi credentials."); 97 | M5Cardputer.Display.print("\nSSID: "); 98 | ssid = readUserInput(); 99 | M5Cardputer.Display.print("\nPassword: "); 100 | password = readUserInput(); 101 | } 102 | } else { 103 | M5Cardputer.Display.print("\nSSID: "); 104 | ssid = readUserInput(); 105 | M5Cardputer.Display.print("\nPassword: "); 106 | password = readUserInput(); 107 | } 108 | 109 | if (!wifiCredentialsLoadedFromFile) { // Only prompt if credentials were not loaded from a file 110 | M5Cardputer.Display.print("\nSave WiFi credentials? (Y/N): "); 111 | String saveWiFiCredentials = readUserInput(true); 112 | if (saveWiFiCredentials == "Y" || saveWiFiCredentials == "y") { 113 | ::saveWiFiCredentials(ssid.c_str(), password.c_str()); 114 | } 115 | } 116 | 117 | // Connect to WiFi 118 | //WiFi.begin(ssid, password); 119 | //while (WiFi.status() != WL_CONNECTED) { 120 | // delay(500); 121 | //} 122 | 123 | // Connect to WiFi 124 | Serial.println("SSID contents:"); 125 | Serial.println(ssid); 126 | 127 | Serial.println("SSID memory representation:"); 128 | for (int i = 0; i < ssid.length(); i++) { 129 | Serial.print(static_cast(ssid[i]), HEX); 130 | Serial.print(" "); 131 | } 132 | Serial.println(); 133 | 134 | Serial.println("Password contents:"); 135 | Serial.println(password); 136 | 137 | Serial.println("Password memory representation:"); 138 | for (int i = 0; i < password.length(); i++) { 139 | Serial.print(static_cast(password[i]), HEX); 140 | Serial.print(" "); 141 | } 142 | Serial.println(); 143 | 144 | WiFi.begin(ssid.c_str(), password.c_str()); 145 | 146 | while (WiFi.status() != WL_CONNECTED) { 147 | delay(500); 148 | } 149 | 150 | // Prompt for WireGuard setup 151 | M5Cardputer.Display.print("\nUse WireGuard VPN? (Y/N) WIP: "); 152 | String useWireGuardInput = readUserInput(true); 153 | while (useWireGuardInput != "Y" && useWireGuardInput != "y" && useWireGuardInput != "N" && useWireGuardInput != "n") { 154 | M5Cardputer.update(); 155 | if (M5Cardputer.Keyboard.isChange()) { 156 | if (M5Cardputer.Keyboard.isPressed()) { 157 | Keyboard_Class::KeysState status = M5Cardputer.Keyboard.keysState(); 158 | for (auto i : status.word) { 159 | useWireGuardInput += i; 160 | M5Cardputer.Display.print(i); 161 | } 162 | if (status.enter) { 163 | break; 164 | } 165 | } 166 | } 167 | } 168 | 169 | useWireGuard = (useWireGuardInput == "Y" || useWireGuardInput == "y"); 170 | 171 | if (useWireGuard) { 172 | read_and_parse_file(); 173 | wg_setup(); 174 | } 175 | 176 | // Prompt SSH setup 177 | M5Cardputer.Display.print("\nUse saved SSH credentials? (Y/N): "); 178 | String useSSHCredentials = readUserInput(true); 179 | 180 | String ssh_host, ssh_user, ssh_password; 181 | bool sshCredentialsLoadedFromFile = false; 182 | if (useSSHCredentials == "Y" || useSSHCredentials == "y") { 183 | if (loadSSHCredentials(ssh_host, ssh_user, ssh_password)) { 184 | M5Cardputer.Display.println("\nSSH credentials loaded."); 185 | sshCredentialsLoadedFromFile = true; 186 | } else { 187 | M5Cardputer.Display.println("\nFailed to load SSH credentials."); 188 | M5Cardputer.Display.print("\nHost: "); 189 | ssh_host = readUserInput(); 190 | M5Cardputer.Display.print("\nUsername: "); 191 | ssh_user = readUserInput(); 192 | M5Cardputer.Display.print("\nPassword: "); 193 | ssh_password = readUserInput(); 194 | } 195 | } else { 196 | M5Cardputer.Display.print("\nHost: "); 197 | ssh_host = readUserInput(); 198 | M5Cardputer.Display.print("\nUsername: "); 199 | ssh_user = readUserInput(); 200 | M5Cardputer.Display.print("\nPassword: "); 201 | ssh_password = readUserInput(); 202 | } 203 | 204 | if (!sshCredentialsLoadedFromFile) { // Only prompt if credentials were not loaded from a file 205 | M5Cardputer.Display.print("\nSave SSH credentials? (Y/N): "); 206 | String saveSSHCredentials = readUserInput(true); 207 | if (saveSSHCredentials == "Y" || saveSSHCredentials == "y") { 208 | ::saveSSHCredentials(ssh_host.c_str(), ssh_user.c_str(), ssh_password.c_str()); 209 | } 210 | } 211 | 212 | // Connect to SSH server 213 | TaskHandle_t sshTaskHandle = NULL; 214 | xTaskCreatePinnedToCore(sshTask, "SSH Task", 20000, NULL, 1, &sshTaskHandle, 1); 215 | if (sshTaskHandle == NULL) { 216 | Serial.println("Failed to create SSH Task"); 217 | } 218 | 219 | // Initialize the cursor Y position 220 | cursorY = M5Cardputer.Display.getCursorY(); 221 | } 222 | 223 | void loop() { 224 | M5Cardputer.update(); 225 | 226 | if (useWireGuard) { 227 | wg_loop(); 228 | } 229 | } 230 | 231 | void wg_loop() { 232 | 233 | } 234 | 235 | ssh_session connect_ssh(const char *host, const char *user, int verbosity) { 236 | ssh_session session = ssh_new(); 237 | if (session == NULL) { 238 | Serial.println("\nFailed to create SSH session"); 239 | return NULL; 240 | } 241 | 242 | ssh_options_set(session, SSH_OPTIONS_HOST, host); 243 | ssh_options_set(session, SSH_OPTIONS_USER, user); 244 | ssh_options_set(session, SSH_OPTIONS_LOG_VERBOSITY, &verbosity); 245 | 246 | if (ssh_connect(session) != SSH_OK) { 247 | Serial.print("\nError connecting to host: "); 248 | Serial.println(ssh_get_error(session)); 249 | ssh_free(session); 250 | return NULL; 251 | } 252 | 253 | return session; 254 | } 255 | 256 | int authenticate_console(ssh_session session, const char *password) { 257 | int rc = ssh_userauth_password(session, NULL, password); 258 | if (rc != SSH_AUTH_SUCCESS) { 259 | Serial.print("\nError authenticating with password: "); 260 | Serial.println(ssh_get_error(session)); 261 | return rc; 262 | } 263 | return SSH_OK; 264 | } 265 | 266 | void sshTask(void *pvParameters) { 267 | ssh_session my_ssh_session = connect_ssh(ssh_host, ssh_user, SSH_LOG_PROTOCOL); 268 | if (my_ssh_session == NULL) { 269 | M5Cardputer.Display.println("\nSSH Connection failed."); 270 | vTaskDelete(NULL); 271 | return; 272 | } 273 | 274 | M5Cardputer.Display.println("\nSSH Connection established."); 275 | if (authenticate_console(my_ssh_session, ssh_password) != SSH_OK) { 276 | M5Cardputer.Display.println("\nSSH Authentication failed."); 277 | ssh_disconnect(my_ssh_session); 278 | ssh_free(my_ssh_session); 279 | vTaskDelete(NULL); 280 | return; 281 | } 282 | 283 | M5Cardputer.Display.println("\nSSH Authentication succeeded."); 284 | 285 | // Open a new channel for the SSH session 286 | ssh_channel channel = ssh_channel_new(my_ssh_session); 287 | if (channel == NULL || ssh_channel_open_session(channel) != SSH_OK) { 288 | M5Cardputer.Display.println("\nSSH Channel open error."); 289 | ssh_disconnect(my_ssh_session); 290 | ssh_free(my_ssh_session); 291 | vTaskDelete(NULL); 292 | return; 293 | } 294 | 295 | // Request a pseudo-terminal 296 | if (ssh_channel_request_pty(channel) != SSH_OK) { 297 | M5Cardputer.Display.println("\nRequest PTY failed."); 298 | ssh_channel_close(channel); 299 | ssh_channel_free(channel); 300 | ssh_disconnect(my_ssh_session); 301 | ssh_free(my_ssh_session); 302 | vTaskDelete(NULL); 303 | return; 304 | } 305 | 306 | // Start a shell session 307 | if (ssh_channel_request_shell(channel) != SSH_OK) { 308 | M5Cardputer.Display.println("\nRequest shell failed."); 309 | ssh_channel_close(channel); 310 | ssh_channel_free(channel); 311 | ssh_disconnect(my_ssh_session); 312 | ssh_free(my_ssh_session); 313 | vTaskDelete(NULL); 314 | return; 315 | } 316 | 317 | while (true) { 318 | M5Cardputer.update(); 319 | 320 | // Handle keyboard input with debounce 321 | if (M5Cardputer.Keyboard.isChange()) { 322 | if (M5Cardputer.Keyboard.isPressed()) { 323 | unsigned long currentMillis = millis(); 324 | if (currentMillis - lastKeyPressMillis >= debounceDelay) { 325 | Keyboard_Class::KeysState status = M5Cardputer.Keyboard.keysState(); 326 | 327 | for (auto i : status.word) { 328 | commandBuffer += i; 329 | M5Cardputer.Display.print(i); // Display the character as it's typed 330 | cursorY = M5Cardputer.Display.getCursorY(); // Update cursor Y position 331 | } 332 | 333 | if (status.del && commandBuffer.length() > 2) { 334 | commandBuffer.remove(commandBuffer.length() - 1); 335 | M5Cardputer.Display.setCursor(M5Cardputer.Display.getCursorX() - 6, M5Cardputer.Display.getCursorY()); 336 | M5Cardputer.Display.print(" "); // Print a space to erase the last character 337 | M5Cardputer.Display.setCursor(M5Cardputer.Display.getCursorX() - 6, M5Cardputer.Display.getCursorY()); 338 | cursorY = M5Cardputer.Display.getCursorY(); // Update cursor Y position 339 | } 340 | 341 | if (status.enter) { 342 | String message = commandBuffer.substring(2) + "\r\n"; // Use "\r\n" for newline 343 | ssh_channel_write(channel, message.c_str(), message.length()); // Send message to SSH server 344 | 345 | commandBuffer = "> "; 346 | M5Cardputer.Display.print('\n'); // Move to the next line 347 | cursorY = M5Cardputer.Display.getCursorY(); // Update cursor Y position 348 | } 349 | 350 | lastKeyPressMillis = currentMillis; 351 | } 352 | } 353 | } 354 | 355 | // Check if the cursor has reached the bottom of the display 356 | if (cursorY > M5Cardputer.Display.height() - lineHeight) { 357 | // Scroll the display up by one line 358 | M5Cardputer.Display.scroll(0, -lineHeight); 359 | 360 | // Reset the cursor to the new line position 361 | cursorY -= lineHeight; 362 | M5Cardputer.Display.setCursor(M5Cardputer.Display.getCursorX(), cursorY); 363 | } 364 | 365 | char buffer[1024]; 366 | int nbytes = ssh_channel_read_nonblocking(channel, buffer, sizeof(buffer), 0); 367 | if (nbytes > 0) { 368 | for (int i = 0; i < nbytes; ++i) { 369 | if (buffer[i] == '\r') { 370 | continue; // Handle carriage return 371 | } 372 | M5Cardputer.Display.write(buffer[i]); 373 | cursorY = M5Cardputer.Display.getCursorY(); // Update cursor Y position 374 | } 375 | } 376 | 377 | if (nbytes < 0 || ssh_channel_is_closed(channel)) { 378 | break; 379 | } 380 | } 381 | 382 | // Clean up 383 | ssh_channel_close(channel); 384 | ssh_channel_free(channel); 385 | ssh_disconnect(my_ssh_session); 386 | ssh_free(my_ssh_session); 387 | vTaskDelete(NULL); 388 | } 389 | 390 | void wg_setup() 391 | { 392 | read_and_parse_file(); 393 | 394 | Serial.println("Adjusting system time..."); 395 | configTime(9 * 60 * 60, 0, "ntp.jst.mfeed.ad.jp", "ntp.nict.jp", "time.google.com"); 396 | M5Cardputer.Display.clear(); 397 | M5Cardputer.Display.setCursor(0, 0); 398 | 399 | Serial.println("Connected. Initializing WireGuard..."); 400 | M5Cardputer.Display.println("Connecting to wireguard..."); 401 | wg.begin( 402 | local_ip, 403 | private_key, 404 | endpoint_address, 405 | public_key, 406 | endpoint_port); 407 | Serial.println(local_ip); 408 | Serial.println(private_key); 409 | Serial.println(endpoint_address); 410 | Serial.println(public_key); 411 | Serial.println(endpoint_port); 412 | 413 | M5Cardputer.Display.clear(); 414 | M5Cardputer.Display.setCursor(0, 0); 415 | 416 | M5Cardputer.Display.setTextColor(GREEN, BGCOLOR); 417 | M5Cardputer.Display.println("Connected!"); 418 | M5Cardputer.Display.setTextColor(FGCOLOR, BGCOLOR); 419 | M5Cardputer.Display.print("IP on tunnel:"); 420 | M5Cardputer.Display.setTextColor(WHITE, BGCOLOR); 421 | M5Cardputer.Display.println(local_ip); 422 | M5Cardputer.Display.setTextColor(FGCOLOR, BGCOLOR); 423 | Serial.println(local_ip); 424 | 425 | } 426 | 427 | void read_and_parse_file() { 428 | if (!SD.begin(SS)) { 429 | Serial.println("\nFailed to initialize SD card"); 430 | return; 431 | } 432 | 433 | File file = SD.open(WG_CONFIG_FILE); 434 | if (!file) { 435 | M5Cardputer.Display.clear(); 436 | M5Cardputer.Display.setCursor(0, 0); 437 | 438 | M5Cardputer.Display.setTextColor(RED, BGCOLOR); 439 | Serial.println("\nFailed to open file"); 440 | M5Cardputer.Display.println("\nNo wg.conf file found"); 441 | M5Cardputer.Display.setTextColor(FGCOLOR, BGCOLOR); 442 | delay(60000); 443 | return; 444 | } 445 | 446 | Serial.println("Readed config file!"); 447 | 448 | Serial.println("Found file!"); 449 | parse_config_file(file); 450 | } 451 | 452 | void parse_config_file(File configFile) { 453 | String line; 454 | 455 | while (configFile.available()) { 456 | line = configFile.readStringUntil('\n'); 457 | Serial.println("==========PRINTING LINE"); 458 | Serial.println(line); 459 | line.trim(); 460 | 461 | if (line.startsWith("[Interface]") || line.isEmpty()) { 462 | // Skip [Interface] or empty lines 463 | continue; 464 | } else if (line.startsWith("PrivateKey")) { 465 | line.remove(0, line.indexOf('=') + 1); 466 | line.trim(); 467 | Serial.println("Private Key: " + line); 468 | strncpy(private_key, line.c_str(), sizeof(private_key) - 1); 469 | private_key[sizeof(private_key) - 1] = '\0'; // Ensure null-terminated 470 | } else if (line.startsWith("Address")) { 471 | line.remove(0, line.indexOf('=') + 1); 472 | line.trim(); 473 | Serial.println("Local IP: " + line); 474 | int slashIndex = line.indexOf('/'); 475 | 476 | if (slashIndex != -1) { 477 | Serial.println("~~~~~~~~~~~~"); 478 | Serial.println(line.substring(0, slashIndex)); 479 | local_ip.fromString(line.substring(0, slashIndex)); 480 | } 481 | 482 | } else if (line.startsWith("[Peer]")) { 483 | // Handle [Peer] section 484 | } else if (line.startsWith("PublicKey")) { 485 | line.remove(0, line.indexOf('=') + 1); 486 | line.trim(); 487 | Serial.println("Public Key: " + line); 488 | strncpy(public_key, line.c_str(), sizeof(public_key) - 1); 489 | public_key[sizeof(public_key) - 1] = '\0'; // Ensure null-terminated 490 | } else if (line.startsWith("Endpoint")) { 491 | //Serial.println("~~~~~~~~~~~endpoint"); 492 | //Serial.println(line); 493 | line.remove(0, line.indexOf('=') + 1); 494 | line.trim(); 495 | int colonIndex = line.indexOf(':'); 496 | 497 | if (colonIndex != -1) { 498 | //Serial.println("Endpoint Line: " + line); 499 | strncpy(endpoint_address, line.substring(0, colonIndex).c_str(), sizeof(endpoint_address) - 1); 500 | endpoint_address[sizeof(endpoint_address) - 1] = '\0'; // Ensure null-terminated 501 | Serial.println("Endpoint Address: " + String(endpoint_address)); 502 | endpoint_port = line.substring(colonIndex + 1).toInt(); 503 | Serial.println("Endpoint Port: " + String(endpoint_port)); 504 | } 505 | } 506 | } 507 | 508 | Serial.println("Closing file!"); 509 | configFile.close(); 510 | } 511 | 512 | void saveWiFiCredentials(const char* ssid, const char* password) { 513 | if (!SD.begin(SS)) { 514 | M5Cardputer.Display.println("\nFailed to initialize SD card."); 515 | return; 516 | } 517 | Serial.println("SD card initialized successfully."); 518 | 519 | File file = SD.open(WIFI_CRED_FILE, FILE_WRITE); 520 | if (!file) { 521 | M5Cardputer.Display.println("\nFailed to open file for writing WiFi credentials."); 522 | return; 523 | } 524 | Serial.println("File opened successfully for writing WiFi credentials."); 525 | 526 | file.println(ssid); 527 | file.print(password); 528 | file.close(); 529 | M5Cardputer.Display.println("\nWiFi credentials saved."); 530 | } 531 | 532 | bool loadWiFiCredentials(String& ssid, String& password) { 533 | File file = SD.open(WIFI_CRED_FILE, FILE_READ); 534 | if (!file) { 535 | Serial.println("Failed to open WiFi credentials file."); 536 | return false; 537 | } 538 | Serial.println("WiFi credentials file opened successfully."); 539 | 540 | ssid = file.readStringUntil('\n'); 541 | password = file.readStringUntil('\n'); 542 | ssid.trim(); 543 | password.trim(); 544 | file.close(); 545 | 546 | // Create new char arrays or std::string objects to store the loaded credentials 547 | static char ssid_buf[128]; 548 | static char password_buf[128]; 549 | 550 | strncpy(ssid_buf, ssid.c_str(), sizeof(ssid_buf) - 1); 551 | ssid_buf[sizeof(ssid_buf) - 1] = '\0'; 552 | strncpy(password_buf, password.c_str(), sizeof(password_buf) - 1); 553 | password_buf[sizeof(password_buf) - 1] = '\0'; 554 | 555 | // Update the global const char* variables with the loaded credentials 556 | ssid = ssid_buf; 557 | password = password_buf; 558 | 559 | Serial.printf("SSID: %s\nPassword: %s\n", ssid, password); 560 | return true; 561 | } 562 | 563 | void saveSSHCredentials(const char* host, const char* user, const char* password) { 564 | File file = SD.open(SSH_CRED_FILE, FILE_WRITE); 565 | if (!SD.begin(SS)) { 566 | M5Cardputer.Display.println("\nFailed to initialize SD card."); 567 | return; 568 | } 569 | if (file) { 570 | file.println(host); 571 | file.println(user); 572 | file.print(password); 573 | file.close(); 574 | M5Cardputer.Display.println("\nSSH credentials saved."); 575 | } else { 576 | M5Cardputer.Display.println("\nFailed to save SSH credentials."); 577 | } 578 | } 579 | 580 | bool loadSSHCredentials(String& host, String& user, String& password) { 581 | File file = SD.open(SSH_CRED_FILE, FILE_READ); 582 | if (!file) { 583 | Serial.println("Failed to open SSH credentials file."); 584 | return false; 585 | } 586 | Serial.println("SSH credentials file opened successfully."); 587 | 588 | host = file.readStringUntil('\n'); 589 | user = file.readStringUntil('\n'); 590 | password = file.readStringUntil('\n'); 591 | host.trim(); 592 | user.trim(); 593 | password.trim(); 594 | file.close(); 595 | 596 | // Create new char arrays or std::string objects to store the loaded credentials 597 | static char ssh_host_buf[128]; 598 | static char ssh_user_buf[128]; 599 | static char ssh_password_buf[128]; 600 | 601 | strncpy(ssh_host_buf, host.c_str(), sizeof(ssh_host_buf) - 1); 602 | ssh_host_buf[sizeof(ssh_host_buf) - 1] = '\0'; 603 | strncpy(ssh_user_buf, user.c_str(), sizeof(ssh_user_buf) - 1); 604 | ssh_user_buf[sizeof(ssh_user_buf) - 1] = '\0'; 605 | strncpy(ssh_password_buf, password.c_str(), sizeof(ssh_password_buf) - 1); 606 | ssh_password_buf[sizeof(ssh_password_buf) - 1] = '\0'; 607 | 608 | // Assign the loaded credentials to the global const char* variables 609 | ssh_host = ssh_host_buf; 610 | ssh_user = ssh_user_buf; 611 | ssh_password = ssh_password_buf; 612 | 613 | return true; 614 | } -------------------------------------------------------------------------------- /wifissh.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SUB0PT1MAL/M5Cardputer_Interactive_SSH_Client/944336fb415fa607660cbf3a3f86f635ef8eb788/wifissh.bin --------------------------------------------------------------------------------