diff --git a/retroshare-service/src/retroshare-service.cc b/retroshare-service/src/retroshare-service.cc index eb4743180..186724a2e 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,36 @@ 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 + return _isatty(_fileno(stdin)) != 0; +#else + 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) { auto fe = dynamic_cast(e.get()); @@ -80,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()) ; @@ -124,6 +175,210 @@ void signalHandler(int signal) } + +#ifdef RS_SERVICE_TERMINAL_LOGIN +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 + * 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; + + 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, false, '0', profileCountDigits) + "]") << " " + << 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 to that profile.") + << 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 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; + if(!reusingProfile) + { + for(int attempt = 0; keepRunning && pgpName.empty() && attempt < MAX_PROMPT_ATTEMPTS; ++attempt) + { + std::cout << colored(COLOR_GREEN, "Please enter your new profile name: "); + 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()) + { + RsErr() << "No account name given, giving up." << std::endl; + return CreateAccountResult::Failed; + } + } + + std::string locationName; + 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 CreateAccountResult::Failed; + } + locationName = trimmed(locationName); + if (locationName.empty()) + std::cout << colored(COLOR_RED, "Location name cannot be empty!") << std::endl; + } + if (!keepRunning) return CreateAccountResult::Cancelled; + if (locationName.empty()) + { + RsErr() << "No location name given, giving up." << std::endl; + return CreateAccountResult::Failed; + } + + std::string pass1, pass2; + bool passphraseAccepted = false; + for(int attempt = 0; keepRunning && attempt < MAX_PROMPT_ATTEMPTS; ++attempt) + { + 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) + { + 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; + } + passphraseAccepted = true; + break; + } + if (!keepRunning) return CreateAccountResult::Cancelled; + if (!passphraseAccepted) + { + RsErr() << "No usable passphrase given, giving up." << std::endl; + return CreateAccountResult::Failed; + } + + if(reusingProfile) + std::cout << colored(COLOR_YELLOW, "Generating certificate for the new node...") << std::endl; + else + 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); + + if (err) + { + RsErr() << colored(COLOR_RED, "Account creation failed: " + err.message()) << std::endl; + return CreateAccountResult::Failed; + } + + std::cout << std::endl + << colored(COLOR_GREEN, "Account successfully created and logged in!") << 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; +} +#endif + + + int main(int argc, char* argv[]) { signal(SIGINT, signalHandler); @@ -209,7 +464,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 @@ -249,9 +504,11 @@ int main(int argc, char* argv[]) std::string webui_pass1; if(askWebUiPassword) { - std::string webui_pass2 = "N"; + std::string webui_pass2 = ""; - 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 : ")); @@ -259,6 +516,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()) @@ -269,6 +527,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()) @@ -351,83 +613,154 @@ 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") + { + switch(doTerminalCreateAccount()) + { + case CreateAccountResult::Created: alreadyLoggedIn = true; break; + case CreateAccountResult::Cancelled: return 0; + case CreateAccountResult::Failed: return -RsInit::ERR_UNKNOWN; + case CreateAccountResult::Unknown: + default: + RsErr() << "An unexpected error occurred during account creation." << std::endl; + return -RsInit::ERR_UNKNOWN; + } + } + 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())) + 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 << colored(COLOR_GREEN,"Please enter account number: "); + std::cout << colored(COLOR_GREEN,"Available accounts:") << std::endl<( ceil(log(locations.size() + 1)/log(10.0)) ); + + for( uint32_t i=0; i(atoi(inputStr.c_str())-1); - if(nacc < locations.size()) + if(!std::getline(std::cin, inputStr)) { - prefUserString = locations[nacc].mLocationId.toStdString(); + RsErr() << "Unable to read an account selection from the terminal." << std::endl; + return -RsInit::ERR_NO_AVAILABLE_ACCOUNT; + } + + inputStr = trimmed(inputStr); + + if(inputStr == "c" || inputStr == "C") + { + switch(doTerminalCreateAccount()) + { + case CreateAccountResult::Created: alreadyLoggedIn = true; break; + case CreateAccountResult::Cancelled: return 0; + case CreateAccountResult::Failed: return -RsInit::ERR_UNKNOWN; + case CreateAccountResult::Unknown: + default: + RsErr() << "An unexpected error occurred during account creation." << std::endl; + return -RsInit::ERR_UNKNOWN; + } break; } - nacc=0; // allow to continue if something goes wrong. + + 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(); + 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; } } - - 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())