From be884ded91880c991dd3a7c9a90d1c34fccc3e3b Mon Sep 17 00:00:00 2001 From: defnax <9952056+defnax@users.noreply.github.com> Date: Tue, 11 Aug 2026 18:56:26 +0200 Subject: [PATCH 1/5] Added account creation for retroshare service --- retroshare-service/src/retroshare-service.cc | 244 ++++++++++++++----- 1 file changed, 180 insertions(+), 64 deletions(-) diff --git a/retroshare-service/src/retroshare-service.cc b/retroshare-service/src/retroshare-service.cc index 52a9052a9..7b53035fb 100644 --- a/retroshare-service/src/retroshare-service.cc +++ b/retroshare-service/src/retroshare-service.cc @@ -24,6 +24,15 @@ #include #include #include +#include +#include + +#ifdef WINDOWS_SYS +#include +#include +#else +#include +#endif #include "retroshare/rsinit.h" #include "retroshare/rstor.h" @@ -70,6 +79,17 @@ std::string colored(int color,const std::string& s) } } +#ifdef RS_SERVICE_TERMINAL_LOGIN +static bool hasInteractiveStdin() +{ +#ifdef WINDOWS_SYS + return _isatty(_fileno(stdin)) != 0; +#else + return isatty(fileno(stdin)) != 0; +#endif +} +#endif + static void eventHandler(std::shared_ptr e) { auto fe = dynamic_cast(e.get()); @@ -124,6 +144,92 @@ void signalHandler(int signal) } + +#ifdef RS_SERVICE_TERMINAL_LOGIN +static bool doTerminalCreateAccount() +{ + if(!hasInteractiveStdin()) + { + RsErr() << "Account creation requires an interactive terminal." << std::endl; + return false; + } + + std::cout << std::endl + << colored(COLOR_GREEN, "=== Create New RetroShare Account ===") << std::endl << std::endl; + + std::string pgpName; + while (keepRunning && pgpName.empty()) + { + std::cout << colored(COLOR_GREEN, "Please enter your Username: "); + std::cout.flush(); + if(!std::getline(std::cin, pgpName)) + { + RsErr() << "Unable to read the account name from the terminal." << std::endl; + return false; + } + if (pgpName.empty()) + std::cout << colored(COLOR_RED, "Name cannot be empty!") << std::endl; + } + if (!keepRunning) return false; + + std::string locationName; + while (keepRunning && locationName.empty()) + { + std::cout << colored(COLOR_GREEN, "Please enter Node/Location Name (e.g. Laptop, Home): "); + std::cout.flush(); + if(!std::getline(std::cin, locationName)) + { + RsErr() << "Unable to read the location name from the terminal." << std::endl; + return false; + } + if (locationName.empty()) + std::cout << colored(COLOR_RED, "Location name cannot be empty!") << std::endl; + } + if (!keepRunning) return false; + + std::string pass1, pass2; + while (keepRunning) + { + pass1 = RsUtil::rs_getpass(colored(COLOR_GREEN, "Please enter passphrase for new account: ")); + pass2 = RsUtil::rs_getpass(colored(COLOR_GREEN, "Please enter the same passphrase again: ")); + + if (pass1 != pass2) + { + std::cout << colored(COLOR_RED, "Passphrases do not match! Please try again.") << std::endl; + continue; + } + if (pass1.empty()) + { + std::cout << colored(COLOR_RED, "Passphrase cannot be empty! Please try again.") << std::endl; + continue; + } + break; + } + if (!keepRunning) return false; + + std::cout << colored(COLOR_YELLOW, "Generating 4096-bit PGP key & SSL certificate (this may take a few seconds)...") << std::endl; + + RsPeerId locationId; + RsPgpId pgpId; + std::error_condition err = rsLoginHelper->createLocationV2(locationId, pgpId, locationName, pgpName, pass1); + + if (err) + { + RsErr() << colored(COLOR_RED, "Account creation failed: " + err.message()) << std::endl; + return false; + } + + std::cout << std::endl + << colored(COLOR_GREEN, "Account successfully created and logged in!") << std::endl; + std::cout << colored(COLOR_GREEN, " Location ID : ") << colored(COLOR_YELLOW, locationId.toStdString()) << std::endl; + std::cout << colored(COLOR_GREEN, " PGP ID : ") << colored(COLOR_BLUE, pgpId.toStdString()) << std::endl << std::endl; + + return true; +} +#endif + + + int main(int argc, char* argv[]) { signal(SIGINT, signalHandler); @@ -209,7 +315,7 @@ int main(int argc, char* argv[]) #ifdef RS_SERVICE_TERMINAL_LOGIN as >> parameter( 'U', "user-id", prefUserString, "ID", "[node Id] Selected account to use and asks for passphrase" - ". Use \"-U list\" in order to list available accounts.", + ". Use \"-U list\" to list accounts, or \"-U create\" to create a new account.", false ); #endif // def RS_SERVICE_TERMINAL_LOGIN @@ -302,83 +408,94 @@ int main(int argc, char* argv[]) #ifdef RS_SERVICE_TERMINAL_LOGIN if(!prefUserString.empty()) // Login from terminal requested { - if(prefUserString == "list") + bool alreadyLoggedIn = false; + + if(prefUserString == "create") + { + alreadyLoggedIn = doTerminalCreateAccount(); + if (!alreadyLoggedIn) return -1; + } + else if(prefUserString == "list") { std::vector locations; rsLoginHelper->getLocations(locations); - if(locations.size() == 0) - { - RsErr() << colored(COLOR_RED,"No available accounts. You cannot use option -U list") << std::endl; - return -RsInit::ERR_NO_AVAILABLE_ACCOUNT; - } - - std::cout << std::endl << std::endl - << colored(COLOR_GREEN,"Available accounts:") << std::endl<( ceil(log(locations.size())/log(10.0)) ); - - for( uint32_t i=0; i= locations.size())) + if(locations.size() == 0) { - std::cout << colored(COLOR_GREEN,"Please enter account number: "); - std::cout.flush(); + RsErr() << colored(COLOR_RED,"No available accounts. Use -U create from an interactive terminal to create one.") << std::endl; + return -RsInit::ERR_NO_AVAILABLE_ACCOUNT; + } + else + { + std::cout << std::endl << std::endl + << colored(COLOR_GREEN,"Available accounts:") << std::endl<( ceil(log(locations.size() + 1)/log(10.0)) ); - nacc = static_cast(atoi(inputStr.c_str())-1); - if(nacc < locations.size()) + for( uint32_t i=0; i(atoi(inputStr.c_str())-1); + if(nacc < locations.size()) + { + prefUserString = locations[nacc].mLocationId.toStdString(); + break; + } } - nacc=0; // allow to continue if something goes wrong. } } - - RsPeerId ssl_id(prefUserString); - if(ssl_id.isNull()) + if(!alreadyLoggedIn) { - RsErr() << colored(COLOR_RED,"Invalid User location id: a hexadecimal ID is expected.") - << std::endl; - return -EINVAL; - } + RsPeerId ssl_id(prefUserString); + if(ssl_id.isNull()) + { + RsErr() << colored(COLOR_RED,"Invalid User location id: a hexadecimal ID, 'list', or 'create' is expected.") + << std::endl; + return -EINVAL; + } - //RsServiceNotify* notify = new RsServiceNotify(); - //rsNotify->registerNotifyClient(notify); + // supply empty passwd so that it is properly asked 3 times on console + RsInit::LoadCertificateStatus result = rsLoginHelper->attemptLogin(ssl_id, ""); - // supply empty passwd so that it is properly asked 3 times on console - RsInit::LoadCertificateStatus result = rsLoginHelper->attemptLogin(ssl_id, ""); - - switch(result) - { - case RsInit::OK: break; - case RsInit::ERR_ALREADY_RUNNING: - RsErr() << "Another RetroShare using the same profile is already " - "running on your system. Please close that instance " - "first." << std::endl << "Lock file: " - << RsInit::lockFilePath() << std::endl; - return -RsInit::ERR_ALREADY_RUNNING; - case RsInit::ERR_CANT_ACQUIRE_LOCK: - RsErr() << "An unexpected error occurred when Retroshare tried to " - "acquire the single instance lock file." << std::endl - << "Lock file: " << RsInit::lockFilePath() << std::endl; - return -RsInit::ERR_CANT_ACQUIRE_LOCK; - case RsInit::ERR_UNKNOWN: // Fall-throug - default: - RsErr() << "Cannot login. Check your passphrase." << std::endl - << std::endl; - return -result; + switch(result) + { + case RsInit::OK: break; + case RsInit::ERR_ALREADY_RUNNING: + RsErr() << "Another RetroShare using the same profile is already " + "running on your system. Please close that instance " + "first." << std::endl << "Lock file: " + << RsInit::lockFilePath() << std::endl; + return -RsInit::ERR_ALREADY_RUNNING; + case RsInit::ERR_CANT_ACQUIRE_LOCK: + RsErr() << "An unexpected error occurred when Retroshare tried to " + "acquire the single instance lock file." << std::endl + << "Lock file: " << RsInit::lockFilePath() << std::endl; + return -RsInit::ERR_CANT_ACQUIRE_LOCK; + case RsInit::ERR_UNKNOWN: // Fall-through + default: + RsErr() << "Cannot login. Check your passphrase." << std::endl + << std::endl; + return -result; + } } if(RsAccounts::isTorAuto()) @@ -411,7 +528,6 @@ int main(int argc, char* argv[]) } } #endif // def RS_SERVICE_TERMINAL_LOGIN - rsControl->setShutdownCallback([&](int){keepRunning = false;}); while(keepRunning) From 411ef267adf11d167ee0cd56ce8109360ab05393 Mon Sep 17 00:00:00 2001 From: defnax <9952056+defnax@users.noreply.github.com> Date: Tue, 11 Aug 2026 19:09:52 +0200 Subject: [PATCH 2/5] Added fallback option when no account available --- retroshare-service/src/retroshare-service.cc | 69 +++++++++++++------- 1 file changed, 44 insertions(+), 25 deletions(-) diff --git a/retroshare-service/src/retroshare-service.cc b/retroshare-service/src/retroshare-service.cc index 7b53035fb..02e2e7f46 100644 --- a/retroshare-service/src/retroshare-service.cc +++ b/retroshare-service/src/retroshare-service.cc @@ -420,15 +420,13 @@ int main(int argc, char* argv[]) std::vector locations; rsLoginHelper->getLocations(locations); - if(locations.size() == 0) - { - RsErr() << colored(COLOR_RED,"No available accounts. Use -U create from an interactive terminal to create one.") << std::endl; - return -RsInit::ERR_NO_AVAILABLE_ACCOUNT; - } + std::cout << std::endl << std::endl; + if(locations.empty()) + std::cout << colored(COLOR_YELLOW,"No existing RetroShare accounts found.") + << std::endl << std::endl; else { - std::cout << std::endl << std::endl - << colored(COLOR_GREEN,"Available accounts:") << std::endl<( ceil(log(locations.size() + 1)/log(10.0)) ); @@ -439,27 +437,48 @@ int main(int argc, char* argv[]) << colored(COLOR_PURPLE,locations[i].mPgpName + " (" + locations[i].mLocationName + ")" ) << std::endl; - std::cout << std::endl; + } - while(keepRunning) + std::cout << colored(COLOR_GREEN," [c]") << " " + << colored(COLOR_YELLOW,"Create new account") << std::endl + << std::endl; + + if(!hasInteractiveStdin()) + { + RsErr() << "Account selection and creation require an interactive terminal." + << std::endl; + return -RsInit::ERR_NO_AVAILABLE_ACCOUNT; + } + + while(keepRunning) + { + std::cout << colored(COLOR_GREEN,"Please enter account number or 'c' to create: "); + std::cout.flush(); + + std::string inputStr; + if(!std::getline(std::cin, inputStr)) { - std::cout << colored(COLOR_GREEN,"Please enter account number: "); - std::cout.flush(); - - std::string inputStr; - if(!std::getline(std::cin, inputStr)) - { - RsErr() << "Unable to read an account selection from the terminal." << std::endl; - return -RsInit::ERR_NO_AVAILABLE_ACCOUNT; - } - - uint32_t nacc = static_cast(atoi(inputStr.c_str())-1); - if(nacc < locations.size()) - { - prefUserString = locations[nacc].mLocationId.toStdString(); - break; - } + RsErr() << "Unable to read an account selection from the terminal." << std::endl; + return -RsInit::ERR_NO_AVAILABLE_ACCOUNT; } + + if(inputStr == "c" || inputStr == "C") + { + alreadyLoggedIn = doTerminalCreateAccount(); + if(!alreadyLoggedIn) return -1; + break; + } + + char* inputEnd = nullptr; + unsigned long selection = std::strtoul(inputStr.c_str(), &inputEnd, 10); + if(inputEnd != inputStr.c_str() && *inputEnd == '\0' && + selection >= 1 && selection <= locations.size()) + { + prefUserString = locations[selection - 1].mLocationId.toStdString(); + break; + } + + std::cout << colored(COLOR_RED,"Invalid selection. Please try again.") << std::endl; } } From 6622cb95266b42cecd6ba026d995a0876f18aaf9 Mon Sep 17 00:00:00 2001 From: jolavillette Date: Thu, 27 Aug 2026 03:15:08 +0200 Subject: [PATCH 3/5] service: harden the terminal account creation Follow-up to "Added account creation for retroshare service", touching only that feature's own paths. **-U create could only mint a brand-new PGP profile.** A null RsPgpId was always passed to createLocationV2(), so a user who already has a profile, sees it in the -U list output and picks [c] silently got a second identity: their friends no longer recognise them and every certificate has to be exchanged again. createLocationV2() takes pgpId as in/out and reuses it when non-null (rsinit.cc:2247), and it explicitly accepts an empty pgpName in that case (rsinit.cc:2243), so the profiles known to RsAccounts::GetPGPLogins() are now listed first and reusing one just adds this machine as another node. The prompt spells out what a new profile costs. **Ctrl-C at a prompt reported the wrong thing.** On glibc signal() installs the handler with SA_RESTART, so the read blocked behind getline is restarted and keepRunning is only seen once the user also presses Enter. Control then fell out of the selector with prefUserString still "list", RsPeerId("list") null, and the user who had just cancelled was told their location id was invalid, with exit -EINVAL. Cancelling now returns 0. doTerminalCreateAccount() returns a three-state result so a deliberate abort is no longer reported as a failure, and the failure paths use -RsInit::ERR_* instead of a bare -1 (exit 255). **-U list failed on a non-tty even when accounts existed.** It returned ERR_NO_AVAILABLE_ACCOUNT whenever stdin was not a terminal, while the option is documented as a way to list accounts and retroshare-webui's README puts it in the first-start flow. It now prints the list and returns 0, keeping the error for the case it names: nothing to list. The [c] line moved below that check, so a non-interactive run no longer advertises an option it then refuses. **Every prompt is bounded.** A terminal dying mid-prompt -- an ssh session dropping, a container losing its tty -- turns each following read into an immediate EOF, and all these loops re-ask on empty or mismatched input. Three attempts then a clear message. This also covers the -W web interface password loop, which had the same shape before this feature existed. **A passphrase is no longer asked of an absent terminal.** The PASSWORD_REQUESTED handler now checks for an interactive stdin first, so -U under systemd or docker fails with a readable message instead of prompting nothing. Selections are trimmed, so "1 " and a CRLF line are no longer rejected as invalid, and MAX_PROMPT_ATTEMPTS sits outside the terminal-login guard because the web interface password prompt has its own build option. Compile-checked in all four RS_SERVICE_TERMINAL_LOGIN / RS_SERVICE_TERMINAL_WEBUI_PASSWORD combinations, warning-free. Note that the underlying spin is a libretroshare bug: rs_getpass() stored getchar()'s EOF in an unsigned char, where -1 becomes 0xFF and never ends the loop. Fixed separately in libretroshare; the bounds here are useful on their own and do not depend on it. --- retroshare-service/src/retroshare-service.cc | 254 ++++++++++++++++--- 1 file changed, 221 insertions(+), 33 deletions(-) diff --git a/retroshare-service/src/retroshare-service.cc b/retroshare-service/src/retroshare-service.cc index 02e2e7f46..e5d787d1f 100644 --- a/retroshare-service/src/retroshare-service.cc +++ b/retroshare-service/src/retroshare-service.cc @@ -79,7 +79,18 @@ std::string colored(int color,const std::string& s) } } +/** A terminal that dies mid-prompt -- an ssh session dropping, a container + * losing its tty -- turns every following read into an immediate EOF. Every + * prompt below re-asks on empty or mismatched input, so without a bound that + * becomes a loop nobody can interrupt. Kept outside the terminal-login guard: + * the web interface password prompt has its own build option. */ +static constexpr int MAX_PROMPT_ATTEMPTS = 3; + #ifdef RS_SERVICE_TERMINAL_LOGIN +/** On POSIX rs_getpass() reads stdin, so this tests the very channel it uses. + * On Windows it reads the console directly through _getch(), so this is a + * conservative proxy: a service with no console has no interactive stdin + * either. */ static bool hasInteractiveStdin() { #ifdef WINDOWS_SYS @@ -88,6 +99,14 @@ static bool hasInteractiveStdin() return isatty(fileno(stdin)) != 0; #endif } + +static std::string trimmed(const std::string& s) +{ + const std::string blanks = " \t\r\n"; + const auto first = s.find_first_not_of(blanks); + if(first == std::string::npos) return std::string(); + return s.substr(first, s.find_last_not_of(blanks) - first + 1); +} #endif static void eventHandler(std::shared_ptr e) @@ -100,6 +119,18 @@ static void eventHandler(std::shared_ptr e) #ifdef RS_SERVICE_TERMINAL_LOGIN if(fe->mEventCode == RsSystemEventCode::PASSWORD_REQUESTED) { + // The core asks for the passphrase through this event on every login, + // including -U from systemd or docker where there is nothing to + // ask. Answering nothing lets attemptLogin() fail with a proper status; + // prompting an absent terminal cannot succeed and only hides the cause. + if(!hasInteractiveStdin()) + { + RsErr() << "A passphrase is required but stdin is not a terminal. " + "Run retroshare-service interactively, or unlock the " + "profile through the JSON API." << std::endl; + return; + } + std::string question1 = fe->passwd_request_title + colored(COLOR_GREEN,"Please enter your PGP password for key:\n ") + fe->passwd_request_key_details + " :"; std::string password = RsUtil::rs_getpass(question1.c_str()) ; @@ -146,51 +177,159 @@ void signalHandler(int signal) #ifdef RS_SERVICE_TERMINAL_LOGIN -static bool doTerminalCreateAccount() +enum class CreateAccountResult { Created, Cancelled, Failed }; + +/** Ask the user for a PGP profile to sign the new node with. + * Returns false when the user cancelled. A null pgpId on return means "make a + * new profile", which is what createLocationV2() does with a null id. */ +static bool askPgpProfile(RsPgpId& pgpId) +{ + pgpId.clear(); + + std::list pgpIds; + RsAccounts::GetPGPLogins(pgpIds); + + if(pgpIds.empty()) return true; // nothing to reuse, nothing to ask + + std::vector choices(pgpIds.begin(), pgpIds.end()); + + std::cout << std::endl + << colored(COLOR_GREEN, "Existing profiles on this machine:") + << std::endl << std::endl; + + for(size_t i = 0; i < choices.size(); ++i) + { + std::string name, email; + RsAccounts::GetPGPLoginDetails(choices[i], name, email); + std::cout << colored(COLOR_GREEN, " [" + RsUtil::NumberToString(i+1) + "]") << " " + << colored(COLOR_BLUE, choices[i].toStdString()) << ": " + << colored(COLOR_PURPLE, name) << std::endl; + } + + std::cout << colored(COLOR_GREEN, " [n]") << " " + << colored(COLOR_YELLOW, "Create a new profile") << std::endl << std::endl + << colored(COLOR_YELLOW, + "A new profile is a new identity: your existing friends " + "will not recognise it,\nand you will have to exchange " + "certificates with them again. Reuse a profile above\n" + "to simply add this machine as another node of it.") + << std::endl << std::endl; + + for(int attempt = 0; keepRunning && attempt < MAX_PROMPT_ATTEMPTS; ++attempt) + { + std::cout << colored(COLOR_GREEN, "Profile to use, or 'n' for a new one: "); + std::cout.flush(); + + std::string inputStr; + if(!std::getline(std::cin, inputStr)) + { + RsErr() << "Unable to read the profile selection from the terminal." << std::endl; + return false; + } + + inputStr = trimmed(inputStr); + + if(inputStr == "n" || inputStr == "N") return true; + + char* inputEnd = nullptr; + unsigned long selection = std::strtoul(inputStr.c_str(), &inputEnd, 10); + if(inputEnd != inputStr.c_str() && *inputEnd == '\0' && + selection >= 1 && selection <= choices.size()) + { + pgpId = choices[selection - 1]; + return true; + } + + std::cout << colored(COLOR_RED, "Invalid selection. Please try again.") << std::endl; + } + + if(keepRunning) + RsErr() << "Too many invalid selections, giving up." << std::endl; + + return false; +} + +static CreateAccountResult doTerminalCreateAccount() { if(!hasInteractiveStdin()) { RsErr() << "Account creation requires an interactive terminal." << std::endl; - return false; + return CreateAccountResult::Failed; } std::cout << std::endl << colored(COLOR_GREEN, "=== Create New RetroShare Account ===") << std::endl << std::endl; + RsPgpId pgpId; + if(!askPgpProfile(pgpId)) + return keepRunning ? CreateAccountResult::Failed : CreateAccountResult::Cancelled; + + const bool reusingProfile = !pgpId.isNull(); + + // Only asked when a profile is created: reusing one keeps its name, and + // createLocationV2() ignores pgpName as soon as pgpId is not null. std::string pgpName; - while (keepRunning && pgpName.empty()) + if(!reusingProfile) { - std::cout << colored(COLOR_GREEN, "Please enter your Username: "); - std::cout.flush(); - if(!std::getline(std::cin, pgpName)) + for(int attempt = 0; keepRunning && pgpName.empty() && attempt < MAX_PROMPT_ATTEMPTS; ++attempt) { - RsErr() << "Unable to read the account name from the terminal." << std::endl; - return false; + std::cout << colored(COLOR_GREEN, "Please enter your Username: "); + std::cout.flush(); + if(!std::getline(std::cin, pgpName)) + { + RsErr() << "Unable to read the account name from the terminal." << std::endl; + return CreateAccountResult::Failed; + } + pgpName = trimmed(pgpName); + if (pgpName.empty()) + std::cout << colored(COLOR_RED, "Name cannot be empty!") << std::endl; } + if (!keepRunning) return CreateAccountResult::Cancelled; if (pgpName.empty()) - std::cout << colored(COLOR_RED, "Name cannot be empty!") << std::endl; + { + RsErr() << "No account name given, giving up." << std::endl; + return CreateAccountResult::Failed; + } } - if (!keepRunning) return false; std::string locationName; - while (keepRunning && locationName.empty()) + for(int attempt = 0; keepRunning && locationName.empty() && attempt < MAX_PROMPT_ATTEMPTS; ++attempt) { std::cout << colored(COLOR_GREEN, "Please enter Node/Location Name (e.g. Laptop, Home): "); std::cout.flush(); if(!std::getline(std::cin, locationName)) { RsErr() << "Unable to read the location name from the terminal." << std::endl; - return false; + return CreateAccountResult::Failed; } + locationName = trimmed(locationName); if (locationName.empty()) std::cout << colored(COLOR_RED, "Location name cannot be empty!") << std::endl; } - if (!keepRunning) return false; + if (!keepRunning) return CreateAccountResult::Cancelled; + if (locationName.empty()) + { + RsErr() << "No location name given, giving up." << std::endl; + return CreateAccountResult::Failed; + } std::string pass1, pass2; - while (keepRunning) + bool passphraseAccepted = false; + for(int attempt = 0; keepRunning && attempt < MAX_PROMPT_ATTEMPTS; ++attempt) { - pass1 = RsUtil::rs_getpass(colored(COLOR_GREEN, "Please enter passphrase for new account: ")); + pass1 = RsUtil::rs_getpass(colored(COLOR_GREEN, + reusingProfile ? "Please enter the passphrase of that profile: " + : "Please enter passphrase for new account: ")); + + if(reusingProfile) + { + // Nothing to confirm: the passphrase already exists and a typo is + // caught by the key itself rather than by a second prompt. + if(!pass1.empty()) { passphraseAccepted = true; break; } + std::cout << colored(COLOR_RED, "Passphrase cannot be empty! Please try again.") << std::endl; + continue; + } + pass2 = RsUtil::rs_getpass(colored(COLOR_GREEN, "Please enter the same passphrase again: ")); if (pass1 != pass2) @@ -203,20 +342,28 @@ static bool doTerminalCreateAccount() std::cout << colored(COLOR_RED, "Passphrase cannot be empty! Please try again.") << std::endl; continue; } + passphraseAccepted = true; break; } - if (!keepRunning) return false; + if (!keepRunning) return CreateAccountResult::Cancelled; + if (!passphraseAccepted) + { + RsErr() << "No usable passphrase given, giving up." << std::endl; + return CreateAccountResult::Failed; + } - std::cout << colored(COLOR_YELLOW, "Generating 4096-bit PGP key & SSL certificate (this may take a few seconds)...") << std::endl; + if(reusingProfile) + std::cout << colored(COLOR_YELLOW, "Generating SSL certificate for the new node...") << std::endl; + else + std::cout << colored(COLOR_YELLOW, "Generating 4096-bit PGP key & SSL certificate (this may take a few seconds)...") << std::endl; RsPeerId locationId; - RsPgpId pgpId; std::error_condition err = rsLoginHelper->createLocationV2(locationId, pgpId, locationName, pgpName, pass1); if (err) { RsErr() << colored(COLOR_RED, "Account creation failed: " + err.message()) << std::endl; - return false; + return CreateAccountResult::Failed; } std::cout << std::endl @@ -224,7 +371,7 @@ static bool doTerminalCreateAccount() std::cout << colored(COLOR_GREEN, " Location ID : ") << colored(COLOR_YELLOW, locationId.toStdString()) << std::endl; std::cout << colored(COLOR_GREEN, " PGP ID : ") << colored(COLOR_BLUE, pgpId.toStdString()) << std::endl << std::endl; - return true; + return CreateAccountResult::Created; } #endif @@ -357,7 +504,9 @@ int main(int argc, char* argv[]) { std::string webui_pass2 = "N"; - while(keepRunning) + // Same bound as the account prompts: -W on a service with no terminal + // re-asks a question that can never be answered. + for(int attempt = 0; keepRunning && attempt < MAX_PROMPT_ATTEMPTS; ++attempt) { webui_pass1 = RsUtil::rs_getpass( colored(COLOR_GREEN,"Please register a password for the web interface: ")); webui_pass2 = RsUtil::rs_getpass( colored(COLOR_GREEN,"Please enter the same password again : ")); @@ -365,6 +514,7 @@ int main(int argc, char* argv[]) if(webui_pass1 != webui_pass2) { std::cout << colored(COLOR_RED,"Passwords do not match!") << std::endl; + webui_pass1.clear(); continue; } if(webui_pass1.empty()) @@ -375,6 +525,10 @@ int main(int argc, char* argv[]) break; } + + if(askWebUiPassword && webui_pass1.empty()) + RsErr() << "No web interface password given, the web interface will " + "not be started." << std::endl; } #ifdef RS_SERVICE_TERMINAL_WEBUI_PASSWORD if(askWebUiPassword && !webui_pass1.empty()) @@ -412,8 +566,12 @@ int main(int argc, char* argv[]) if(prefUserString == "create") { - alreadyLoggedIn = doTerminalCreateAccount(); - if (!alreadyLoggedIn) return -1; + switch(doTerminalCreateAccount()) + { + case CreateAccountResult::Created: alreadyLoggedIn = true; break; + case CreateAccountResult::Cancelled: return 0; + case CreateAccountResult::Failed: return -RsInit::ERR_UNKNOWN; + } } else if(prefUserString == "list") { @@ -439,18 +597,27 @@ int main(int argc, char* argv[]) } + // "-U list" is documented as a way to list accounts, so on a + // non-interactive stdin print the list and stop there rather than + // advertising a [c] entry nobody can type. The error is kept for + // the case it actually describes: no account to list. + if(!hasInteractiveStdin()) + { + if(locations.empty()) + { + RsErr() << "No available account, and stdin is not a terminal " + "to create one." << std::endl; + return -RsInit::ERR_NO_AVAILABLE_ACCOUNT; + } + return 0; + } + std::cout << colored(COLOR_GREEN," [c]") << " " << colored(COLOR_YELLOW,"Create new account") << std::endl << std::endl; - if(!hasInteractiveStdin()) - { - RsErr() << "Account selection and creation require an interactive terminal." - << std::endl; - return -RsInit::ERR_NO_AVAILABLE_ACCOUNT; - } - - while(keepRunning) + bool selectionMade = false; + for(int attempt = 0; keepRunning && attempt < MAX_PROMPT_ATTEMPTS; ++attempt) { std::cout << colored(COLOR_GREEN,"Please enter account number or 'c' to create: "); std::cout.flush(); @@ -462,10 +629,16 @@ int main(int argc, char* argv[]) return -RsInit::ERR_NO_AVAILABLE_ACCOUNT; } + inputStr = trimmed(inputStr); + if(inputStr == "c" || inputStr == "C") { - alreadyLoggedIn = doTerminalCreateAccount(); - if(!alreadyLoggedIn) return -1; + switch(doTerminalCreateAccount()) + { + case CreateAccountResult::Created: alreadyLoggedIn = true; break; + case CreateAccountResult::Cancelled: return 0; + case CreateAccountResult::Failed: return -RsInit::ERR_UNKNOWN; + } break; } @@ -475,11 +648,25 @@ int main(int argc, char* argv[]) selection >= 1 && selection <= locations.size()) { prefUserString = locations[selection - 1].mLocationId.toStdString(); + selectionMade = true; break; } std::cout << colored(COLOR_RED,"Invalid selection. Please try again.") << std::endl; } + + // Ctrl-C at a prompt: on glibc signal() installs the handler with + // SA_RESTART, so the blocked read is restarted and keepRunning is + // only seen once the user also presses Enter. Without this, control + // fell through with prefUserString still "list" and the user who + // just cancelled was told their location id was invalid. + if(!keepRunning) return 0; + + if(!alreadyLoggedIn && !selectionMade) + { + RsErr() << "No account selected, giving up." << std::endl; + return -RsInit::ERR_NO_AVAILABLE_ACCOUNT; + } } if(!alreadyLoggedIn) @@ -547,6 +734,7 @@ int main(int argc, char* argv[]) } } #endif // def RS_SERVICE_TERMINAL_LOGIN + rsControl->setShutdownCallback([&](int){keepRunning = false;}); while(keepRunning) From 3a7b465321de0635f4a7bad4fcaa17791c7440e3 Mon Sep 17 00:00:00 2001 From: defnax <9952056+defnax@users.noreply.github.com> Date: Thu, 10 Sep 2026 21:20:54 +0200 Subject: [PATCH 4/5] define explicit enum values for CreateAccountResult --- retroshare-service/src/retroshare-service.cc | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/retroshare-service/src/retroshare-service.cc b/retroshare-service/src/retroshare-service.cc index e5d787d1f..6ded0e5e6 100644 --- a/retroshare-service/src/retroshare-service.cc +++ b/retroshare-service/src/retroshare-service.cc @@ -177,7 +177,7 @@ void signalHandler(int signal) #ifdef RS_SERVICE_TERMINAL_LOGIN -enum class CreateAccountResult { Created, Cancelled, Failed }; +enum class CreateAccountResult { Unknown = 0x00, Created = 0x01, Cancelled = 0x02, Failed = 0x03 }; /** Ask the user for a PGP profile to sign the new node with. * Returns false when the user cancelled. A null pgpId on return means "make a @@ -570,7 +570,9 @@ int main(int argc, char* argv[]) { case CreateAccountResult::Created: alreadyLoggedIn = true; break; case CreateAccountResult::Cancelled: return 0; - case CreateAccountResult::Failed: return -RsInit::ERR_UNKNOWN; + case CreateAccountResult::Failed: + case CreateAccountResult::Unknown: + default: return -RsInit::ERR_UNKNOWN; } } else if(prefUserString == "list") @@ -637,7 +639,9 @@ int main(int argc, char* argv[]) { case CreateAccountResult::Created: alreadyLoggedIn = true; break; case CreateAccountResult::Cancelled: return 0; - case CreateAccountResult::Failed: return -RsInit::ERR_UNKNOWN; + case CreateAccountResult::Failed: + case CreateAccountResult::Unknown: + default: return -RsInit::ERR_UNKNOWN; } break; } From f1db29079aafee6b12a36d9b464d6888963ea501 Mon Sep 17 00:00:00 2001 From: defnax <9952056+defnax@users.noreply.github.com> Date: Thu, 10 Sep 2026 21:38:45 +0200 Subject: [PATCH 5/5] Fix terminal prompts and formatting - Left-pad profile indices with leading zeros to align with node list - Clarify prompts: use "profile name" instead of "Username" and "node to that profile" instead of "of it" - Update terms to use "Node ID" and "Profile ID" - Initialize webui_pass2 to empty string - Report unexpected error in switches for CreateAccountResult::Unknown --- retroshare-service/src/retroshare-service.cc | 32 ++++++++++++-------- 1 file changed, 19 insertions(+), 13 deletions(-) diff --git a/retroshare-service/src/retroshare-service.cc b/retroshare-service/src/retroshare-service.cc index 6ded0e5e6..cb928173e 100644 --- a/retroshare-service/src/retroshare-service.cc +++ b/retroshare-service/src/retroshare-service.cc @@ -197,11 +197,13 @@ static bool askPgpProfile(RsPgpId& pgpId) << colored(COLOR_GREEN, "Existing profiles on this machine:") << std::endl << std::endl; + int profileCountDigits = static_cast( ceil(log(choices.size() + 1)/log(10.0)) ); + for(size_t i = 0; i < choices.size(); ++i) { std::string name, email; RsAccounts::GetPGPLoginDetails(choices[i], name, email); - std::cout << colored(COLOR_GREEN, " [" + RsUtil::NumberToString(i+1) + "]") << " " + std::cout << colored(COLOR_GREEN, " [" + RsUtil::NumberToString(i+1, false, '0', profileCountDigits) + "]") << " " << colored(COLOR_BLUE, choices[i].toStdString()) << ": " << colored(COLOR_PURPLE, name) << std::endl; } @@ -212,7 +214,7 @@ static bool askPgpProfile(RsPgpId& pgpId) "A new profile is a new identity: your existing friends " "will not recognise it,\nand you will have to exchange " "certificates with them again. Reuse a profile above\n" - "to simply add this machine as another node of it.") + "to simply add this machine as another node to that profile.") << std::endl << std::endl; for(int attempt = 0; keepRunning && attempt < MAX_PROMPT_ATTEMPTS; ++attempt) @@ -273,7 +275,7 @@ static CreateAccountResult doTerminalCreateAccount() { for(int attempt = 0; keepRunning && pgpName.empty() && attempt < MAX_PROMPT_ATTEMPTS; ++attempt) { - std::cout << colored(COLOR_GREEN, "Please enter your Username: "); + std::cout << colored(COLOR_GREEN, "Please enter your new profile name: "); std::cout.flush(); if(!std::getline(std::cin, pgpName)) { @@ -353,9 +355,9 @@ static CreateAccountResult doTerminalCreateAccount() } if(reusingProfile) - std::cout << colored(COLOR_YELLOW, "Generating SSL certificate for the new node...") << std::endl; + std::cout << colored(COLOR_YELLOW, "Generating certificate for the new node...") << std::endl; else - std::cout << colored(COLOR_YELLOW, "Generating 4096-bit PGP key & SSL certificate (this may take a few seconds)...") << std::endl; + std::cout << colored(COLOR_YELLOW, "Generating profile key and node certificate (this may take a few seconds)...") << std::endl; RsPeerId locationId; std::error_condition err = rsLoginHelper->createLocationV2(locationId, pgpId, locationName, pgpName, pass1); @@ -368,8 +370,8 @@ static CreateAccountResult doTerminalCreateAccount() std::cout << std::endl << colored(COLOR_GREEN, "Account successfully created and logged in!") << std::endl; - std::cout << colored(COLOR_GREEN, " Location ID : ") << colored(COLOR_YELLOW, locationId.toStdString()) << std::endl; - std::cout << colored(COLOR_GREEN, " PGP ID : ") << colored(COLOR_BLUE, pgpId.toStdString()) << std::endl << std::endl; + std::cout << colored(COLOR_GREEN, " Node ID : ") << colored(COLOR_YELLOW, locationId.toStdString()) << std::endl; + std::cout << colored(COLOR_GREEN, " Profile ID : ") << colored(COLOR_BLUE, pgpId.toStdString()) << std::endl << std::endl; return CreateAccountResult::Created; } @@ -502,7 +504,7 @@ int main(int argc, char* argv[]) std::string webui_pass1; if(askWebUiPassword) { - std::string webui_pass2 = "N"; + std::string webui_pass2 = ""; // Same bound as the account prompts: -W on a service with no terminal // re-asks a question that can never be answered. @@ -570,9 +572,11 @@ int main(int argc, char* argv[]) { case CreateAccountResult::Created: alreadyLoggedIn = true; break; case CreateAccountResult::Cancelled: return 0; - case CreateAccountResult::Failed: + case CreateAccountResult::Failed: return -RsInit::ERR_UNKNOWN; case CreateAccountResult::Unknown: - default: return -RsInit::ERR_UNKNOWN; + default: + RsErr() << "An unexpected error occurred during account creation." << std::endl; + return -RsInit::ERR_UNKNOWN; } } else if(prefUserString == "list") @@ -615,7 +619,7 @@ int main(int argc, char* argv[]) } std::cout << colored(COLOR_GREEN," [c]") << " " - << colored(COLOR_YELLOW,"Create new account") << std::endl + << colored(COLOR_YELLOW,"Create new profile/node") << std::endl << std::endl; bool selectionMade = false; @@ -639,9 +643,11 @@ int main(int argc, char* argv[]) { case CreateAccountResult::Created: alreadyLoggedIn = true; break; case CreateAccountResult::Cancelled: return 0; - case CreateAccountResult::Failed: + case CreateAccountResult::Failed: return -RsInit::ERR_UNKNOWN; case CreateAccountResult::Unknown: - default: return -RsInit::ERR_UNKNOWN; + default: + RsErr() << "An unexpected error occurred during account creation." << std::endl; + return -RsInit::ERR_UNKNOWN; } break; }