1 /*
2  * Copyright (C) by Olivier Goffart <ogoffart@owncloud.com>
3  * Copyright (C) by Klaas Freitag <freitag@owncloud.com>
4  * Copyright (C) by Daniel Heule <daniel.heule@gmail.com>
5  *
6  * This program is free software; you can redistribute it and/or modify
7  * it under the terms of the GNU General Public License as published by
8  * the Free Software Foundation; either version 2 of the License, or
9  * (at your option) any later version.
10  *
11  * This program is distributed in the hope that it will be useful, but
12  * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
13  * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
14  * for more details.
15  */
16 
17 #include <iostream>
18 #include <random>
19 #include <qcoreapplication.h>
20 #include <QStringList>
21 #include <QUrl>
22 #include <QFile>
23 #include <QFileInfo>
24 #include <QJsonDocument>
25 #include <QJsonObject>
26 #include <QNetworkProxy>
27 #include <qdebug.h>
28 
29 #include "account.h"
30 #include "configfile.h" // ONLY ACCESS THE STATIC FUNCTIONS!
31 #ifdef TOKEN_AUTH_ONLY
32 # include "creds/tokencredentials.h"
33 #else
34 # include "creds/httpcredentials.h"
35 #endif
36 #include "simplesslerrorhandler.h"
37 #include "syncengine.h"
38 #include "common/syncjournaldb.h"
39 #include "config.h"
40 #include "csync_exclude.h"
41 
42 
43 #include "cmd.h"
44 
45 #include "theme.h"
46 #include "netrcparser.h"
47 #include "libsync/logger.h"
48 
49 #include "config.h"
50 
51 #ifdef Q_OS_WIN32
52 #include <windows.h>
53 #else
54 #include <termios.h>
55 #include <unistd.h>
56 #endif
57 
58 using namespace OCC;
59 
60 namespace {
61 
62 struct CmdOptions
63 {
64     QString source_dir;
65     QString target_url;
66     QString config_directory;
67     QString user;
68     QString password;
69     QString proxy;
70     bool silent;
71     bool trustSSL;
72     bool useNetrc;
73     bool interactive;
74     bool ignoreHiddenFiles;
75     QString exclude;
76     QString unsyncedfolders;
77     QString davPath;
78     int restartTimes;
79     int downlimit;
80     int uplimit;
81     bool deltasync;
82     qint64 deltasyncminfilesize;
83 };
84 
85 struct SyncCTX
86 {
87     const CmdOptions &options;
88     const QUrl url;
89     const QString folder;
90     const AccountPtr account;
91     const QString user;
92 };
93 
94 
95 /* If the selective sync list is different from before, we need to disable the read from db
96   (The normal client does it in SelectiveSyncDialog::accept*)
97  */
selectiveSyncFixup(OCC::SyncJournalDb * journal,const QStringList & newList)98 void selectiveSyncFixup(OCC::SyncJournalDb *journal, const QStringList &newList)
99 {
100     SqlDatabase db;
101     if (!db.openOrCreateReadWrite(journal->databaseFilePath())) {
102         return;
103     }
104 
105     bool ok;
106 
107     auto oldBlackListSet = journal->getSelectiveSyncList(SyncJournalDb::SelectiveSyncBlackList, &ok).toSet();
108     if (ok) {
109         auto blackListSet = newList.toSet();
110         auto changes = (oldBlackListSet - blackListSet) + (blackListSet - oldBlackListSet);
111         foreach (const auto &it, changes) {
112             journal->schedulePathForRemoteDiscovery(it);
113         }
114 
115         journal->setSelectiveSyncList(SyncJournalDb::SelectiveSyncBlackList, newList);
116     }
117 }
118 
119 
sync(const SyncCTX & ctx,int restartCount)120 int sync(const SyncCTX &ctx, int restartCount)
121 {
122     QStringList selectiveSyncList;
123     if (!ctx.options.unsyncedfolders.isEmpty()) {
124         QFile f(ctx.options.unsyncedfolders);
125         if (!f.open(QFile::ReadOnly)) {
126             qCritical() << "Could not open file containing the list of unsynced folders: " << ctx.options.unsyncedfolders;
127         } else {
128             // filter out empty lines and comments
129             selectiveSyncList = QString::fromUtf8(f.readAll()).split('\n').filter(QRegExp("\\S+")).filter(QRegExp("^[^#]"));
130 
131             for (int i = 0; i < selectiveSyncList.count(); ++i) {
132                 if (!selectiveSyncList.at(i).endsWith(QLatin1Char('/'))) {
133                     selectiveSyncList[i].append(QLatin1Char('/'));
134                 }
135             }
136         }
137     }
138 
139     Cmd cmd;
140     QString dbPath = ctx.options.source_dir + SyncJournalDb::makeDbName(ctx.options.source_dir, ctx.url, ctx.folder, ctx.user);
141     SyncJournalDb db(dbPath);
142 
143     if (!selectiveSyncList.empty()) {
144         selectiveSyncFixup(&db, selectiveSyncList);
145     }
146 
147     SyncOptions opt;
148     opt.fillFromEnvironmentVariables();
149     opt.verifyChunkSizes();
150     SyncEngine engine(ctx.account, ctx.options.source_dir, ctx.folder, &db);
151     engine.setSyncOptions(opt);
152     engine.setIgnoreHiddenFiles(ctx.options.ignoreHiddenFiles);
153     engine.setNetworkLimits(ctx.options.uplimit, ctx.options.downlimit);
154     QObject::connect(&engine, &SyncEngine::finished,
155         [](bool result) { qApp->exit(result ? EXIT_SUCCESS : EXIT_FAILURE); });
156     QObject::connect(&engine, &SyncEngine::transmissionProgress, &cmd, &Cmd::transmissionProgressSlot);
157     QObject::connect(&engine, &SyncEngine::syncError,
158         [](const QString &error) { qWarning() << "Sync error:" << error; });
159 
160 
161     // Exclude lists
162 
163     bool hasUserExcludeFile = !ctx.options.exclude.isEmpty();
164     QString systemExcludeFile = ConfigFile::excludeFileFromSystem();
165 
166     // Always try to load the user-provided exclude list if one is specified
167     if (hasUserExcludeFile) {
168         engine.excludedFiles().addExcludeFilePath(ctx.options.exclude);
169     }
170     // Load the system list if available, or if there's no user-provided list
171     if (!hasUserExcludeFile || QFile::exists(systemExcludeFile)) {
172         engine.excludedFiles().addExcludeFilePath(systemExcludeFile);
173     }
174 
175     if (!engine.excludedFiles().reloadExcludeFiles()) {
176         qFatal("Cannot load system exclude list or list supplied via --exclude");
177         return EXIT_FAILURE;
178     }
179 
180 
181     // Have to be done async, else, an error before exec() does not terminate the event loop.
182     QMetaObject::invokeMethod(&engine, "startSync", Qt::QueuedConnection);
183 
184     const int resultCode = qApp->exec();
185     if (engine.isAnotherSyncNeeded() != NoFollowUpSync) {
186         if (restartCount < ctx.options.restartTimes) {
187             restartCount++;
188             qDebug() << "Restarting Sync, because another sync is needed" << restartCount;
189             return sync(ctx, restartCount);
190         }
191         qWarning() << "Another sync is needed, but not done because restart count is exceeded" << restartCount;
192     }
193     return resultCode;
194 }
195 
196 }
197 
198 
nullMessageHandler(QtMsgType,const QMessageLogContext &,const QString &)199 static void nullMessageHandler(QtMsgType, const QMessageLogContext &, const QString &)
200 {
201 }
202 
203 
204 class EchoDisabler
205 {
206 public:
EchoDisabler()207     EchoDisabler()
208     {
209 #ifdef Q_OS_WIN
210         hStdin = GetStdHandle(STD_INPUT_HANDLE);
211         GetConsoleMode(hStdin, &mode);
212         SetConsoleMode(hStdin, mode & (~ENABLE_ECHO_INPUT));
213 #else
214         tcgetattr(STDIN_FILENO, &tios);
215         termios tios_new = tios;
216         tios_new.c_lflag &= ~ECHO;
217         tcsetattr(STDIN_FILENO, TCSANOW, &tios_new);
218 #endif
219     }
220 
~EchoDisabler()221     ~EchoDisabler()
222     {
223 #ifdef Q_OS_WIN
224         SetConsoleMode(hStdin, mode);
225 #else
226         tcsetattr(STDIN_FILENO, TCSANOW, &tios);
227 #endif
228     }
229 
230 private:
231 #ifdef Q_OS_WIN
232     DWORD mode = 0;
233     HANDLE hStdin;
234 #else
235     termios tios;
236 #endif
237 };
238 
queryPassword(const QString & user)239 QString queryPassword(const QString &user)
240 {
241     EchoDisabler disabler;
242     std::cout << "Password for user " << qPrintable(user) << ": ";
243     std::string s;
244     std::getline(std::cin, s);
245     return QString::fromStdString(s);
246 }
247 
248 #ifndef TOKEN_AUTH_ONLY
249 class HttpCredentialsText : public HttpCredentials
250 {
251 public:
HttpCredentialsText(const QString & user,const QString & password)252     HttpCredentialsText(const QString &user, const QString &password)
253         : HttpCredentials(DetermineAuthTypeJob::AuthType::Basic ,user, password)
254         , // FIXME: not working with client certs yet (qknight)
255         _sslTrusted(false)
256     {
257     }
258 
askFromUser()259     void askFromUser() override
260     {
261         _password = ::queryPassword(user());
262         _ready = true;
263         persist();
264         emit asked();
265     }
266 
setSSLTrusted(bool isTrusted)267     void setSSLTrusted(bool isTrusted)
268     {
269         _sslTrusted = isTrusted;
270     }
271 
sslIsTrusted()272     bool sslIsTrusted() override
273     {
274         return _sslTrusted;
275     }
276 
277 private:
278     bool _sslTrusted;
279 };
280 #endif /* TOKEN_AUTH_ONLY */
281 
help()282 void help()
283 {
284     const char *binaryName = APPLICATION_EXECUTABLE "cmd";
285 
286     std::cout << binaryName << " - command line " APPLICATION_NAME " client tool" << std::endl;
287     std::cout << "" << std::endl;
288     std::cout << "Usage: " << binaryName << " [OPTION] <source_dir> <server_url>" << std::endl;
289     std::cout << "" << std::endl;
290     std::cout << "A proxy can either be set manually using --httpproxy." << std::endl;
291     std::cout << "Otherwise, the setting from a configured sync client will be used." << std::endl;
292     std::cout << std::endl;
293     std::cout << "Options:" << std::endl;
294     std::cout << "  --silent, -s           Don't be so verbose" << std::endl;
295     std::cout << "  --httpproxy [proxy]    Specify a http proxy to use." << std::endl;
296     std::cout << "                         Proxy is http://server:port" << std::endl;
297     std::cout << "  --trust                Trust the SSL certification." << std::endl;
298     std::cout << "  --exclude [file]       Exclude list file" << std::endl;
299     std::cout << "  --unsyncedfolders [file]    File containing the list of unsynced remote folders (selective sync)" << std::endl;
300     std::cout << "  --user, -u [name]      Use [name] as the login name" << std::endl;
301     std::cout << "  --password, -p [pass]  Use [pass] as password" << std::endl;
302     std::cout << "  -n                     Use netrc (5) for login" << std::endl;
303     std::cout << "  --non-interactive      Do not block execution with interaction" << std::endl;
304     std::cout << "  --davpath [path]       Custom themed dav path" << std::endl;
305     std::cout << "  --max-sync-retries [n] Retries maximum n times (default to 3)" << std::endl;
306     std::cout << "  --uplimit [n]          Limit the upload speed of files to n KB/s" << std::endl;
307     std::cout << "  --downlimit [n]        Limit the download speed of files to n KB/s" << std::endl;
308     std::cout << "  -h                     Sync hidden files,do not ignore them" << std::endl;
309     std::cout << "  --version, -v          Display version and exit" << std::endl;
310     std::cout << "  --logdebug             More verbose logging" << std::endl;
311     std::cout << "" << std::endl;
312     exit(0);
313 }
314 
showVersion()315 void showVersion()
316 {
317     std::cout << qUtf8Printable(Theme::instance()->versionSwitchOutput());
318     exit(0);
319 }
320 
parseOptions(const QStringList & app_args,CmdOptions * options)321 void parseOptions(const QStringList &app_args, CmdOptions *options)
322 {
323     QStringList args(app_args);
324 
325     int argCount = args.count();
326 
327     if (argCount < 3) {
328         if (argCount >= 2) {
329             const QString option = args.at(1);
330             if (option == "-v" || option == "--version") {
331                 showVersion();
332             }
333         }
334         help();
335     }
336 
337     options->target_url = args.takeLast();
338 
339     options->source_dir = args.takeLast();
340     if (!options->source_dir.endsWith('/')) {
341         options->source_dir.append('/');
342     }
343     QFileInfo fi(options->source_dir);
344     if (!fi.exists()) {
345         std::cerr << "Source dir '" << qPrintable(options->source_dir) << "' does not exist." << std::endl;
346         exit(1);
347     }
348     options->source_dir = fi.absoluteFilePath();
349 
350     QStringListIterator it(args);
351     // skip file name;
352     if (it.hasNext())
353         it.next();
354 
355     while (it.hasNext()) {
356         const QString option = it.next();
357 
358         if (option == "--httpproxy" && !it.peekNext().startsWith("-")) {
359             options->proxy = it.next();
360         } else if (option == "-s" || option == "--silent") {
361             options->silent = true;
362         } else if (option == "--trust") {
363             options->trustSSL = true;
364         } else if (option == "-n") {
365             options->useNetrc = true;
366         } else if (option == "-h") {
367             options->ignoreHiddenFiles = false;
368         } else if (option == "--non-interactive") {
369             options->interactive = false;
370         } else if ((option == "-u" || option == "--user") && !it.peekNext().startsWith("-")) {
371             options->user = it.next();
372         } else if ((option == "-p" || option == "--password") && !it.peekNext().startsWith("-")) {
373             options->password = it.next();
374         } else if (option == "--exclude" && !it.peekNext().startsWith("-")) {
375             options->exclude = it.next();
376         } else if (option == "--unsyncedfolders" && !it.peekNext().startsWith("-")) {
377             options->unsyncedfolders = it.next();
378         } else if (option == "--davpath" && !it.peekNext().startsWith("-")) {
379             options->davPath = it.next();
380         } else if (option == "--max-sync-retries" && !it.peekNext().startsWith("-")) {
381             options->restartTimes = it.next().toInt();
382         } else if (option == "--uplimit" && !it.peekNext().startsWith("-")) {
383             options->uplimit = it.next().toInt() * 1000;
384         } else if (option == "--downlimit" && !it.peekNext().startsWith("-")) {
385             options->downlimit = it.next().toInt() * 1000;
386         } else if (option == "--logdebug") {
387             Logger::instance()->setLogFile("-");
388             Logger::instance()->setLogDebug(true);
389         } else {
390             help();
391         }
392     }
393 
394     if (options->target_url.isEmpty() || options->source_dir.isEmpty()) {
395         help();
396     }
397 }
398 
main(int argc,char ** argv)399 int main(int argc, char **argv)
400 {
401     QCoreApplication app(argc, argv);
402 
403 #ifdef Q_OS_WIN
404     // Ensure OpenSSL config file is only loaded from app directory
405     QString opensslConf = QCoreApplication::applicationDirPath() + QString("/openssl.cnf");
406     qputenv("OPENSSL_CONF", opensslConf.toLocal8Bit());
407 #endif
408 
409     qsrand(std::random_device()());
410 
411     CmdOptions options;
412     options.silent = false;
413     options.trustSSL = false;
414     options.useNetrc = false;
415     options.interactive = true;
416     options.ignoreHiddenFiles = true;
417     options.restartTimes = 3;
418     options.uplimit = 0;
419     options.downlimit = 0;
420 
421     parseOptions(app.arguments(), &options);
422 
423     if (options.silent) {
424         qInstallMessageHandler(nullMessageHandler);
425     } else {
426         qSetMessagePattern("%{time MM-dd hh:mm:ss:zzz} [ %{type} %{category} ]%{if-debug}\t[ %{function} ]%{endif}:\t%{message}");
427     }
428 
429     AccountPtr account = Account::create();
430 
431     if (!account) {
432         qFatal("Could not initialize account!");
433         return EXIT_FAILURE;
434     }
435     // check if the webDAV path was added to the url and append if not.
436     if (!options.target_url.endsWith("/")) {
437         options.target_url.append("/");
438     }
439 
440     if (!options.davPath.isEmpty()) {
441         account->setDavPath(options.davPath);
442     }
443 
444     if (!options.target_url.contains(account->davPath())) {
445         options.target_url.append(account->davPath());
446     }
447 
448     QUrl url = QUrl::fromUserInput(options.target_url);
449 
450     // Order of retrieval attempt (later attempts override earlier ones):
451     // 1. From URL
452     // 2. From options
453     // 3. From netrc (if enabled)
454     // 4. From prompt (if interactive)
455 
456     QString user = url.userName();
457     QString password = url.password();
458 
459     if (!options.user.isEmpty()) {
460         user = options.user;
461     }
462 
463     if (!options.password.isEmpty()) {
464         password = options.password;
465     }
466 
467     if (options.useNetrc) {
468         NetrcParser parser;
469         if (parser.parse()) {
470             NetrcParser::LoginPair pair = parser.find(url.host());
471             user = pair.first;
472             password = pair.second;
473         }
474     }
475 
476     if (options.interactive) {
477         if (user.isEmpty()) {
478             std::cout << "Please enter user name: ";
479             std::string s;
480             std::getline(std::cin, s);
481             user = QString::fromStdString(s);
482         }
483         if (password.isEmpty()) {
484             password = queryPassword(user);
485         }
486     }
487 
488     // take the unmodified url to pass to csync_create()
489     QByteArray remUrl = options.target_url.toUtf8();
490 
491     // Find the folder and the original owncloud url
492     QStringList splitted = url.path().split("/" + account->davPath());
493     url.setPath(splitted.value(0));
494 
495     url.setScheme(url.scheme().replace("owncloud", "http"));
496 
497     QUrl credentialFreeUrl = url;
498     credentialFreeUrl.setUserName(QString());
499     credentialFreeUrl.setPassword(QString());
500 
501     // Remote folders typically start with a / and don't end with one
502     QString folder = "/" + splitted.value(1);
503     if (folder.endsWith("/") && folder != "/") {
504         folder.chop(1);
505     }
506 
507     if (!options.proxy.isNull()) {
508         QString host;
509         int port = 0;
510         bool ok;
511 
512         QStringList pList = options.proxy.split(':');
513         if (pList.count() == 3) {
514             // http: //192.168.178.23 : 8080
515             //  0            1            2
516             host = pList.at(1);
517             if (host.startsWith("//"))
518                 host.remove(0, 2);
519 
520             port = pList.at(2).toInt(&ok);
521 
522             QNetworkProxyFactory::setUseSystemConfiguration(false);
523             QNetworkProxy::setApplicationProxy(QNetworkProxy(QNetworkProxy::HttpProxy, host, port));
524         } else {
525             qFatal("Could not read httpproxy. The proxy should have the format \"http://hostname:port\".");
526         }
527     }
528 
529     SimpleSslErrorHandler *sslErrorHandler = new SimpleSslErrorHandler;
530 
531 #ifdef TOKEN_AUTH_ONLY
532     TokenCredentials *cred = new TokenCredentials(user, password, "");
533     account->setCredentials(cred);
534 #else
535     HttpCredentialsText *cred = new HttpCredentialsText(user, password);
536     account->setCredentials(cred);
537     if (options.trustSSL) {
538         cred->setSSLTrusted(true);
539     }
540 #endif
541 
542     account->setUrl(url);
543     account->setSslErrorHandler(sslErrorHandler);
544 
545     // Perform a call to get the capabilities.
546     QEventLoop loop;
547     JsonApiJob *job = new JsonApiJob(account, QLatin1String("ocs/v1.php/cloud/capabilities"));
548     QObject::connect(job, &JsonApiJob::jsonReceived, [&](const QJsonDocument &json) {
549         auto caps = json.object().value("ocs").toObject().value("data").toObject().value("capabilities").toObject();
550         qDebug() << "Server capabilities" << caps;
551         account->setCapabilities(caps.toVariantMap());
552         account->setServerVersion(caps["core"].toObject()["status"].toObject()["version"].toString());
553         loop.quit();
554     });
555     job->start();
556     loop.exec();
557 
558     if (job->reply()->error() != QNetworkReply::NoError) {
559         std::cout << "Error connecting to server\n";
560         return EXIT_FAILURE;
561     }
562 
563     job = new JsonApiJob(account, QLatin1String("ocs/v1.php/cloud/user"));
564     QObject::connect(job, &JsonApiJob::jsonReceived, [&](const QJsonDocument &json) {
565         const QJsonObject data = json.object().value("ocs").toObject().value("data").toObject();
566         account->setDavUser(data.value("id").toString());
567         account->setDavDisplayName(data.value("display-name").toString());
568         loop.quit();
569     });
570     job->start();
571     loop.exec();
572     // much lower age than the default since this utility is usually made to be run right after a change in the tests
573     SyncEngine::minimumFileAgeForUpload = std::chrono::milliseconds(0);
574     return sync({ options, credentialFreeUrl, folder, account, user }, 0);
575 }
576