Fix #90, #109: Support 2FA when running as a daemon (#175)

* Fix #90: Support 2FA when running as a daemon

In daemon mode, when PSTATUS_TFA_REQUIRED fires:
- Automatically send an SMS 2FA code via psync_tfa_send_sms()
- Log delivery details to syslog with instructions to use
  `echo 'tfa CODE' | pcloudcc -k`
- Block on a condition variable until the code arrives via RPC

Add a new `tfa <code>` command to the pcloudcc -k control interface.
This sends the code to the daemon over the existing Unix socket RPC
channel (SENDTFA message type).

Bad codes (PSTATUS_BAD_TFA_CODE) are handled: the daemon logs a syslog
warning and waits for a corrected code without re-sending SMS.

Devices are trusted by default (trusted=1 in psync_tfa_set_code) so
repeated 2FA prompts are avoided for the configured trust period.
TFA codes are wiped from memory after use.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* Add auth command: supply password to running daemon

Mirrors the tfa command pattern. When PSTATUS_LOGIN_REQUIRED fires in
daemon mode, read_password() now logs a syslog notice and blocks on a
condition variable rather than calling exit(1).

The new `auth <password>` control command (SENDAUTH RPC type) signals
the condvar and unblocks the daemon. Usage:

  echo 'auth PASSWORD' | pcloudcc -k

The password is wiped from the CLI-side buffer immediately after the
RPC call. putil_wipe() is used for both the tfa and auth transient
strings on the sending side.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* Add status command: show current sync state from CLI

Adds a `status` (alias `st`) command to the interactive CLI and to the
pcloudcc -k control interface. The daemon-side handler calls
psync_get_status(), formats the status name plus download/upload
strings, and writes the result to shared memory. The client reads and
prints it.

Example output:
  Status:   READY
  Download: idle
  Upload:   idle

Also works non-interactively:
  echo 'status' | pcloudcc -k

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Levi Neely <lkn@darkstar.example.net>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Levi Neely 2026-03-02 21:36:33 +01:00 committed by GitHub
parent e3b9f7bfd8
commit d6858a0166
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 189 additions and 12 deletions

View File

