#include "Client.h" #include <QDebug> #include <QJsonDocument> #include <QNetworkAccessManager> #include <QNetworkRequest> #include <QNetworkReply> #include <QtWebSockets/QtWebSockets> #include <assert.h> #include "../hws.h" namespace hws { //! \{ typedef QSharedPointer<QNetworkRequest> QNetworkRequestPtr; typedef QSharedPointer<QByteArray> QByteArrayPtr; // The URL passed in by a user should probably just indicate a server. // We typically place the API under this prefix path. QString DEFAULT_HWS_PREFIX_PATH = "/hws"; //! \{ class Client::Impl : public QObject { Q_OBJECT public: //! \{ Impl(Client *parent) : mClient(parent), mManager(parent), mUrl("http://example.com"), mSession(NULL), mHWSPrefixPath(DEFAULT_HWS_PREFIX_PATH), mConfig(), mIngoreSslErrors(false) { } QNetworkRequestPtr createRequest() { QNetworkRequestPtr request = QNetworkRequestPtr::create(); request->setRawHeader("User-Agent", (QString("helix_web_services_client %1.%2.%3") .arg(HELIX_WEB_SERVICES_MAJOR_VERSION) .arg(HELIX_WEB_SERVICES_MINOR_VERSION) .arg(HELIX_WEB_SERVICES_PATCH_VERSION)).toUtf8()); request->setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); request->setRawHeader("Accept", "application/json"); request->setRawHeader("Accept-Encoding", "gzip, deflate"); foreach(QString key, mConfig.keys()) { QString fullKey = QString("X-Perforce-Helix-Web-Services-%1").arg(key); request->setRawHeader(fullKey.toUtf8(), mConfig[key].toUtf8()); } return request; } // Configures the url on the path with the indicated final path part. // The path should not be encoded. // // Note: will not set the query parameter. void setRequestUrlWithPath(QNetworkRequestPtr request, QString path, const QSharedPointer<QStringHash> params = QSharedPointer<QStringHash>()) { QUrl url(mUrl); QString fullPath(mHWSPrefixPath); fullPath += path; url.setPath(fullPath); if (params) { QUrlQuery urlQuery(url); foreach (QString key, params->keys()) { QString value = (*params)[key]; urlQuery.addQueryItem(key, value); } url.setQuery(urlQuery); } qDebug() << "setting request url " << url; request->setUrl(url); } void setBasicAuthFromSession(QNetworkRequestPtr request); QByteArray createSessionPostBody(const QString & user, const QString & password) { QJsonObject obj; obj["user"] = user; obj["password"] = password; QJsonDocument doc(obj); return doc.toJson(QJsonDocument::Compact); } void logInFinished(QNetworkReply *reply, const QString & user); RequestErrorPtr fromNetworkError(QNetworkReply::NetworkError code); RequestErrorPtr fromSslErrors(const QList<QSslError> & errors); void setSession(const Session & session); void executeMethodFinished(QNetworkReply * reply, const QString & method, const QString & path); // !\} public slots: //! \{ void validateSessionFinished(); void validateSessionError(QNetworkReply::NetworkError code); void validateSessionSslErrors(const QList<QSslError> & errors); void handleLogInError(QNetworkReply::NetworkError code); void handleLogInSslErrors(const QList<QSslError> & errors); void executeMethodError(QNetworkReply::NetworkError code); void executeMethodSslErrors(const QList<QSslError> & errors); //! \} // Member state public: //! \{ Client *mClient; QNetworkAccessManager mManager; QUrl mUrl; SessionPtr mSession; QString mHWSPrefixPath; friend class LogInFinished; QStringHash mConfig; bool mIngoreSslErrors; //! \} }; //! \} //-------------------------------------------------------------------------- // Client::LogInFinished //-------------------------------------------------------------------------- // This is really a relatively simple object to bind the user and network // reply to a login callback on the main object. class Client::LogInFinished : public QObject { Q_OBJECT public: //! \{ LogInFinished(QObject *parent, Client::Impl & impl, QNetworkReply *reply, const QString & user) : QObject(parent), mImpl(impl), mReply(reply), mUser(user) { }; //! \} public slots: //! \{ void finished() { mImpl.logInFinished(mReply, mUser); }; //! \} private: Client::Impl & mImpl; QNetworkReply *mReply; QString mUser; }; //-------------------------------------------------------------------------- // Client::ExecuteMethodFinished //-------------------------------------------------------------------------- // This is really a relatively simple object to bind the method and path // to the execute method callback. class Client::ExecuteMethodFinished : public QObject { Q_OBJECT public: //! \{ ExecuteMethodFinished(QObject *parent, Client::Impl & impl, QNetworkReply *reply, const QString & method, const QString & path) : QObject(parent), mImpl(impl), mReply(reply), mMethod(method), mPath(path) { } //! \} public slots: //! \{ void finished() { mImpl.executeMethodFinished(mReply, mMethod, mPath); }; //! \} private: Client::Impl & mImpl; QNetworkReply * mReply; QString mMethod; QString mPath; }; //-------------------------------------------------------------------------- // Client::Impl //-------------------------------------------------------------------------- void Client::Impl::setBasicAuthFromSession( QNetworkRequestPtr request) { if (mClient->hasSession()) { QString userpass = QString("%1:%2").arg(mClient->session().user()) .arg(mClient->session().p4Ticket()); QString b64Userpass(userpass.toUtf8().toBase64()); QString value = QString("Basic %1").arg(b64Userpass); request->setRawHeader("Authorization", value.toUtf8()); } } void Client::Impl::logInFinished(QNetworkReply *reply, const QString & user) { if (reply->isFinished() && reply->error() == QNetworkReply::NoError) { QByteArray data = reply->readAll(); RequestErrorPtr error = parseRequestError(data); SessionPtr session; if (!error) { QJsonDocument doc; QJsonParseError err; doc = doc.fromJson(data, &err); if (err.error == QJsonParseError::NoError) { QJsonObject object = doc.object(); if (object.contains("ticket")) { QString p4Ticket = object["ticket"].toString(); session.reset(new Session); session->setUrl(mUrl); session->setUser(user); session->setP4Ticket(p4Ticket); mClient->setSession(*session); } } } if (!session) { QString msg = QString( "Unable to create session from JSON data: %1").arg( QString(data)); error.reset(new RequestError(msg, msg, RequestError::JSON_ERROR, RequestError::ERROR)); } emit mClient->logInDone(error, session); } } RequestErrorPtr Client::Impl::fromNetworkError( QNetworkReply::NetworkError code) { QNetworkReply *reply = static_cast<QNetworkReply *>(sender()); RequestErrorPtr error; QVariant variantStatus = reply->attribute( QNetworkRequest::HttpStatusCodeAttribute); int status = -1; if (variantStatus.isValid()) { status = variantStatus.toInt(); } if (status == 403 || code == QNetworkReply::AuthenticationRequiredError) { error.reset(new RequestError); QString msg("Authentication Required"); error->setBaseMessageText(msg); error->setMessageText(msg); error->setCode(RequestError::AUTHENTICATION_ERROR); error->setSeverity(RequestError::ERROR); } if (error.isNull() && reply->isReadable()) { // See if we have any kind of request error response QByteArray data = reply->readAll(); RequestErrorPtr e = parseRequestError(data); if (!e.isNull()) error = e; } if (error.isNull()) { error.reset(new RequestError); // There's not a lot of information here, beyond typical request headers QString msg = QString("Network Error %1").arg(code); error->setMessageText(msg); error->setBaseMessageText(msg); error->setCode(RequestError::NETWORK_ERROR); error->setSeverity(RequestError::ERROR); } return error; } RequestErrorPtr Client::Impl::fromSslErrors( const QList<QSslError> & errors) { // We may want to convert these SSL errors to messages that are more // informative, but I have next to no information what clients would // need. I've found that with this API, information I neewd from Qt // is only logged, and not returned via these objects. RequestErrorPtr error(new RequestError()); error->setMessageText("SSL Error"); error->setBaseMessageText("SSL Error"); error->setCode(RequestError::SSL_ERROR); error->setSeverity(RequestError::ERROR); return error; } void Client::Impl::setSession(const Session & session) { mSession.reset(new Session(session)); } void Client::Impl::validateSessionFinished() { QNetworkReply * reply = static_cast<QNetworkReply*>(sender()); if (reply->isFinished() && reply->error() == QNetworkReply::NoError) { RequestErrorPtr error; emit mClient->validateSessionDone(error); } } void Client::Impl::validateSessionError(QNetworkReply::NetworkError code) { RequestErrorPtr error = fromNetworkError(code); emit mClient->validateSessionDone(error); } void Client::Impl::validateSessionSslErrors(const QList<QSslError> & errors) { if (mIngoreSslErrors) { QNetworkReply * reply = static_cast<QNetworkReply*>(sender()); reply->ignoreSslErrors(); } else { RequestErrorPtr error = fromSslErrors(errors); emit mClient->validateSessionDone(error); } } void Client::Impl::handleLogInError( QNetworkReply::NetworkError code) { RequestErrorPtr error = fromNetworkError(code); emit mClient->logInDone(error, SessionPtr()); }; void Client::Impl::handleLogInSslErrors( const QList<QSslError> & errors) { if (mIngoreSslErrors) { QNetworkReply * reply = static_cast<QNetworkReply*>(sender()); reply->ignoreSslErrors(); } else { RequestErrorPtr error = fromSslErrors(errors); emit mClient->logInDone(error, SessionPtr()); } } void Client::Impl::executeMethodFinished(QNetworkReply * reply, const QString & method, const QString & path) { QSharedPointer<QVariantMapList> data; RequestErrorPtr error; if (reply->isFinished() && reply->error() == QNetworkReply::NoError) { // The web API is intended to return small amounts of data. If you // try to, say, download a large file, you're more than likely // going to get a failure from the web service. QByteArray replyData = reply->readAll(); QJsonParseError jsonParseError; QJsonDocument jsonDocument; jsonDocument = jsonDocument.fromJson(replyData, &jsonParseError); if (jsonParseError.error == QJsonParseError::NoError) { data.reset(new QVariantMapList()); if (jsonDocument.isObject()) { QJsonObject jsonObject = jsonDocument.object(); data->append(jsonObject.toVariantMap()); } else if (jsonDocument.isArray()) { QJsonArray array = jsonDocument.array(); foreach (QJsonValue val, array) { if (val.isObject()) { data->append(val.toObject().toVariantMap()); } else { QVariantMap map; map["value"] = val.toVariant(); data->append(map); } } } else { data.reset(); error.reset(new RequestError("Invalid JSON data", "Invalid JSON data", RequestError::JSON_ERROR, RequestError::ERROR)); } } else { error.reset(new RequestError("Invalid JSON", "Invalid JSON", RequestError::JSON_ERROR, RequestError::ERROR)); } } // We *may* want to check for our error JSON format, though that // should be handled by the executeMethodError mechanism. emit mClient->executeMethodDone(error, method, path, data); } void Client::Impl::executeMethodError(QNetworkReply::NetworkError code) { RequestErrorPtr error = fromNetworkError(code); emit mClient->executeMethodDone(error, "", "", QSharedPointer<QVariantMapList>()); } void Client::Impl::executeMethodSslErrors(const QList<QSslError> & errors) { if (mIngoreSslErrors) { QNetworkReply * reply = static_cast<QNetworkReply*>(sender()); reply->ignoreSslErrors(); } else { RequestErrorPtr error = fromSslErrors(errors); emit mClient->executeMethodDone(error, "", "", QSharedPointer<QVariantMapList>()); } } //-------------------------------------------------------------------------- // Client //-------------------------------------------------------------------------- Client::Client(QObject *parent) : QObject(parent), mImpl(new Impl(this)) { } Client::Client(QObject *parent, QUrl url) : QObject(parent), mImpl(new Impl(this)) { setUrl(url); } Client::~Client() { delete mImpl; } void Client::setUrl(const QUrl & url) { mImpl->mUrl = url; } const QUrl & Client::url() const { return mImpl->mUrl; } bool Client::hasSession() const { return mImpl->mSession.isNull() == false; } const Session & Client::session() const { return *(mImpl->mSession); } void Client::setSession(const Session & session) { mImpl->setSession(session); } const QString & Client::hwsPrefixPath() const { return mImpl->mHWSPrefixPath; } void Client::setHWSPrefixPath(const QString & path) { mImpl->mHWSPrefixPath = path; } void Client::addRequestConfig(QString key, QString value) { mImpl->mConfig[key] = value; } void Client::ignoreSslErrors(bool ignore) { mImpl->mIngoreSslErrors = true; } // The status method might be used to double check our user has void Client::validateSession() { QNetworkRequestPtr request = mImpl->createRequest(); mImpl->setRequestUrlWithPath(request, "/status"); mImpl->setBasicAuthFromSession(request); QNetworkReply *reply = mImpl->mManager.get(*request); connect(reply, SIGNAL(finished()), mImpl, SLOT(validateSessionFinished())); connect(reply, SIGNAL(error(QNetworkReply::NetworkError)), mImpl, SLOT(validateSessionError(QNetworkReply::NetworkError))); connect(reply, SIGNAL(sslErrors( const QList<QSslError> &)), mImpl, SLOT(validateSessionSslErrors( const QList<QSslError> &))); } void Client::logIn(const QString & user, const QString & password) { QByteArray data = mImpl->createSessionPostBody(user, password); QNetworkRequestPtr request = mImpl->createRequest(); mImpl->setRequestUrlWithPath(request, "/auth/v1/login"); // request->setRawHeader("Content-Length", // QByteArray::number(data.size())); QNetworkReply *reply = mImpl->mManager.post(*request, data); LogInFinished *finisher = new LogInFinished(this, *mImpl, reply, user); connect(reply, SIGNAL(finished()), finisher, SLOT(finished())); connect(reply, SIGNAL(error(QNetworkReply::NetworkError)), mImpl, SLOT(handleLogInError(QNetworkReply::NetworkError))); connect(reply, SIGNAL(sslErrors( const QList<QSslError> &)), mImpl, SLOT(handleLogInSslErrors( const QList<QSslError> &))); } void Client::executeMethod(const QString & method, const QString & path, const QSharedPointer<QStringHash> params, const QSharedPointer<QByteArray> body) { QNetworkRequestPtr request = mImpl->createRequest(); mImpl->setRequestUrlWithPath(request, path, params); mImpl->setBasicAuthFromSession(request); QIODevice *data = NULL; if (body) { data = new QBuffer(body.data(), this); } QNetworkReply *reply = mImpl->mManager.sendCustomRequest(*request, method.toUtf8(), data); ExecuteMethodFinished * finisher = new ExecuteMethodFinished(this, *mImpl, reply, method, path); connect(reply, SIGNAL(finished()), finisher, SLOT(finished())); connect(reply, SIGNAL(error(QNetworkReply::NetworkError)), mImpl, SLOT(executeMethodError(QNetworkReply::NetworkError))); connect(reply, SIGNAL(sslErrors( const QList<QSslError> &)), mImpl, SLOT(executeMethodSslErrors( const QList<QSslError> &))); } QJsonObject hashToJsonObject(const QVariantMap & map) { QJsonObject obj; foreach(QString key, map.keys()) { QVariant value = map[key]; QJsonValue jsonValue = QJsonValue::fromVariant(value); obj.insert(key, jsonValue); } return obj; } void Client::executeMethod(const QString & method, const QString & path, const QSharedPointer<QStringHash> params, const QVariantMap & body) { QJsonObject obj = hashToJsonObject(body); QJsonDocument doc(obj); QByteArray json = doc.toJson(QJsonDocument::Compact); executeMethod(method, path, params, QSharedPointer<QByteArray>(&json)); } void Client::executeMethod(const QString & method, const QString & path, const QSharedPointer<QStringHash> params, const QVariantMapList & body) { QJsonArray arr; foreach(QVariantMap map, body) { QJsonObject obj = hashToJsonObject(map); arr.append(obj); } QJsonDocument doc(arr); QByteArray json = doc.toJson(QJsonDocument::Compact); executeMethod(method, path, params, QSharedPointer<QByteArray>(&json)); } //! \} } #include "Client.moc"
# | Change | User | Description | Committed | |
---|---|---|---|---|---|
#1 | 15741 | ptomiak | Branch HWS for my use. | ||
//guest/perforce_software/helix-web-services/main/build/helix_web_services_client_qt/hws/Client.cpp | |||||
#7 | 15601 | tjuricek | validateSessionFinished should not emit a signal if an error occurred on the request | ||
#6 | 15578 | tjuricek |
Removing QSettings* usage from hws::Client. The way QSettings was being used only is relevant for one connection at a time, and, it didn't seem to work on windows nicely anyway. |
||
#5 | 15521 | tjuricek | Call client.ignoreSslErrors(true) to bypass self-signed cert problems. | ||
#4 | 15448 | tjuricek |
Qt SDK revision: remove higher-level objects from the SDK. It's likely we could add higher-level objects that adapt the executeMethodDone and convert the variant maps to something, well, typed and easier to use. That's not in my current scope of work, however. |
||
#3 | 14102 | tjuricek |
Set the default prefix to "/hws" for now. This is very likely to become a real convention. The "primary" UI will be hosted on the root, with the Web Services instance hosted "under" /hws. |
||
#2 | 14055 | tjuricek | Updating helix_web_services_client build for new 'my' vs 'all' projects feature | ||
#1 | 14050 | tjuricek | Prep versioned release directory for inclusion into Helix Sync app. |