Merge pull request #3290 from defnax/account-creation-service

⚙️Added account creation for retroshare service - v2
This commit is contained in:
csoler 2026-09-11 20:09:27 +02:00 committed by GitHub
commit 360da46e96
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

View File

@ -24,6 +24,15 @@
#include <csignal>
#include <iomanip>
#include <atomic>
#include <cstdlib>
#include <cstdio>
#ifdef WINDOWS_SYS
#include <windows.h>
#include <io.h>
#else
#include <unistd.h>
#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<const RsEvent> e)
{
auto fe = dynamic_cast<const RsSystemEvent*>(e.get());
@ -80,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()) ;
@ -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<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;
int profileCountDigits = static_cast<int>( 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<RsLoginHelper::Location> 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<<std::endl;
int accountCountDigits = static_cast<int>( ceil(log(locations.size())/log(10.0)) );
for( uint32_t i=0; i<locations.size(); ++i )
std::cout << colored(COLOR_GREEN," [" + RsUtil::NumberToString(i+1,false,'0',accountCountDigits)+"]") << " "
<< colored(COLOR_YELLOW,locations[i].mLocationId.toStdString())<< " "
<< colored(COLOR_BLUE,"(" + locations[i].mPgpId.toStdString()+ "): ")
<< colored(COLOR_PURPLE,locations[i].mPgpName + " (" + locations[i].mLocationName + ")" )
<< std::endl;
std::cout << std::endl;
uint32_t nacc = 0;
while(keepRunning && (nacc < 1 || nacc >= 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<<std::endl;
int accountCountDigits = static_cast<int>( ceil(log(locations.size() + 1)/log(10.0)) );
for( uint32_t i=0; i<locations.size(); ++i )
std::cout << colored(COLOR_GREEN," [" + RsUtil::NumberToString(i+1,false,'0',accountCountDigits)+"]") << " "
<< colored(COLOR_YELLOW,locations[i].mLocationId.toStdString())<< " "
<< colored(COLOR_BLUE,"(" + locations[i].mPgpId.toStdString()+ "): ")
<< colored(COLOR_PURPLE,locations[i].mPgpName + " (" + locations[i].mLocationName + ")" )
<< std::endl;
}
// "-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 profile/node") << std::endl
<< std::endl;
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();
std::string inputStr;
std::getline(std::cin, inputStr);
nacc = static_cast<uint32_t>(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())