diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 139a3e318..1b7f014a1 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -427,7 +427,7 @@ if(KRAZY2_EXECUTABLE) endif() set(owncloudcmd_NAME ${APPLICATION_EXECUTABLE}cmd) -set(OWNCLOUDCMD_SRC owncloudcmd/simplesslerrorhandler.cpp owncloudcmd/owncloudcmd.cpp) +set(OWNCLOUDCMD_SRC owncloudcmd/simplesslerrorhandler.cpp owncloudcmd/owncloudcmd.cpp owncloudcmd/netrcparser.cpp) if(NOT BUILD_LIBRARIES_ONLY) add_executable(${owncloudcmd_NAME} ${OWNCLOUDCMD_SRC}) diff --git a/src/creds/credentialsfactory.cpp b/src/creds/credentialsfactory.cpp index 83f9711c5..f91081d0c 100644 --- a/src/creds/credentialsfactory.cpp +++ b/src/creds/credentialsfactory.cpp @@ -36,7 +36,7 @@ AbstractCredentials* create(const QString& type) // empty string might happen for old version of configuration if (type == "http" || type == "") { - return new HttpCredentials; + return new HttpCredentialsGui; } else if (type == "dummy") { return new DummyCredentials; } else if (type == "shibboleth") { diff --git a/src/creds/httpcredentials.cpp b/src/creds/httpcredentials.cpp index c9881e668..9b91af983 100644 --- a/src/creds/httpcredentials.cpp +++ b/src/creds/httpcredentials.cpp @@ -37,9 +37,6 @@ using namespace QKeychain; namespace Mirall { -namespace -{ - int getauth(const char *prompt, char *buf, size_t len, @@ -74,10 +71,10 @@ int getauth(const char *prompt, return re; } +namespace +{ const char userC[] = "user"; const char authenticationFailedC[] = "owncloud-authentication-failed"; - - } // ns class HttpCredentialsAccessManager : public MirallAccessManager { @@ -299,19 +296,6 @@ void HttpCredentials::slotReadJobDone(QKeychain::Job *job) } } -QString HttpCredentials::queryPassword(bool *ok) -{ - if (ok) { - QString str = QInputDialog::getText(0, tr("Enter Password"), - tr("Please enter %1 password for user '%2':") - .arg(Theme::instance()->appNameGUI(), _user), - QLineEdit::Password, QString(), ok); - return str; - } else { - return QString(); - } -} - void HttpCredentials::invalidateToken(Account *account) { _password = QString(); @@ -380,4 +364,17 @@ void HttpCredentials::slotAuthentication(QNetworkReply* reply, QAuthenticator* a reply->close(); } +QString HttpCredentialsGui::queryPassword(bool *ok) +{ + if (ok) { + QString str = QInputDialog::getText(0, tr("Enter Password"), + tr("Please enter %1 password for user '%2':") + .arg(Theme::instance()->appNameGUI(), _user), + QLineEdit::Password, QString(), ok); + return str; + } else { + return QString(); + } +} + } // ns Mirall diff --git a/src/creds/httpcredentials.h b/src/creds/httpcredentials.h index e90b586e1..6fec0883c 100644 --- a/src/creds/httpcredentials.h +++ b/src/creds/httpcredentials.h @@ -49,7 +49,7 @@ public: void persist(Account *account) Q_DECL_OVERRIDE; QString user() const Q_DECL_OVERRIDE; QString password() const; - QString queryPassword(bool *ok); + virtual QString queryPassword(bool *ok) = 0; void invalidateToken(Account *account) Q_DECL_OVERRIDE; QString fetchUser(Account *account); @@ -58,14 +58,23 @@ private Q_SLOTS: void slotReadJobDone(QKeychain::Job*); void slotWriteJobDone(QKeychain::Job*); -private: +protected: QString _user; QString _password; + +private: bool _ready; bool _fetchJobInProgress; //True if the keychain job is in progress or the input dialog visible bool _readPwdFromDeprecatedPlace; }; +class OWNCLOUDSYNC_EXPORT HttpCredentialsGui : public HttpCredentials { +public: + HttpCredentialsGui() : HttpCredentials() {} + HttpCredentialsGui(const QString& user, const QString& password) : HttpCredentials(user, password) {} + QString queryPassword(bool *ok) Q_DECL_OVERRIDE; +}; + } // ns Mirall #endif diff --git a/src/owncloudcmd/netrcparser.cpp b/src/owncloudcmd/netrcparser.cpp new file mode 100644 index 000000000..7db8d2be4 --- /dev/null +++ b/src/owncloudcmd/netrcparser.cpp @@ -0,0 +1,97 @@ +/* + * Copyright (C) by Daniel Molkentin + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * for more details. + */ + +#include +#include +#include + +#include "netrcparser.h" + +namespace Mirall { + +namespace { +QString defaultKeyword = QLatin1String("default"); +QString machineKeyword = QLatin1String("machine"); +QString loginKeyword = QLatin1String("login"); +QString passwordKeyword = QLatin1String("password"); + +} + +NetrcParser::NetrcParser(const QString &fileName) + : _fileName(fileName) +{ + if (_fileName.isEmpty()) { + _fileName = QDir::homePath()+QLatin1String("/.netrc"); + } +} + +void NetrcParser::tryAddEntryAndClear(QString& machine, LoginPair& pair, bool& isDefault) { + if (isDefault) { + _default = pair; + } else if (!machine.isEmpty() && !pair.first.isEmpty()){ + _entries.insert(machine, pair); + } + pair = qMakePair(QString(), QString()); + machine.clear(); + isDefault = false; +} + +bool NetrcParser::parse() +{ + QFile netrc(_fileName); + if (!netrc.open(QIODevice::ReadOnly)) { + return false; + } + + QTextStream ts(&netrc); + LoginPair pair; + QString machine; + bool isDefault = false; + while (!ts.atEnd()) { + QString next; + ts >> next; + if (next == defaultKeyword) { + tryAddEntryAndClear(machine, pair, isDefault); + isDefault = true; + } + if (next == machineKeyword) { + tryAddEntryAndClear(machine, pair, isDefault); + ts >> machine; + } else if (next == loginKeyword) { + ts >> pair.first; + } else if (next == passwordKeyword) { + ts >> pair.second; + } // ignore unsupported tokens + + } + tryAddEntryAndClear(machine, pair, isDefault); + + if (!_entries.isEmpty() || _default != qMakePair(QString(), QString())) { + return true; + } else { + return false; + } +} + +NetrcParser::LoginPair NetrcParser::find(const QString &machine) +{ + QHash::const_iterator it = _entries.find(machine); + if (it != _entries.end()) { + return *it; + } else { + return _default; + } +} + +} // namespace Mirall diff --git a/src/owncloudcmd/netrcparser.h b/src/owncloudcmd/netrcparser.h new file mode 100644 index 000000000..355333d43 --- /dev/null +++ b/src/owncloudcmd/netrcparser.h @@ -0,0 +1,41 @@ +/* + * Copyright (C) by Daniel Molkentin + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * for more details. + */ + +#ifndef NETRCPARSER_H +#define NETRCPARSER_H + +#include +#include + +namespace Mirall { + +class NetrcParser +{ +public: + typedef QPair LoginPair; + + NetrcParser(const QString &fileName = QString::null); + bool parse(); + LoginPair find(const QString &machine); + +private: + void tryAddEntryAndClear(QString &machine, LoginPair &pair, bool &isDefault); + QHash _entries; + LoginPair _default; + QString _fileName; +}; + +} // namespace Mirall + +#endif // NETRCPARSER_H diff --git a/src/owncloudcmd/owncloudcmd.cpp b/src/owncloudcmd/owncloudcmd.cpp index c95755240..f8bb13084 100644 --- a/src/owncloudcmd/owncloudcmd.cpp +++ b/src/owncloudcmd/owncloudcmd.cpp @@ -31,15 +31,27 @@ #include "owncloudcmd.h" #include "simplesslerrorhandler.h" +#include "netrcparser.h" + +#ifdef Q_OS_WIN32 +#include +#else +#include +#endif + using namespace Mirall; struct CmdOptions { QString source_dir; QString target_url; QString config_directory; + QString user; + QString password; QString proxy; bool silent; bool trustSSL; + bool useNetrc; + bool interactive; QString exclude; }; @@ -47,21 +59,59 @@ struct CmdOptions { // So we have to use a global variable CmdOptions *opts = 0; -int getauth(const char* prompt, char* buf, size_t len, int a, int b, void *userdata) +class EchoDisabler { - Q_UNUSED(a) Q_UNUSED(b) Q_UNUSED(userdata) - - std::cout << "** Authentication required: \n" << prompt << std::endl; - std::string s; - if(opts && opts->trustSSL) { - s = "yes"; - } else { - std::getline(std::cin, s); +public: + EchoDisabler() + { +#ifdef Q_OS_WIN + HANDLE hStdin = GetStdHandle(STD_INPUT_HANDLE); + GetConsoleMode(hStdin, &mode); + SetConsoleMode(hStdin, mode & (~ENABLE_ECHO_INPUT)); +#else + tcgetattr(STDIN_FILENO, &tios); + termios tios_new = tios; + tios_new.c_lflag &= ~ECHO; + tcsetattr(STDIN_FILENO, TCSANOW, &tios_new); +#endif } - strncpy( buf, s.c_str(), len ); - return 0; + + ~EchoDisabler() + { +#ifdef Q_OS_WIN + SetConsoleMode(hStdin, mode); +#else + tcsetattr(STDIN_FILENO, TCSANOW, &tios); +#endif + } +private: +#ifdef Q_OS_WIN + DWORD mode = 0; +#else + termios tios; +#endif +}; + +QString queryPassword(const QString &user) +{ + EchoDisabler disabler; + std::cout << "Password for user " << qPrintable(user) << ": "; + std::string s; + std::getline(std::cin, s); + return QString::fromStdString(s); } +class HttpCredentialsText : public HttpCredentials { +public: + HttpCredentialsText(const QString& user, const QString& password) : HttpCredentials(user, password) {} + QString queryPassword(bool *ok) { + if (ok) { + *ok = true; + } + return ::queryPassword(user()); + } +}; + void help() { std::cout << "owncloudcmd - command line ownCloud client tool." << std::endl; @@ -72,12 +122,15 @@ void help() std::cout << "uses the setting from a configured sync client." << std::endl; std::cout << std::endl; std::cout << "Options:" << std::endl; - std::cout << " --silent Don't be so verbose" << std::endl; - std::cout << " --confdir = configdir: Read config from there." << std::endl; - std::cout << " --httpproxy = proxy: Specify a http proxy to use." << std::endl; + std::cout << " --silent, -s Don't be so verbose" << std::endl; + std::cout << " --httpproxy [proxy] Specify a http proxy to use." << std::endl; std::cout << " Proxy is http://server:port" << std::endl; std::cout << " --trust Trust the SSL certification." << std::endl; std::cout << " --exclude [file] exclude list file" << std::endl; + std::cout << " --user, -u [name] Use [name] as the login name" << std::endl; + std::cout << " --password, -p [pass] Use [pass] as password" << std::endl; + std::cout << " -n Use netrc (5) for login" << std::endl; + std::cout << " --non-interactive Do not block execution with interaction" << std::endl; std::cout << "" << std::endl; exit(1); @@ -97,7 +150,7 @@ void parseOptions( const QStringList& app_args, CmdOptions *options ) if(!options->target_url.endsWith("/")) { options->target_url.append("/"); } - options->target_url.append("remote.php/webdav/"); + options->target_url.append("remote.php/webdav"); } if (options->target_url.startsWith("http")) options->target_url.replace(0, 4, "owncloud"); @@ -114,14 +167,20 @@ void parseOptions( const QStringList& app_args, CmdOptions *options ) while(it.hasNext()) { const QString option = it.next(); - if( option == "--confdir" && !it.peekNext().startsWith("-") ) { - options->config_directory = it.next(); - } else if( option == "--httpproxy" && !it.peekNext().startsWith("-")) { + if( option == "--httpproxy" && !it.peekNext().startsWith("-")) { options->proxy = it.next(); - } else if( option == "--silent") { + } else if( option == "-s" || option == "--silent") { options->silent = true; } else if( option == "--trust") { options->trustSSL = true; + } else if( option == "-n") { + options->useNetrc = true; + } else if( option == "--non-interactive") { + options->interactive = false; + } else if( (option == "-u" || option == "--user") && !it.peekNext().startsWith("-") ) { + options->user = it.next(); + } else if( (option == "-p" || option == "--password") && !it.peekNext().startsWith("-") ) { + options->user = it.next(); } else if( option == "--exclude" && !it.peekNext().startsWith("-") ) { options->exclude = it.next(); } else { @@ -140,26 +199,57 @@ int main(int argc, char **argv) { CmdOptions options; options.silent = false; options.trustSSL = false; + options.useNetrc = false; + options.interactive = true; ClientProxy clientProxy; parseOptions( app.arguments(), &options ); - QUrl url(options.target_url.toUtf8()); - if (url.userName().isEmpty()) { - std::cout << "** Please enter the username:" << std::endl; - std::string s; - std::getline(std::cin, s); - url.setUserName(QString::fromStdString(s)); - } - if (url.password().isEmpty()) { - std::cout << "** Please enter the password:" << std::endl; - std::string s; - std::getline(std::cin, s); - url.setPassword(QString::fromStdString(s)); + QUrl url = QUrl::fromUserInput(options.target_url); + + // Fetch username and password. If empty, try to retrieve + // from URL and strip URL + QString user; + QString password; + + if (options.useNetrc) { + NetrcParser parser; + if (parser.parse()) { + NetrcParser::LoginPair pair = parser.find(url.host()); + user = pair.first; + password = pair.second; + } + } else { + user = options.user; + if (user.isEmpty()) { + user = url.userName(); + } + password = options.password; + if (password.isEmpty()) { + password = url.password(); + } + + if (options.interactive) { + if (user.isEmpty()) { + std::cout << "Please enter user name: "; + std::string s; + std::getline(std::cin, s); + user = QString::fromStdString(s); + } + if (password.isEmpty()) { + password = queryPassword(user); + } + } } - QUrl originalUrl = url; + // ### ensure URL is free of credentials + if (url.userName().isEmpty()) { + url.setUserName(user); + } + if (url.password().isEmpty()) { + url.setPassword(password); + } Account account; @@ -171,16 +261,19 @@ int main(int argc, char **argv) { SimpleSslErrorHandler *sslErrorHandler = new SimpleSslErrorHandler; + HttpCredentials *cred = new HttpCredentialsText(user, password); + account.setUrl(url); - account.setCredentials(new HttpCredentials(url.userName(), url.password())); + account.setCredentials(cred); account.setSslErrorHandler(sslErrorHandler); + AccountManager::instance()->setAccount(&account); restart_sync: CSYNC *_csync_ctx; if( csync_create( &_csync_ctx, options.source_dir.toUtf8(), - originalUrl.toEncoded().constData()) < 0 ) { + url.toEncoded().constData()) < 0 ) { qFatal("Unable to create csync-context!"); return EXIT_FAILURE; } @@ -192,7 +285,7 @@ restart_sync: csync_set_log_level(options.silent ? 1 : 11); opts = &options; - csync_set_auth_callback( _csync_ctx, getauth ); + cred->syncContextPreInit(_csync_ctx); if( csync_init( _csync_ctx ) < 0 ) { qFatal("Could not initialize csync!"); @@ -239,8 +332,9 @@ restart_sync: csync_add_exclude_list(_csync_ctx, options.exclude.toLocal8Bit()); } - OwncloudCmd owncloudCmd; + cred->syncContextPreStart(_csync_ctx); + OwncloudCmd owncloudCmd; SyncJournalDb db(options.source_dir); SyncEngine engine(_csync_ctx, options.source_dir, QUrl(options.target_url).path(), folder, &db); QObject::connect(&engine, SIGNAL(finished()), &app, SLOT(quit())); diff --git a/src/wizard/owncloudhttpcredspage.cpp b/src/wizard/owncloudhttpcredspage.cpp index e150af2d3..d6f7a9cd6 100644 --- a/src/wizard/owncloudhttpcredspage.cpp +++ b/src/wizard/owncloudhttpcredspage.cpp @@ -148,7 +148,7 @@ void OwncloudHttpCredsPage::setErrorString(const QString& err) AbstractCredentials* OwncloudHttpCredsPage::getCredentials() const { - return new HttpCredentials(_ui.leUsername->text(), _ui.lePassword->text()); + return new HttpCredentialsGui(_ui.leUsername->text(), _ui.lePassword->text()); } void OwncloudHttpCredsPage::setConfigExists(bool config) diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 37ac1e83b..ab0c44003 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -26,5 +26,6 @@ if( UNIX AND NOT APPLE ) endif(UNIX AND NOT APPLE) owncloud_add_test(CSyncSqlite "") +owncloud_add_test(NetrcParser ../src/owncloudcmd/netrcparser.cpp) diff --git a/test/testnetrcparser.h b/test/testnetrcparser.h new file mode 100644 index 000000000..4ace249dc --- /dev/null +++ b/test/testnetrcparser.h @@ -0,0 +1,76 @@ +/* + * This software is in the public domain, furnished "as is", without technical + * support, and with no warranty, express or implied, as to its usefulness for + * any purpose. + * */ + +#ifndef MIRALL_INOTIFYWATCHER_H +#define MIRALL_INOTIFYWATCHER_H + +#include + +#include "owncloudcmd/netrcparser.h" + +using namespace Mirall; + +namespace { + +const char testfileC[] = "netrctest"; +const char testfileWithDefaultC[] = "netrctestDefault"; +const char testfileEmptyC[] = "netrctestEmpty"; + +} + +class TestNetrcParser : public QObject +{ + Q_OBJECT + +private slots: + void initTestCase() { + QFile netrc(testfileC); + QVERIFY(netrc.open(QIODevice::WriteOnly)); + netrc.write("machine foo login bar password baz\n"); + netrc.write("machine broken login bar2 dontbelonghere password baz2 extratokens dontcare andanother\n"); + netrc.write("machine\nfunnysplit\tlogin bar3 password baz3\n"); + QFile netrcWithDefault(testfileWithDefaultC); + QVERIFY(netrcWithDefault.open(QIODevice::WriteOnly)); + netrcWithDefault.write("machine foo login bar password baz\n"); + netrcWithDefault.write("default login user password pass\n"); + QFile netrcEmpty(testfileEmptyC); + QVERIFY(netrcEmpty.open(QIODevice::WriteOnly)); + } + + void cleanupTestCase() { + QVERIFY(QFile::remove(testfileC)); + QVERIFY(QFile::remove(testfileWithDefaultC)); + QVERIFY(QFile::remove(testfileEmptyC)); + } + + void testValidNetrc() { + NetrcParser parser(testfileC); + QVERIFY(parser.parse()); + QCOMPARE(parser.find("foo"), qMakePair(QString("bar"), QString("baz"))); + QCOMPARE(parser.find("broken"), qMakePair(QString("bar2"), QString("baz2"))); + QCOMPARE(parser.find("funnysplit"), qMakePair(QString("bar3"), QString("baz3"))); + } + + void testEmptyNetrc() { + NetrcParser parser(testfileEmptyC); + QVERIFY(!parser.parse()); + QCOMPARE(parser.find("foo"), qMakePair(QString(), QString())); + } + + void testValidNetrcWithDefault() { + NetrcParser parser(testfileWithDefaultC); + QVERIFY(parser.parse()); + QCOMPARE(parser.find("foo"), qMakePair(QString("bar"), QString("baz"))); + QCOMPARE(parser.find("dontknow"), qMakePair(QString("user"), QString("pass"))); + } + + void testInvalidNetrc() { + NetrcParser parser("/invalid"); + QVERIFY(!parser.parse()); + } +}; + +#endif