mirror of
https://github.com/RetroShare/RetroShare.git
synced 2026-09-12 19:50:17 +05:00
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 <hexid> 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.
This commit is contained in:
parent
411ef267ad
commit
6622cb9526
@ -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<const RsEvent> e)
|
||||
@ -100,6 +119,18 @@ static void eventHandler(std::shared_ptr<const RsEvent> 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 <hexid> 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<RsPgpId> pgpIds;
|
||||
RsAccounts::GetPGPLogins(pgpIds);
|
||||
|
||||
if(pgpIds.empty()) return true; // nothing to reuse, nothing to ask
|
||||
|
||||
std::vector<RsPgpId> 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)
|
||||
|
||||
Loading…
Reference in New Issue
Block a user