diff --git a/editor/editor.pro b/editor/editor.pro index bed077a591..ce3506fdbe 100644 --- a/editor/editor.pro +++ b/editor/editor.pro @@ -13,9 +13,11 @@ SOURCES += \ server_api.cpp \ ui2oh.cpp \ xml_feature.cpp \ + osm_auth.cpp \ HEADERS += \ opening_hours_ui.hpp \ server_api.hpp \ ui2oh.hpp \ xml_feature.hpp \ + osm_auth.hpp \ diff --git a/editor/editor_tests/editor_tests.pro b/editor/editor_tests/editor_tests.pro index dc8f421d52..27c40ab2fa 100644 --- a/editor/editor_tests/editor_tests.pro +++ b/editor/editor_tests/editor_tests.pro @@ -4,7 +4,7 @@ CONFIG -= app_bundle TEMPLATE = app ROOT_DIR = ../.. -DEPENDENCIES = editor geometry coding base stats_client opening_hours pugixml +DEPENDENCIES = editor geometry coding base stats_client opening_hours pugixml oauthcpp include($$ROOT_DIR/common.pri) @@ -18,3 +18,4 @@ SOURCES += \ server_api_test.cpp \ xml_feature_test.cpp \ ui2oh_test.cpp \ + osm_auth_test.cpp \ diff --git a/editor/editor_tests/osm_auth_test.cpp b/editor/editor_tests/osm_auth_test.cpp new file mode 100644 index 0000000000..5d7df5e7fa --- /dev/null +++ b/editor/editor_tests/osm_auth_test.cpp @@ -0,0 +1,48 @@ +#include "testing/testing.hpp" + +#include "editor/osm_auth.hpp" + +using osm::OsmOAuth; +using osm::ClientToken; + +namespace +{ +constexpr char const * kTestServer = "http://188.166.112.124:3000"; +constexpr char const * kConsumerKey = "QqwiALkYZ4Jd19lo1dtoPhcwGQUqMCMeVGIQ8Ahb"; +constexpr char const * kConsumerSecret = "wi9HZKFoNYS06Yad5s4J0bfFo2hClMlH7pXaXWS3"; +constexpr char const * kTestUser = "Testuser"; +constexpr char const * kTestPassword = "testtest"; +constexpr char const * kInvalidPassword = "123"; +constexpr char const * kFacebookToken = "CAAYYoGXMFUcBAHZBpDFyFPFQroYRMtzdCzXVFiqKcZAZB44jKjzW8WWoaPWI4xxl9EK8INIuTZAkhpURhwSiyOIKoWsgbqZAKEKIKZC3IdlUokPOEuaUpKQzgLTUcYNLiqgJogjUTL1s7Myqpf8cf5yoxQm32cqKZAdozrdx2df4FMJBSF7h0dXI49M2WjCyjPcEKntC4LfQsVwrZBn8uStvUJBVGMTwNWkZD"; +} // namespace + +UNIT_TEST(OSM_Auth_InvalidLogin) +{ + OsmOAuth auth(kConsumerKey, kConsumerSecret, kTestServer, kTestServer); + ClientToken token; + auto result = auth.AuthorizePassword(kTestUser, kInvalidPassword, token); + TEST_EQUAL(result, OsmOAuth::AuthResult::FailLogin, ("invalid password")); + TEST(!token.IsValid(), ("not authorized")); +} + +UNIT_TEST(OSM_Auth_Login) +{ + OsmOAuth auth(kConsumerKey, kConsumerSecret, kTestServer, kTestServer); + ClientToken token; + auto result = auth.AuthorizePassword(kTestUser, kTestPassword, token); + TEST_EQUAL(result, OsmOAuth::AuthResult::OK, ("login to test server")); + TEST(token.IsValid(), ("authorized")); + string const perm = auth.Request(token, "/permissions"); + TEST(perm.find("write_api") != string::npos, ("can write to api")); +} + +UNIT_TEST(OSM_Auth_Facebook) +{ + OsmOAuth auth(kConsumerKey, kConsumerSecret, kTestServer, kTestServer); + ClientToken token; + auto result = auth.AuthorizeFacebook(kFacebookToken, token); + TEST_EQUAL(result, OsmOAuth::AuthResult::OK, ("login via facebook")); + TEST(token.IsValid(), ("authorized")); + string const perm = auth.Request(token, "/permissions"); + TEST(perm.find("write_api") != string::npos, ("can write to api")); +} diff --git a/editor/osm_auth.cpp b/editor/osm_auth.cpp new file mode 100644 index 0000000000..fea76a0501 --- /dev/null +++ b/editor/osm_auth.cpp @@ -0,0 +1,238 @@ +#include "editor/osm_auth.hpp" + +#include "base/assert.hpp" +#include "base/logging.hpp" +#include "coding/url_encode.hpp" +#include "std/iostream.hpp" +#include "std/map.hpp" +#include "3party/liboauthcpp/include/liboauthcpp/liboauthcpp.h" +#include "3party/Alohalytics/src/http_client.h" + +using alohalytics::HTTPClientPlatformWrapper; + +namespace osm +{ +constexpr char const * kApiVersion = "/api/0.6"; + +namespace +{ +string const kOSMSessionCookie = "_osm_session"; + +string findAuthenticityToken(string const & body) +{ + auto pos = body.find("name=\"authenticity_token\""); + if (pos == string::npos) + return string(); + string const kValue = "value=\""; + auto start = body.find(kValue, pos); + if (start == string::npos) + return string(); + start += kValue.length(); + auto const end = body.find("\"", start); + return end == string::npos ? string() : body.substr(start, end - start); +} + +string buildPostRequest(map const & params) +{ + string result; + for (auto it = params.begin(); it != params.end(); ++it) + { + if (it != params.begin()) + result += "&"; + result += it->first + "=" + UrlEncode(it->second); + } + return result; +} + +// Trying to determine whether it's a login page. +bool isLoggedIn(string const & contents) +{ + return contents.find("
params; + params["oauth_token"] = requestTokenKey; + params["oauth_callback"] = ""; + params["authenticity_token"] = sid.m_token; + params["allow_read_prefs"] = "yes"; + params["allow_write_api"] = "yes"; + params["commit"] = "Save changes"; + HTTPClientPlatformWrapper request(m_baseUrl + "/oauth/authorize"); + request.set_body_data(buildPostRequest(params), "application/x-www-form-urlencoded"); + request.set_cookies(kOSMSessionCookie + "=" + sid.m_id); + + if (!request.RunHTTPRequest()) + return string(); + + string const callbackURL = request.url_received(); + string const vKey = "oauth_verifier="; + auto const pos = callbackURL.find(vKey); + if (pos == string::npos) + return string(); + auto const end = callbackURL.find("&", pos); + return callbackURL.substr(pos + vKey.length(), end == string::npos ? end : end - pos - vKey.length()+ 1); +} + +// Given a web session id, fetches an OAuth access token. +OsmOAuth::AuthResult OsmOAuth::FetchAccessToken(SessionID const & sid, ClientToken & token) const +{ + // Aquire a request token. + OAuth::Consumer consumer(m_consumerKey, m_consumerSecret); + OAuth::Client oauth(&consumer); + string const requestTokenUrl = m_baseUrl + "/oauth/request_token"; + string const requestTokenQuery = oauth.getURLQueryString(OAuth::Http::Get, requestTokenUrl + "?oauth_callback=oob"); + HTTPClientPlatformWrapper request(requestTokenUrl + "?" + requestTokenQuery); + if (!(request.RunHTTPRequest() && request.error_code() == 200 && !request.was_redirected())) + return AuthResult::NoOAuth; + OAuth::Token requestToken = OAuth::Token::extract(request.server_response()); + + // Faking a button press for access rights. + string const pin = SendAuthRequest(requestToken.key(), sid); + if (pin.empty()) + return AuthResult::FailAuth; + requestToken.setPin(pin); + + // Got pin, exchange it for the access token. + oauth = OAuth::Client(&consumer, &requestToken); + string const accessTokenUrl = m_baseUrl + "/oauth/access_token"; + string const queryString = oauth.getURLQueryString(OAuth::Http::Get, accessTokenUrl, "", true); + HTTPClientPlatformWrapper request2(accessTokenUrl + "?" + queryString); + if (!(request2.RunHTTPRequest() && request2.error_code() == 200 && !request2.was_redirected())) + return AuthResult::NoAccess; + OAuth::KeyValuePairs responseData = OAuth::ParseKeyValuePairs(request2.server_response()); + OAuth::Token accessToken = OAuth::Token::extract(responseData); + + token.m_key = accessToken.key(); + token.m_secret = accessToken.secret(); + + LogoutUser(sid); + + return AuthResult::OK; +} + +OsmOAuth::AuthResult OsmOAuth::AuthorizePassword(string const & login, string const & password, ClientToken & token) const +{ + SessionID sid; + AuthResult result = FetchSessionId(sid); + if (result != AuthResult::OK) + return result; + + result = LoginUserPassword(login, password, sid); + if (result != AuthResult::OK) + return result; + + return FetchAccessToken(sid, token); +} + +OsmOAuth::AuthResult OsmOAuth::AuthorizeFacebook(string const & facebookToken, ClientToken & token) const +{ + SessionID sid; + AuthResult result = FetchSessionId(sid); + if (result != AuthResult::OK) + return result; + + result = LoginFacebook(facebookToken, sid); + if (result != AuthResult::OK) + return result; + + return FetchAccessToken(sid, token); +} + +string OsmOAuth::Request(ClientToken const & token, string const & method, string const & httpMethod, string const & body) const +{ + OAuth::Consumer const consumer(m_consumerKey, m_consumerSecret); + OAuth::Token const oatoken(token.m_key, token.m_secret); + OAuth::Client oauth(&consumer, &oatoken); + + OAuth::Http::RequestType reqType; + if (httpMethod == "GET") + reqType = OAuth::Http::Get; + else if (httpMethod == "POST") + reqType = OAuth::Http::Post; + else if (httpMethod == "PUT") + reqType = OAuth::Http::Put; + else + { + ASSERT(false, ("Unsupported OSM API request method", httpMethod)); + return string(); + } + + string const url = m_apiUrl + kApiVersion + method; + string const query = oauth.getURLQueryString(reqType, url, body); + + HTTPClientPlatformWrapper request(url + "?" + query); + if (httpMethod != "GET") + request.set_body_data(body, "application/xml", httpMethod); + if (request.RunHTTPRequest() && request.error_code() == 200 && !request.was_redirected()) + return request.server_response(); + + return string(); +} + +} // namespace osm diff --git a/editor/osm_auth.hpp b/editor/osm_auth.hpp new file mode 100644 index 0000000000..050a230f0a --- /dev/null +++ b/editor/osm_auth.hpp @@ -0,0 +1,67 @@ +#pragma once + +#include "std/string.hpp" + +namespace osm +{ + +struct ClientToken +{ + string m_key; + string m_secret; + inline bool IsValid() const { return !m_key.empty() && !m_secret.empty(); } +}; + +constexpr char const * kDefaultBaseURL = "https://www.openstreetmap.org"; +constexpr char const * kDefaultApiURL = "https://api.openstreetmap.org"; + +class OsmOAuth +{ +public: + enum AuthResult + { + OK, + FailCookie, + FailLogin, + NoOAuth, + FailAuth, + NoAccess, + NetworkError, + ServerError + }; + + OsmOAuth(string const & consumerKey, string const & consumerSecret, + string const & baseUrl = kDefaultBaseURL, + string const & apiUrl = kDefaultApiURL): + m_consumerKey(consumerKey), + m_consumerSecret(consumerSecret), + m_baseUrl(baseUrl), + m_apiUrl(apiUrl) + { + } + + AuthResult AuthorizePassword(string const & login, string const & password, ClientToken & token) const; + AuthResult AuthorizeFacebook(string const & facebookToken, ClientToken & token) const; + string Request(ClientToken const & token, string const & method, string const & httpMethod = "GET", string const & body = "") const; + +private: + struct SessionID + { + string m_id; + string m_token; + }; + + string m_consumerKey; + string m_consumerSecret; + string m_baseUrl; + string m_apiUrl; + + AuthResult FetchSessionId(SessionID & sid) const; + AuthResult LogoutUser(SessionID const & sid) const; + AuthResult LoginUserPassword(string const & login, string const & password, SessionID const & sid) const; + AuthResult LoginFacebook(string const & facebookToken, SessionID const & sid) const; + string SendAuthRequest(string const & requestTokenKey, SessionID const & sid) const; + AuthResult FetchAccessToken(SessionID const & sid, ClientToken & token) const; +}; + +} // namespace osm