| #define _CRT_SECURE_NO_DEPRECATE 1 |
| |
| #include <time.h> |
| |
| #if defined(POSIX) |
| #include <unistd.h> |
| #endif |
| |
| #include <iomanip> |
| #include <iostream> |
| #include <string> |
| |
| #if HAVE_CONFIG_H |
| #include "config.h" |
| #endif // HAVE_CONFIG_H |
| |
| #include "talk/base/sslconfig.h" // For SSL_USE_* |
| |
| #if SSL_USE_OPENSSL |
| #define USE_SSL_TUNNEL |
| #endif |
| |
| #include "talk/base/basicdefs.h" |
| #include "talk/base/common.h" |
| #include "talk/base/helpers.h" |
| #include "talk/base/logging.h" |
| #include "talk/base/ssladapter.h" |
| #include "talk/base/stringutils.h" |
| #include "talk/base/thread.h" |
| #include "talk/p2p/base/sessionmanager.h" |
| #include "talk/p2p/client/autoportallocator.h" |
| #include "talk/p2p/client/sessionmanagertask.h" |
| #include "talk/xmpp/xmppengine.h" |
| #ifdef USE_SSL_TUNNEL |
| #include "talk/session/tunnel/securetunnelsessionclient.h" |
| #endif |
| #include "talk/session/tunnel/tunnelsessionclient.h" |
| #include "talk/xmpp/xmppclient.h" |
| #include "talk/xmpp/xmppclientsettings.h" |
| #include "talk/xmpp/xmpppump.h" |
| #include "talk/xmpp/xmppsocket.h" |
| |
| #ifndef MAX_PATH |
| #define MAX_PATH 256 |
| #endif |
| |
| #if defined(_MSC_VER) && (_MSC_VER < 1400) |
| // The following are necessary to properly link when compiling STL without |
| // /EHsc, otherwise known as C++ exceptions. |
| void __cdecl std::_Throw(const std::exception &) {} |
| std::_Prhand std::_Raise_handler = 0; |
| #endif |
| |
| enum { |
| MSG_LOGIN_COMPLETE = 1, |
| MSG_LOGIN_FAILED, |
| MSG_DONE, |
| }; |
| |
| buzz::Jid gUserJid; |
| talk_base::InsecureCryptStringImpl gUserPass; |
| std::string gXmppHost = "talk.google.com"; |
| int gXmppPort = 5222; |
| buzz::TlsOptions gXmppUseTls = buzz::TLS_REQUIRED; |
| |
| class DebugLog : public sigslot::has_slots<> { |
| public: |
| DebugLog() : |
| debug_input_buf_(NULL), debug_input_len_(0), debug_input_alloc_(0), |
| debug_output_buf_(NULL), debug_output_len_(0), debug_output_alloc_(0), |
| censor_password_(false) |
| {} |
| char * debug_input_buf_; |
| int debug_input_len_; |
| int debug_input_alloc_; |
| char * debug_output_buf_; |
| int debug_output_len_; |
| int debug_output_alloc_; |
| bool censor_password_; |
| |
| void Input(const char * data, int len) { |
| if (debug_input_len_ + len > debug_input_alloc_) { |
| char * old_buf = debug_input_buf_; |
| debug_input_alloc_ = 4096; |
| while (debug_input_alloc_ < debug_input_len_ + len) { |
| debug_input_alloc_ *= 2; |
| } |
| debug_input_buf_ = new char[debug_input_alloc_]; |
| memcpy(debug_input_buf_, old_buf, debug_input_len_); |
| delete[] old_buf; |
| } |
| memcpy(debug_input_buf_ + debug_input_len_, data, len); |
| debug_input_len_ += len; |
| DebugPrint(debug_input_buf_, &debug_input_len_, false); |
| } |
| |
| void Output(const char * data, int len) { |
| if (debug_output_len_ + len > debug_output_alloc_) { |
| char * old_buf = debug_output_buf_; |
| debug_output_alloc_ = 4096; |
| while (debug_output_alloc_ < debug_output_len_ + len) { |
| debug_output_alloc_ *= 2; |
| } |
| debug_output_buf_ = new char[debug_output_alloc_]; |
| memcpy(debug_output_buf_, old_buf, debug_output_len_); |
| delete[] old_buf; |
| } |
| memcpy(debug_output_buf_ + debug_output_len_, data, len); |
| debug_output_len_ += len; |
| DebugPrint(debug_output_buf_, &debug_output_len_, true); |
| } |
| |
| static bool |
| IsAuthTag(const char * str, size_t len) { |
| if (str[0] == '<' && str[1] == 'a' && |
| str[2] == 'u' && |
| str[3] == 't' && |
| str[4] == 'h' && |
| str[5] <= ' ') { |
| std::string tag(str, len); |
| |
| if (tag.find("mechanism") != std::string::npos) |
| return true; |
| |
| } |
| return false; |
| } |
| |
| void |
| DebugPrint(char * buf, int * plen, bool output) { |
| int len = *plen; |
| if (len > 0) { |
| time_t tim = time(NULL); |
| struct tm * now = localtime(&tim); |
| char *time_string = asctime(now); |
| if (time_string) { |
| size_t time_len = strlen(time_string); |
| if (time_len > 0) { |
| time_string[time_len-1] = 0; // trim off terminating \n |
| } |
| } |
| LOG(INFO) << (output ? "SEND >>>>>>>>>>>>>>>>>>>>>>>>>" : "RECV <<<<<<<<<<<<<<<<<<<<<<<<<") |
| << " : " << time_string; |
| |
| bool indent; |
| int start = 0, nest = 3; |
| for (int i = 0; i < len; i += 1) { |
| if (buf[i] == '>') { |
| if ((i > 0) && (buf[i-1] == '/')) { |
| indent = false; |
| } else if ((start + 1 < len) && (buf[start + 1] == '/')) { |
| indent = false; |
| nest -= 2; |
| } else { |
| indent = true; |
| } |
| |
| // Output a tag |
| LOG(INFO) << std::setw(nest) << " " << std::string(buf + start, i + 1 - start); |
| |
| if (indent) |
| nest += 2; |
| |
| // Note if it's a PLAIN auth tag |
| if (IsAuthTag(buf + start, i + 1 - start)) { |
| censor_password_ = true; |
| } |
| |
| // incr |
| start = i + 1; |
| } |
| |
| if (buf[i] == '<' && start < i) { |
| if (censor_password_) { |
| LOG(INFO) << std::setw(nest) << " " << "## TEXT REMOVED ##"; |
| censor_password_ = false; |
| } |
| else { |
| LOG(INFO) << std::setw(nest) << " " << std::string(buf + start, i - start); |
| } |
| start = i; |
| } |
| } |
| len = len - start; |
| memcpy(buf, buf + start, len); |
| *plen = len; |
| } |
| } |
| |
| }; |
| |
| static DebugLog debug_log_; |
| |
| // Prints out a usage message then exits. |
| void Usage() { |
| std::cerr << "Usage:" << std::endl; |
| std::cerr << " pcp [options] <my_jid> (server mode)" << std::endl; |
| std::cerr << " pcp [options] <my_jid> <src_file> <dst_full_jid>:<dst_file> (client sending)" << std::endl; |
| std::cerr << " pcp [options] <my_jid> <src_full_jid>:<src_file> <dst_file> (client rcv'ing)" << std::endl; |
| std::cerr << " --verbose" << std::endl; |
| std::cerr << " --xmpp-host=<host>" << std::endl; |
| std::cerr << " --xmpp-port=<port>" << std::endl; |
| std::cerr << " --xmpp-use-tls=(true|false)" << std::endl; |
| exit(1); |
| } |
| |
| // Prints out an error message, a usage message, then exits. |
| void Error(const std::string& msg) { |
| std::cerr << "error: " << msg << std::endl; |
| std::cerr << std::endl; |
| Usage(); |
| } |
| |
| void FatalError(const std::string& msg) { |
| std::cerr << "error: " << msg << std::endl; |
| std::cerr << std::endl; |
| exit(1); |
| } |
| |
| // Determines whether the given string is an option. If so, the name and |
| // value are appended to the given strings. |
| bool ParseArg(const char* arg, std::string* name, std::string* value) { |
| if (strncmp(arg, "--", 2) != 0) |
| return false; |
| |
| const char* eq = strchr(arg + 2, '='); |
| if (eq) { |
| if (name) |
| name->append(arg + 2, eq); |
| if (value) |
| value->append(eq + 1, arg + strlen(arg)); |
| } else { |
| if (name) |
| name->append(arg + 2, arg + strlen(arg)); |
| if (value) |
| value->clear(); |
| } |
| |
| return true; |
| } |
| |
| int ParseIntArg(const std::string& name, const std::string& value) { |
| char* end; |
| long val = strtol(value.c_str(), &end, 10); |
| if (*end != '\0') |
| Error(std::string("value of option ") + name + " must be an integer"); |
| return static_cast<int>(val); |
| } |
| |
| #ifdef WIN32 |
| #pragma warning(push) |
| // disable "unreachable code" warning b/c it varies between dbg and opt |
| #pragma warning(disable: 4702) |
| #endif |
| bool ParseBoolArg(const std::string& name, const std::string& value) { |
| if (value == "true") |
| return true; |
| else if (value == "false") |
| return false; |
| else { |
| Error(std::string("value of option ") + name + " must be true or false"); |
| return false; |
| } |
| } |
| #ifdef WIN32 |
| #pragma warning(pop) |
| #endif |
| |
| void ParseFileArg(const char* arg, buzz::Jid* jid, std::string* file) { |
| const char* sep = strchr(arg, ':'); |
| if (!sep) { |
| *file = arg; |
| } else { |
| buzz::Jid jid_arg(std::string(arg, sep-arg)); |
| if (jid_arg.IsBare()) |
| Error("A full JID is required for the source or destination arguments."); |
| *jid = jid_arg; |
| *file = std::string(sep+1); |
| } |
| } |
| |
| |
| void SetConsoleEcho(bool on) { |
| #ifdef WIN32 |
| HANDLE hIn = GetStdHandle(STD_INPUT_HANDLE); |
| if ((hIn == INVALID_HANDLE_VALUE) || (hIn == NULL)) |
| return; |
| |
| DWORD mode; |
| if (!GetConsoleMode(hIn, &mode)) |
| return; |
| |
| if (on) { |
| mode = mode | ENABLE_ECHO_INPUT; |
| } else { |
| mode = mode & ~ENABLE_ECHO_INPUT; |
| } |
| |
| SetConsoleMode(hIn, mode); |
| #else |
| int re; |
| if (on) |
| re = system("stty echo"); |
| else |
| re = system("stty -echo"); |
| if (-1 == re) |
| return; |
| #endif |
| } |
| |
| // Fills in a settings object with the values from the arguments. |
| buzz::XmppClientSettings LoginSettings() { |
| buzz::XmppClientSettings xcs; |
| xcs.set_user(gUserJid.node()); |
| xcs.set_host(gUserJid.domain()); |
| xcs.set_resource("pcp"); |
| xcs.set_pass(talk_base::CryptString(gUserPass)); |
| talk_base::SocketAddress server(gXmppHost, gXmppPort); |
| xcs.set_server(server); |
| xcs.set_use_tls(gXmppUseTls); |
| return xcs; |
| } |
| |
| // Runs the current thread until a message with the given ID is seen. |
| uint32 Loop(const std::vector<uint32>& ids) { |
| talk_base::Message msg; |
| while (talk_base::Thread::Current()->Get(&msg)) { |
| if (msg.phandler == NULL) { |
| if (std::find(ids.begin(), ids.end(), msg.message_id) != ids.end()) |
| return msg.message_id; |
| std::cout << "orphaned message: " << msg.message_id; |
| continue; |
| } |
| talk_base::Thread::Current()->Dispatch(&msg); |
| } |
| return 0; |
| } |
| |
| #ifdef WIN32 |
| #pragma warning(disable:4355) |
| #endif |
| |
| class CustomXmppPump : public buzz::XmppPumpNotify, public buzz::XmppPump { |
| public: |
| CustomXmppPump() : XmppPump(this), server_(false) { } |
| |
| void Serve(cricket::TunnelSessionClient* client) { |
| client->SignalIncomingTunnel.connect(this, |
| &CustomXmppPump::OnIncomingTunnel); |
| server_ = true; |
| } |
| |
| void OnStateChange(buzz::XmppEngine::State state) { |
| switch (state) { |
| case buzz::XmppEngine::STATE_START: |
| std::cout << "connecting..." << std::endl; |
| break; |
| case buzz::XmppEngine::STATE_OPENING: |
| std::cout << "logging in..." << std::endl; |
| break; |
| case buzz::XmppEngine::STATE_OPEN: |
| std::cout << "logged in..." << std::endl; |
| talk_base::Thread::Current()->Post(NULL, MSG_LOGIN_COMPLETE); |
| break; |
| case buzz::XmppEngine::STATE_CLOSED: |
| std::cout << "logged out..." << std::endl; |
| talk_base::Thread::Current()->Post(NULL, MSG_LOGIN_FAILED); |
| break; |
| } |
| } |
| |
| void OnIncomingTunnel(cricket::TunnelSessionClient* client, buzz::Jid jid, |
| std::string description, cricket::Session* session) { |
| std::cout << "IncomingTunnel from " << jid.Str() |
| << ": " << description << std::endl; |
| if (!server_ || file_) { |
| client->DeclineTunnel(session); |
| return; |
| } |
| std::string filename; |
| bool send; |
| if (strncmp(description.c_str(), "send:", 5) == 0) { |
| send = true; |
| } else if (strncmp(description.c_str(), "recv:", 5) == 0) { |
| send = false; |
| } else { |
| client->DeclineTunnel(session); |
| return; |
| } |
| filename = description.substr(5); |
| talk_base::StreamInterface* stream = client->AcceptTunnel(session); |
| if (!ProcessStream(stream, filename, send)) |
| talk_base::Thread::Current()->Post(NULL, MSG_DONE); |
| |
| // TODO: There is a potential memory leak, however, since the PCP |
| // app doesn't work right now, I can't verify the fix actually works, so |
| // comment out the following line until we fix the PCP app. |
| |
| // delete stream; |
| } |
| |
| bool ProcessStream(talk_base::StreamInterface* stream, |
| const std::string& filename, bool send) { |
| ASSERT(file_); |
| sending_ = send; |
| file_.reset(new talk_base::FileStream); |
| buffer_len_ = 0; |
| int err; |
| if (!file_->Open(filename.c_str(), sending_ ? "rb" : "wb", &err)) { |
| std::cerr << "Error opening <" << filename << ">: " |
| << std::strerror(err) << std::endl; |
| return false; |
| } |
| stream->SignalEvent.connect(this, &CustomXmppPump::OnStreamEvent); |
| if (stream->GetState() == talk_base::SS_CLOSED) { |
| std::cerr << "Failed to establish P2P tunnel" << std::endl; |
| return false; |
| } |
| if (stream->GetState() == talk_base::SS_OPEN) { |
| OnStreamEvent(stream, |
| talk_base::SE_OPEN | talk_base::SE_READ | talk_base::SE_WRITE, 0); |
| } |
| return true; |
| } |
| |
| void OnStreamEvent(talk_base::StreamInterface* stream, int events, |
| int error) { |
| if (events & talk_base::SE_CLOSE) { |
| if (error == 0) { |
| std::cout << "Tunnel closed normally" << std::endl; |
| } else { |
| std::cout << "Tunnel closed with error: " << error << std::endl; |
| } |
| Cleanup(stream); |
| return; |
| } |
| if (events & talk_base::SE_OPEN) { |
| std::cout << "Tunnel connected" << std::endl; |
| } |
| talk_base::StreamResult result; |
| size_t count; |
| if (sending_ && (events & talk_base::SE_WRITE)) { |
| LOG(LS_VERBOSE) << "Tunnel SE_WRITE"; |
| while (true) { |
| size_t write_pos = 0; |
| while (write_pos < buffer_len_) { |
| result = stream->Write(buffer_ + write_pos, buffer_len_ - write_pos, |
| &count, &error); |
| if (result == talk_base::SR_SUCCESS) { |
| write_pos += count; |
| continue; |
| } |
| if (result == talk_base::SR_BLOCK) { |
| buffer_len_ -= write_pos; |
| memmove(buffer_, buffer_ + write_pos, buffer_len_); |
| LOG(LS_VERBOSE) << "Tunnel write block"; |
| return; |
| } |
| if (result == talk_base::SR_EOS) { |
| std::cout << "Tunnel closed unexpectedly on write" << std::endl; |
| } else { |
| std::cout << "Tunnel write error: " << error << std::endl; |
| } |
| Cleanup(stream); |
| return; |
| } |
| buffer_len_ = 0; |
| while (buffer_len_ < sizeof(buffer_)) { |
| result = file_->Read(buffer_ + buffer_len_, |
| sizeof(buffer_) - buffer_len_, |
| &count, &error); |
| if (result == talk_base::SR_SUCCESS) { |
| buffer_len_ += count; |
| continue; |
| } |
| if (result == talk_base::SR_EOS) { |
| if (buffer_len_ > 0) |
| break; |
| std::cout << "End of file" << std::endl; |
| // A hack until we have friendly shutdown |
| Cleanup(stream, true); |
| return; |
| } else if (result == talk_base::SR_BLOCK) { |
| std::cout << "File blocked unexpectedly on read" << std::endl; |
| } else { |
| std::cout << "File read error: " << error << std::endl; |
| } |
| Cleanup(stream); |
| return; |
| } |
| } |
| } |
| if (!sending_ && (events & talk_base::SE_READ)) { |
| LOG(LS_VERBOSE) << "Tunnel SE_READ"; |
| while (true) { |
| buffer_len_ = 0; |
| while (buffer_len_ < sizeof(buffer_)) { |
| result = stream->Read(buffer_ + buffer_len_, |
| sizeof(buffer_) - buffer_len_, |
| &count, &error); |
| if (result == talk_base::SR_SUCCESS) { |
| buffer_len_ += count; |
| continue; |
| } |
| if (result == talk_base::SR_BLOCK) { |
| if (buffer_len_ > 0) |
| break; |
| LOG(LS_VERBOSE) << "Tunnel read block"; |
| return; |
| } |
| if (result == talk_base::SR_EOS) { |
| std::cout << "Tunnel closed unexpectedly on read" << std::endl; |
| } else { |
| std::cout << "Tunnel read error: " << error << std::endl; |
| } |
| Cleanup(stream); |
| return; |
| } |
| size_t write_pos = 0; |
| while (write_pos < buffer_len_) { |
| result = file_->Write(buffer_ + write_pos, buffer_len_ - write_pos, |
| &count, &error); |
| if (result == talk_base::SR_SUCCESS) { |
| write_pos += count; |
| continue; |
| } |
| if (result == talk_base::SR_EOS) { |
| std::cout << "File closed unexpectedly on write" << std::endl; |
| } else if (result == talk_base::SR_BLOCK) { |
| std::cout << "File blocked unexpectedly on write" << std::endl; |
| } else { |
| std::cout << "File write error: " << error << std::endl; |
| } |
| Cleanup(stream); |
| return; |
| } |
| } |
| } |
| } |
| |
| void Cleanup(talk_base::StreamInterface* stream, bool delay = false) { |
| LOG(LS_VERBOSE) << "Closing"; |
| stream->Close(); |
| file_.reset(); |
| if (!server_) { |
| if (delay) |
| talk_base::Thread::Current()->PostDelayed(2000, NULL, MSG_DONE); |
| else |
| talk_base::Thread::Current()->Post(NULL, MSG_DONE); |
| } |
| } |
| |
| private: |
| bool server_, sending_; |
| talk_base::scoped_ptr<talk_base::FileStream> file_; |
| char buffer_[1024 * 64]; |
| size_t buffer_len_; |
| }; |
| |
| int main(int argc, char **argv) { |
| talk_base::LogMessage::LogThreads(); |
| talk_base::LogMessage::LogTimestamps(); |
| |
| // TODO: Default the username to the current users's name. |
| |
| // Parse the arguments. |
| |
| int index = 1; |
| while (index < argc) { |
| std::string name, value; |
| if (!ParseArg(argv[index], &name, &value)) |
| break; |
| |
| if (name == "help") { |
| Usage(); |
| } else if (name == "verbose") { |
| talk_base::LogMessage::LogToDebug(talk_base::LS_VERBOSE); |
| } else if (name == "xmpp-host") { |
| gXmppHost = value; |
| } else if (name == "xmpp-port") { |
| gXmppPort = ParseIntArg(name, value); |
| } else if (name == "xmpp-use-tls") { |
| gXmppUseTls = ParseBoolArg(name, value)? |
| buzz::TLS_REQUIRED : buzz::TLS_DISABLED; |
| } else { |
| Error(std::string("unknown option: ") + name); |
| } |
| |
| index += 1; |
| } |
| |
| if (index >= argc) |
| Error("bad arguments"); |
| gUserJid = buzz::Jid(argv[index++]); |
| if (!gUserJid.IsValid()) |
| Error("bad arguments"); |
| |
| char path[MAX_PATH]; |
| #if WIN32 |
| GetCurrentDirectoryA(MAX_PATH, path); |
| #else |
| if (NULL == getcwd(path, MAX_PATH)) |
| Error("Unable to get current path"); |
| #endif |
| |
| std::cout << "Directory: " << std::string(path) << std::endl; |
| |
| buzz::Jid gSrcJid; |
| buzz::Jid gDstJid; |
| std::string gSrcFile; |
| std::string gDstFile; |
| |
| bool as_server = true; |
| if (index + 2 == argc) { |
| ParseFileArg(argv[index], &gSrcJid, &gSrcFile); |
| ParseFileArg(argv[index+1], &gDstJid, &gDstFile); |
| if(gSrcJid.Str().empty() == gDstJid.Str().empty()) |
| Error("Exactly one of source JID or destination JID must be empty."); |
| as_server = false; |
| } else if (index != argc) { |
| Error("bad arguments"); |
| } |
| |
| std::cout << "Password: "; |
| SetConsoleEcho(false); |
| std::cin >> gUserPass.password(); |
| SetConsoleEcho(true); |
| std::cout << std::endl; |
| |
| talk_base::InitializeSSL(); |
| // Log in. |
| CustomXmppPump pump; |
| pump.client()->SignalLogInput.connect(&debug_log_, &DebugLog::Input); |
| pump.client()->SignalLogOutput.connect(&debug_log_, &DebugLog::Output); |
| pump.DoLogin(LoginSettings(), new buzz::XmppSocket(gXmppUseTls), 0); |
| //new XmppAuth()); |
| |
| // Wait until login succeeds. |
| std::vector<uint32> ids; |
| ids.push_back(MSG_LOGIN_COMPLETE); |
| ids.push_back(MSG_LOGIN_FAILED); |
| if (MSG_LOGIN_FAILED == Loop(ids)) |
| FatalError("Failed to connect"); |
| |
| { |
| talk_base::scoped_ptr<buzz::XmlElement> presence( |
| new buzz::XmlElement(buzz::QN_PRESENCE)); |
| presence->AddElement(new buzz::XmlElement(buzz::QN_PRIORITY)); |
| presence->AddText("-1", 1); |
| pump.SendStanza(presence.get()); |
| } |
| |
| std::string user_jid_str = pump.client()->jid().Str(); |
| std::cout << "Logged in as " << user_jid_str << std::endl; |
| |
| // Prepare the random number generator. |
| talk_base::InitRandom(user_jid_str.c_str(), user_jid_str.size()); |
| |
| // Create the P2P session manager. |
| talk_base::BasicNetworkManager network_manager; |
| AutoPortAllocator allocator(&network_manager, "pcp_agent"); |
| allocator.SetXmppClient(pump.client()); |
| cricket::SessionManager session_manager(&allocator); |
| #ifdef USE_SSL_TUNNEL |
| cricket::SecureTunnelSessionClient session_client(pump.client()->jid(), |
| &session_manager); |
| if (!session_client.GenerateIdentity()) |
| FatalError("Failed to generate SSL identity"); |
| #else // !USE_SSL_TUNNEL |
| cricket::TunnelSessionClient session_client(pump.client()->jid(), |
| &session_manager); |
| #endif // USE_SSL_TUNNEL |
| cricket::SessionManagerTask *receiver = |
| new cricket::SessionManagerTask(pump.client(), &session_manager); |
| receiver->EnableOutgoingMessages(); |
| receiver->Start(); |
| |
| bool success = true; |
| |
| // Establish the appropriate connection. |
| if (as_server) { |
| pump.Serve(&session_client); |
| } else { |
| talk_base::StreamInterface* stream = NULL; |
| std::string filename; |
| bool sending; |
| if (gSrcJid.Str().empty()) { |
| std::string message("recv:"); |
| message.append(gDstFile); |
| stream = session_client.CreateTunnel(gDstJid, message); |
| filename = gSrcFile; |
| sending = true; |
| } else { |
| std::string message("send:"); |
| message.append(gSrcFile); |
| stream = session_client.CreateTunnel(gSrcJid, message); |
| filename = gDstFile; |
| sending = false; |
| } |
| success = pump.ProcessStream(stream, filename, sending); |
| } |
| |
| if (success) { |
| // Wait until the copy is done. |
| ids.clear(); |
| ids.push_back(MSG_DONE); |
| ids.push_back(MSG_LOGIN_FAILED); |
| Loop(ids); |
| } |
| |
| // Log out. |
| pump.DoDisconnect(); |
| |
| return 0; |
| } |