@ -41,6 +41,7 @@
#include "pclsync_lib.h"
#include "pclsync/pshm.h"
#include "pclsync/putil.h"
#include "pclsync/pfoldersync.h"
#include "pclsync/pcommands.h"
@ -60,6 +61,9 @@ static char* command_generator(const char* text, int state) {
"sync", "sync ls", "sync add", "sync remove", "sync rm", "sync pause", "sync resume",
"s", "s ls", "s add", "s remove", "s rm", "s pause", "s resume",
"pending", "p",
"status", "st",
"tfa",
"auth",
"finalize", "f",
"quit", "q",
nullptr
@ -104,6 +108,9 @@ void setup_app(CLI::App *app) {
<< " pause: Pause syncing (filesystem remains mounted)" << std::endl
<< " resume: Resume syncing" << std::endl
<< " pending(p): Check for pending transfers" << std::endl
<< " status(st): Show current sync state" << std::endl
<< " tfa <code>: Supply 2FA code to a running daemon" << std::endl
<< " auth <password>: Supply password to a running daemon" << std::endl
<< " finalize(f): Kill daemon and quit" << std::endl
<< " quit(q): Exit this program" << std::endl;
});
@ -151,6 +158,75 @@ void setup_app(CLI::App *app) {
exit(0);
});
// tfa command
auto tfa_cmd = app->add_subcommand("tfa", "Supply 2FA code to a running daemon");
static std::string tfa_code_input;
tfa_cmd->add_option("code", tfa_code_input, "2FA code")->required();
tfa_cmd->callback([] {
char *errm = NULL;
size_t errm_size = 0;
RpcClient *rpc = new RpcClient();
if (int result = rpc->Call(SENDTFA, tfa_code_input.c_str(), &errm, &errm_size) != 0) {
std::cerr << "Failed to send 2FA code: " << (errm ? errm : "no message") << std::endl;
if (errm) { free(errm); }
delete rpc;
return result;
}
delete rpc;
std::cout << "2FA code sent." << std::endl;
if (errm) { free(errm); }
return 0;
});
// auth command
auto auth_cmd = app->add_subcommand("auth", "Supply password to a running daemon");
static std::string auth_pass_input;
auth_cmd->add_option("password", auth_pass_input, "Password")->required();
auth_cmd->callback([] {
char *errm = NULL;
size_t errm_size = 0;
RpcClient *rpc = new RpcClient();
int result = rpc->Call(SENDAUTH, auth_pass_input.c_str(), &errm, &errm_size);
putil_wipe(auth_pass_input.data(), auth_pass_input.size());
auth_pass_input.clear();
if (result != 0) {
std::cerr << "Failed to send auth: " << (errm ? errm : "no message") << std::endl;
if (errm) { free(errm); }
delete rpc;
return result;
}
delete rpc;
std::cout << "Auth sent." << std::endl;
if (errm) { free(errm); }
return 0;
});
// status command
app->add_subcommand("status", "Show current sync state")->alias("st")->callback([] {
char *errm = NULL;
size_t errm_size = 0;
RpcClient *rpc = new RpcClient();
if (int result = rpc->Call(GETSTATUS, "", &errm, &errm_size) != 0) {
std::cerr << "Status failed: " << (errm ? errm : "no message") << std::endl;
if (errm) { free(errm); }
delete rpc;
return result;
}
delete rpc;
char *status_str = nullptr;
if (pshm_read((void**)&status_str, nullptr) && status_str) {
std::cout << status_str << std::endl;
free(status_str);
} else {
std::cerr << "Failed to read status from daemon." << std::endl;
if (errm) { free(errm); }
return -1;
}
if (errm) { free(errm); }
return 0;
});
// pending command
app->add_subcommand("pending", "Check for pending transfers")->alias("p")->callback([] {
char *errm = NULL;

View File

@ -7,5 +7,8 @@ enum command_ids_ {
STOPSYNC,
CHECKPENDING,
SYNCPAUSE,
SYNCRESUME
SYNCRESUME,
SENDTFA,
SENDAUTH,
GETSTATUS
};

View File

@ -34,6 +34,7 @@
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <syslog.h>
#include <termios.h>
#include <time.h>
#include <unistd.h>
@ -56,6 +57,10 @@ namespace cc = console_client;
namespace clib = cc::clibrary;
static pthread_mutex_t mtx = PTHREAD_MUTEX_INITIALIZER;
static pthread_mutex_t tfa_mtx = PTHREAD_MUTEX_INITIALIZER;
static pthread_cond_t tfa_cond = PTHREAD_COND_INITIALIZER;
static pthread_mutex_t auth_mtx = PTHREAD_MUTEX_INITIALIZER;
static pthread_cond_t auth_cond = PTHREAD_COND_INITIALIZER;
static const std::string client_name = "pCloud CC v3.0.0";
@ -161,16 +166,70 @@ clib::pclsync_lib &clib::pclsync_lib::get_lib() {
}
void clib::pclsync_lib::read_password() {
if (daemon_) {
syslog(LOG_NOTICE,
"pcloudcc: password required. "
"Provide with: echo 'auth PASSWORD' | pcloudcc -k");
pthread_mutex_lock(&auth_mtx);
while (password_.empty()) {
pthread_cond_wait(&auth_cond, &auth_mtx);
}
pthread_mutex_unlock(&auth_mtx);
return;
}
read_from_stdin(password_);
}
void clib::pclsync_lib::read_tfa_code()
int clib::pclsync_lib::receive_auth(const char *pass) {
pthread_mutex_lock(&auth_mtx);
get_lib().password_ = std::string(pass);
pthread_cond_signal(&auth_cond);
pthread_mutex_unlock(&auth_mtx);
std::cout << "Auth received." << std::endl;
return 0;
}
void clib::pclsync_lib::read_tfa_code(bool auto_sms)
{
if (daemon_) {
std::cout << "Not able to read 2fa code when started as daemon." << std::endl;
exit(1);
if (auto_sms) {
char *country_code = nullptr;
char *phone_number = nullptr;
int result = psync_tfa_send_sms(&country_code, &phone_number);
if (result == 0) {
if (country_code && phone_number) {
syslog(LOG_NOTICE,
"pcloudcc: 2FA required. SMS sent to +%s %s. "
"Provide code with: echo 'tfa CODE' | pcloudcc -k",
country_code, phone_number);
free(country_code);
free(phone_number);
} else {
syslog(LOG_NOTICE,
"pcloudcc: 2FA required. SMS sent. "
"Provide code with: echo 'tfa CODE' | pcloudcc -k");
}
} else {
syslog(LOG_WARNING,
"pcloudcc: 2FA required but SMS failed (error %d). "
"Provide code with: echo 'tfa CODE' | pcloudcc -k", result);
}
} else {
syslog(LOG_WARNING,
"pcloudcc: 2FA code was rejected. "
"Provide correct code with: echo 'tfa CODE' | pcloudcc -k");
}
// Block until code arrives via RPC (pcloudcc -k)
pthread_mutex_lock(&tfa_mtx);
while (tfa_code_.empty()) {
pthread_cond_wait(&tfa_cond, &tfa_mtx);
}
pthread_mutex_unlock(&tfa_mtx);
return;
}
// Interactive mode
std::cout << "2FA required. Enter code from authenticator app, or type 'sms' to receive code via SMS: ";
std::string input;
std::getline(std::cin, input);
@ -199,6 +258,15 @@ void clib::pclsync_lib::read_tfa_code()
}
}
int clib::pclsync_lib::receive_tfa_code(const char *code) {
pthread_mutex_lock(&tfa_mtx);
get_lib().tfa_code_ = std::string(code);
pthread_cond_signal(&tfa_cond);
pthread_mutex_unlock(&tfa_mtx);
std::cout << "2FA code received." << std::endl;
return 0;
}
void clib::pclsync_lib::read_cryptopass() {
read_from_stdin(crypto_pass_);
}
@ -379,10 +447,15 @@ static void status_change(pstatus_t *status) {
if (clib::pclsync_lib::get_lib().get_tfa_code().empty()) {
clib::pclsync_lib::get_lib().read_tfa_code();
}
psync_tfa_set_code(clib::pclsync_lib::get_lib().get_tfa_code().c_str(),
clib::pclsync_lib::get_lib().get_trusted_device(),
0);
1 /* trusted */, 0);
clib::pclsync_lib::get_lib().wipe_tfa_code();
} else if (status->status == PSTATUS_BAD_TFA_CODE) {
clib::pclsync_lib::get_lib().wipe_tfa_code();
clib::pclsync_lib::get_lib().read_tfa_code(false /* auto_sms */);
psync_tfa_set_code(clib::pclsync_lib::get_lib().get_tfa_code().c_str(),
1 /* trusted */, 0);
clib::pclsync_lib::get_lib().wipe_tfa_code();
} else if (status->status == PSTATUS_BAD_LOGIN_DATA) {
if (!clib::pclsync_lib::get_lib().newuser_) {
clib::pclsync_lib::get_lib().read_password();
@ -467,6 +540,25 @@ int clib::pclsync_lib::check_pending(const char *unused) {
return 0;
}
int clib::pclsync_lib::get_status(const char *unused) {
(void)unused;
pstatus_t st;
psync_get_status(&st);
char buf[512];
snprintf(buf, sizeof(buf),
"Status: %s\n"
"Download: %s\n"
"Upload: %s",
status2string(st.status),
st.downloadstr ? st.downloadstr : "",
st.uploadstr ? st.uploadstr : "");
pshm_write(buf, strlen(buf) + 1);
return 0;
}
int clib::pclsync_lib::finalize(const char *unused) {
(void)unused;
psync_destroy();
@ -602,6 +694,9 @@ int clib::pclsync_lib::init() {
prpc_register(STOPSYNC, &remove_sync_folder);
prpc_register(SYNCPAUSE, &pause_sync);
prpc_register(SYNCRESUME, &resume_sync);
prpc_register(SENDTFA, &receive_tfa_code);
prpc_register(SENDAUTH, &receive_auth);
prpc_register(GETSTATUS, &get_status);
return 0;
}

View File

@ -47,7 +47,7 @@ public:
~pclsync_lib();
pclsync_lib();
bool trusted_device_;
bool trusted_device_ = true;
bool crypto_on_;
bool save_pass_;
bool setup_crypto_;
@ -87,7 +87,7 @@ public:
static pclsync_lib &get_lib();
// Console
void read_tfa_code();
void read_tfa_code(bool auto_sms = true);
void read_password();
void read_cryptopass();
@ -104,6 +104,9 @@ public:
static int remove_sync_folder(const char *path);
static int pause_sync(const char *unused);
static int resume_sync(const char *unused);
static int receive_tfa_code(const char *code);
static int receive_auth(const char *pass);
static int get_status(const char *unused);
int logout();
int unlink();