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