diff --git a/android/jni/Android.mk b/android/jni/Android.mk index 5d72693732..aed7643bfd 100644 --- a/android/jni/Android.mk +++ b/android/jni/Android.mk @@ -61,48 +61,51 @@ TARGET_PLATFORM := android-15 LOCAL_HEADER_FILES := \ ../../private.h \ com/mapswithme/core/jni_helper.hpp \ - com/mapswithme/core/ScopedLocalRef.hpp \ com/mapswithme/core/logging.hpp \ + com/mapswithme/core/ScopedEnv.hpp \ + com/mapswithme/core/ScopedLocalRef.hpp \ com/mapswithme/maps/Framework.hpp \ - com/mapswithme/platform/Platform.hpp \ com/mapswithme/opengl/android_gl_utils.hpp \ com/mapswithme/opengl/androidoglcontext.hpp \ com/mapswithme/opengl/androidoglcontextfactory.hpp \ + com/mapswithme/platform/Platform.hpp \ LOCAL_SRC_FILES := \ com/mapswithme/core/jni_helper.cpp \ com/mapswithme/core/logging.cpp \ - com/mapswithme/maps/DisplayedCategories.cpp \ - com/mapswithme/maps/Framework.cpp \ com/mapswithme/maps/bookmarks/data/Bookmark.cpp \ com/mapswithme/maps/bookmarks/data/BookmarkManager.cpp \ com/mapswithme/maps/bookmarks/data/BookmarkCategory.cpp \ - com/mapswithme/maps/sound/tts.cpp \ - com/mapswithme/maps/MapFragment.cpp \ - com/mapswithme/maps/MwmApplication.cpp \ + com/mapswithme/maps/DisplayedCategories.cpp \ + com/mapswithme/maps/DownloadResourcesActivity.cpp \ + com/mapswithme/maps/editor/OpeningHours.cpp \ + com/mapswithme/maps/editor/Editor.cpp \ + com/mapswithme/maps/editor/OsmOAuth.cpp \ + com/mapswithme/maps/Framework.cpp \ com/mapswithme/maps/LocationState.cpp \ com/mapswithme/maps/LocationHelper.cpp \ - com/mapswithme/maps/TrackRecorder.cpp \ + com/mapswithme/maps/MapFragment.cpp \ com/mapswithme/maps/MapManager.cpp \ - com/mapswithme/maps/DownloadResourcesActivity.cpp \ + com/mapswithme/maps/MwmApplication.cpp \ com/mapswithme/maps/PrivateVariables.cpp \ com/mapswithme/maps/SearchEngine.cpp \ com/mapswithme/maps/SearchRecents.cpp \ - com/mapswithme/maps/UserMarkHelper.cpp \ - com/mapswithme/maps/SponsoredHotel.cpp \ com/mapswithme/maps/settings/UnitLocale.cpp \ - com/mapswithme/platform/Platform.cpp \ - com/mapswithme/platform/HttpThread.cpp \ - com/mapswithme/platform/Language.cpp \ - com/mapswithme/platform/PThreadImpl.cpp \ - com/mapswithme/util/StringUtils.cpp \ - com/mapswithme/util/Config.cpp \ + com/mapswithme/maps/sound/tts.cpp \ + com/mapswithme/maps/SponsoredHotel.cpp \ + com/mapswithme/maps/TrackRecorder.cpp \ + com/mapswithme/maps/UserMarkHelper.cpp \ com/mapswithme/opengl/android_gl_utils.cpp \ com/mapswithme/opengl/androidoglcontext.cpp \ com/mapswithme/opengl/androidoglcontextfactory.cpp \ - com/mapswithme/maps/editor/OpeningHours.cpp \ - com/mapswithme/maps/editor/Editor.cpp \ - com/mapswithme/maps/editor/OsmOAuth.cpp + com/mapswithme/platform/HttpThread.cpp \ + com/mapswithme/platform/Language.cpp \ + com/mapswithme/platform/Platform.cpp \ + com/mapswithme/platform/PThreadImpl.cpp \ + com/mapswithme/util/Config.cpp \ + com/mapswithme/util/HttpClient.cpp \ + com/mapswithme/util/StringUtils.cpp \ + LOCAL_LDLIBS := -llog -landroid -lEGL -lGLESv2 -latomic -lz diff --git a/android/jni/com/mapswithme/core/ScopedEnv.hpp b/android/jni/com/mapswithme/core/ScopedEnv.hpp new file mode 100644 index 0000000000..d87b51419f --- /dev/null +++ b/android/jni/com/mapswithme/core/ScopedEnv.hpp @@ -0,0 +1,40 @@ +#pragma once + +#include + +// Scoped environment which can attach to any thread and automatically detach +class ScopedEnv final +{ +public: + ScopedEnv(JavaVM * vm) + { + JNIEnv * env; + auto result = vm->GetEnv(reinterpret_cast(&env), JNI_VERSION_1_6); + if (result == JNI_EDETACHED) + { + result = vm->AttachCurrentThread(&env, nullptr); + m_needToDetach = (result == JNI_OK); + } + + if (result == JNI_OK) + { + m_env = env; + m_vm = vm; + } + } + + ~ScopedEnv() + { + if (m_vm != nullptr && m_needToDetach) + m_vm->DetachCurrentThread(); + } + + JNIEnv * operator->() { return m_env; } + operator bool() const { return m_env != nullptr; } + JNIEnv * get() { return m_env; } + +private: + bool m_needToDetach = false; + JNIEnv * m_env = nullptr; + JavaVM * m_vm = nullptr; +}; diff --git a/android/jni/com/mapswithme/core/jni_helper.cpp b/android/jni/com/mapswithme/core/jni_helper.cpp index 0524197558..e6eba49c83 100644 --- a/android/jni/com/mapswithme/core/jni_helper.cpp +++ b/android/jni/com/mapswithme/core/jni_helper.cpp @@ -13,10 +13,12 @@ extern JavaVM * GetJVM() return g_jvm; } -// caching is necessary to create class from native threads +// Caching is necessary to create class from native threads. jclass g_mapObjectClazz; jclass g_bookmarkClazz; jclass g_myTrackerClazz; +jclass g_httpClientClazz; +jclass g_httpParamsClazz; extern "C" { @@ -31,6 +33,8 @@ JNI_OnLoad(JavaVM * jvm, void *) g_mapObjectClazz = jni::GetGlobalClassRef(env, "com/mapswithme/maps/bookmarks/data/MapObject"); g_bookmarkClazz = jni::GetGlobalClassRef(env, "com/mapswithme/maps/bookmarks/data/Bookmark"); g_myTrackerClazz = jni::GetGlobalClassRef(env, "com/my/tracker/MyTracker"); + g_httpClientClazz = jni::GetGlobalClassRef(env, "com/mapswithme/util/HttpClient"); + g_httpParamsClazz = jni::GetGlobalClassRef(env, "com/mapswithme/util/HttpClient$Params"); return JNI_VERSION_1_6; } @@ -43,6 +47,8 @@ JNI_OnUnload(JavaVM *, void *) env->DeleteGlobalRef(g_mapObjectClazz); env->DeleteGlobalRef(g_bookmarkClazz); env->DeleteGlobalRef(g_myTrackerClazz); + env->DeleteGlobalRef(g_httpClientClazz); + env->DeleteGlobalRef(g_httpParamsClazz); } } // extern "C" diff --git a/android/jni/com/mapswithme/core/jni_helper.hpp b/android/jni/com/mapswithme/core/jni_helper.hpp index 5e507d0bcc..f697e65d01 100644 --- a/android/jni/com/mapswithme/core/jni_helper.hpp +++ b/android/jni/com/mapswithme/core/jni_helper.hpp @@ -12,6 +12,8 @@ extern jclass g_mapObjectClazz; extern jclass g_bookmarkClazz; extern jclass g_myTrackerClazz; +extern jclass g_httpClientClazz; +extern jclass g_httpParamsClazz; namespace jni { diff --git a/android/jni/com/mapswithme/util/HttpClient.cpp b/android/jni/com/mapswithme/util/HttpClient.cpp new file mode 100644 index 0000000000..ccb2c74977 --- /dev/null +++ b/android/jni/com/mapswithme/util/HttpClient.cpp @@ -0,0 +1,240 @@ +/******************************************************************************* +The MIT License (MIT) + +Copyright (c) 2015 Alexander Zolotarev from Minsk, Belarus + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*******************************************************************************/ +#include + +#include "../core/jni_helper.hpp" +#include "../core/ScopedEnv.hpp" +#include "../core/ScopedLocalRef.hpp" + +#include "platform/http_client.hpp" + +#include "base/assert.hpp" +#include "base/exception.hpp" +#include "base/logging.hpp" + +#include "std/string.hpp" +#include "std/unordered_map.hpp" + +DECLARE_EXCEPTION(JniException, RootException); + +#define CLEAR_AND_RETURN_FALSE_ON_EXCEPTION \ + if (env->ExceptionCheck()) \ + { \ + env->ExceptionDescribe(); \ + env->ExceptionClear(); \ + return false; \ + } + +namespace +{ +void RethrowOnJniException(ScopedEnv & env) +{ + if (!env->ExceptionCheck()) + return; + + env->ExceptionDescribe(); + env->ExceptionClear(); + MYTHROW(JniException, ()); +} + +jfieldID GetHttpParamsFieldId(ScopedEnv & env, const char * name) +{ + return env->GetFieldID(g_httpParamsClazz, name, "Ljava/lang/String;"); +} + +// Set string value to HttpClient.Params object, throws JniException and +void SetString(ScopedEnv & env, jobject params, jfieldID const fieldId, string const & value) +{ + if (value.empty()) + return; + + jni::ScopedLocalRef const wrappedValue(env.get(), jni::ToJavaString(env.get(), value)); + RethrowOnJniException(env); + + env->SetObjectField(params, fieldId, wrappedValue.get()); + RethrowOnJniException(env); +} + +// Get string value from HttpClient.Params object, throws JniException. +void GetString(ScopedEnv & env, jobject const params, jfieldID const fieldId, string & result) +{ + jni::ScopedLocalRef const wrappedValue( + env.get(), static_cast(env->GetObjectField(params, fieldId))); + RethrowOnJniException(env); + if (wrappedValue) + result = jni::ToNativeString(env.get(), wrappedValue.get()); +} + +class Ids +{ +public: + explicit Ids(ScopedEnv & env) + { + m_fieldIds = + {{"httpMethod", GetHttpParamsFieldId(env, "httpMethod")}, + {"contentType", GetHttpParamsFieldId(env, "contentType")}, + {"contentEncoding", GetHttpParamsFieldId(env, "contentEncoding")}, + {"userAgent", GetHttpParamsFieldId(env, "userAgent")}, + {"inputFilePath", GetHttpParamsFieldId(env, "inputFilePath")}, + {"basicAuthUser", GetHttpParamsFieldId(env, "basicAuthUser")}, + {"basicAuthPassword", GetHttpParamsFieldId(env, "basicAuthPassword")}, + {"cookies", GetHttpParamsFieldId(env, "cookies")}, + {"receivedUrl", GetHttpParamsFieldId(env, "receivedUrl")}}; + } + + jfieldID GetId(string const & fieldName) const + { + auto const it = m_fieldIds.find(fieldName); + CHECK(it != m_fieldIds.end(), ("Incorrect field name:", fieldName)); + return it->second; + } + +private: + unordered_map m_fieldIds; +}; +} // namespace + +//*********************************************************************** +// Exported functions implementation +//*********************************************************************** +namespace platform +{ +bool HttpClient::RunHttpRequest() +{ + ScopedEnv env(jni::GetJVM()); + + if (!env) + return false; + + static Ids ids(env); + + // Create and fill request params. + jni::ScopedLocalRef const jniUrl(env.get(), + jni::ToJavaString(env.get(), m_urlRequested)); + CLEAR_AND_RETURN_FALSE_ON_EXCEPTION + + static jmethodID const httpParamsConstructor = + env->GetMethodID(g_httpParamsClazz, "", "(Ljava/lang/String;)V"); + + jni::ScopedLocalRef const httpParamsObject( + env.get(), env->NewObject(g_httpParamsClazz, httpParamsConstructor, jniUrl.get())); + CLEAR_AND_RETURN_FALSE_ON_EXCEPTION + + // Cache it on the first call. + static jfieldID const dataField = env->GetFieldID(g_httpParamsClazz, "data", "[B"); + if (!m_bodyData.empty()) + { + jni::ScopedLocalRef const jniPostData(env.get(), + env->NewByteArray(m_bodyData.size())); + CLEAR_AND_RETURN_FALSE_ON_EXCEPTION + + env->SetByteArrayRegion(jniPostData.get(), 0, m_bodyData.size(), + reinterpret_cast(m_bodyData.data())); + CLEAR_AND_RETURN_FALSE_ON_EXCEPTION + + env->SetObjectField(httpParamsObject.get(), dataField, jniPostData.get()); + CLEAR_AND_RETURN_FALSE_ON_EXCEPTION + } + + ASSERT(!m_httpMethod.empty(), ("Http method type can not be empty.")); + + try + { + SetString(env, httpParamsObject.get(), ids.GetId("httpMethod"), m_httpMethod); + SetString(env, httpParamsObject.get(), ids.GetId("contentType"), m_contentType); + SetString(env, httpParamsObject.get(), ids.GetId("contentEncoding"), m_contentEncoding); + SetString(env, httpParamsObject.get(), ids.GetId("userAgent"), m_userAgent); + SetString(env, httpParamsObject.get(), ids.GetId("inputFilePath"), m_inputFile); + SetString(env, httpParamsObject.get(), ids.GetId("outputFilePath"), m_outputFile); + SetString(env, httpParamsObject.get(), ids.GetId("basicAuthUser"), m_basicAuthUser); + SetString(env, httpParamsObject.get(), ids.GetId("basicAuthPassword"), m_basicAuthPassword); + SetString(env, httpParamsObject.get(), ids.GetId("cookies"), m_cookies); + } + catch (JniException const & ex) + { + return false; + } + + static jfieldID const debugModeField = env->GetFieldID(g_httpParamsClazz, "debugMode", "Z"); + env->SetBooleanField(httpParamsObject.get(), debugModeField, m_debugMode); + CLEAR_AND_RETURN_FALSE_ON_EXCEPTION + + static jfieldID const followRedirectsField = + env->GetFieldID(g_httpParamsClazz, "followRedirects", "Z"); + env->SetBooleanField(httpParamsObject.get(), followRedirectsField, m_handleRedirects); + CLEAR_AND_RETURN_FALSE_ON_EXCEPTION + + static jmethodID const httpClientClassRun = + env->GetStaticMethodID(g_httpClientClazz, "run", + "(Lcom/mapswithme/util/HttpClient$Params;)Lcom/mapswithme/util/HttpClient$Params;"); + + // Current Java implementation simply reuses input params instance, so we don't need to + // call DeleteLocalRef(response). + jobject const response = + env->CallStaticObjectMethod(g_httpClientClazz, httpClientClassRun, httpParamsObject.get()); + CLEAR_AND_RETURN_FALSE_ON_EXCEPTION + + static jfieldID const httpResponseCodeField = + env->GetFieldID(g_httpParamsClazz, "httpResponseCode", "I"); + m_errorCode = env->GetIntField(response, httpResponseCodeField); + CLEAR_AND_RETURN_FALSE_ON_EXCEPTION + + try + { + GetString(env, response, ids.GetId("receivedUrl"), m_urlReceived); + GetString(env, response, ids.GetId("contentType"), m_contentTypeReceived); + GetString(env, response, ids.GetId("contentEncoding"), m_contentEncodingReceived); + } + catch (JniException const & ex) + { + return false; + } + + // Note: cookies field is used not only to send Cookie header, but also to receive back + // Server-Cookie header. CookiesField is already cached above. + jni::ScopedLocalRef const jniServerCookies( + env.get(), static_cast(env->GetObjectField(response, ids.GetId("cookies")))); + CLEAR_AND_RETURN_FALSE_ON_EXCEPTION + if (jniServerCookies) + { + m_serverCookies = + NormalizeServerCookies(jni::ToNativeString(env.get(), jniServerCookies.get())); + } + + // dataField is already cached above. + jni::ScopedLocalRef const jniData( + env.get(), static_cast(env->GetObjectField(response, dataField))); + CLEAR_AND_RETURN_FALSE_ON_EXCEPTION + if (jniData) + { + jbyte * buffer = env->GetByteArrayElements(jniData.get(), nullptr); + if (buffer) + { + m_serverResponse.assign(reinterpret_cast(buffer), env->GetArrayLength(jniData.get())); + env->ReleaseByteArrayElements(jniData.get(), buffer, JNI_ABORT); + } + } + return true; +} +} // namespace platform diff --git a/android/src/com/mapswithme/util/HttpClient.java b/android/src/com/mapswithme/util/HttpClient.java new file mode 100644 index 0000000000..3f3510ad97 --- /dev/null +++ b/android/src/com/mapswithme/util/HttpClient.java @@ -0,0 +1,241 @@ +/******************************************************************************* + * The MIT License (MIT) + *

+ * Copyright (c) 2014 Alexander Zolotarev from Minsk, Belarus + *

+ * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + *

+ * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + *

+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + *******************************************************************************/ + +package com.mapswithme.util; + +import android.support.annotation.NonNull; +import android.text.TextUtils; +import android.util.Base64; +import android.util.Log; + +import java.io.BufferedInputStream; +import java.io.BufferedOutputStream; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.net.HttpURLConnection; +import java.net.URL; +import java.util.List; +import java.util.Map; + +public final class HttpClient +{ + // TODO(AlexZ): tune for larger files + private final static int STREAM_BUFFER_SIZE = 1024 * 64; + private final static String TAG = "Alohalytics-Http"; + // Globally accessible for faster unit-testing + public static int TIMEOUT_IN_MILLISECONDS = 30000; + + public static Params run(@NonNull final Params p) throws IOException, NullPointerException + { + if (TextUtils.isEmpty(p.httpMethod)) + throw new IllegalArgumentException("Please set valid HTTP method for request at Params.httpMethod field."); + + HttpURLConnection connection = null; + if (p.debugMode) + Log.d(TAG, "Connecting to " + p.url); + try + { + connection = (HttpURLConnection) new URL(p.url).openConnection(); // NullPointerException, MalformedUrlException, IOException + // Redirects from http to https or vice versa are not supported by Android implementation. + // There is also a nasty bug on Androids before 4.4: + // if you send any request with Content-Length set, and it is redirected, and your instance is set to automatically follow redirects, + // then next (internal) GET request to redirected location will incorrectly have have all headers set from the previous request, + // including Content-Length, Content-Type etc. This leads to unexpected hangs and timeout errors, because some servers are + // correctly trying to wait for the body if Content-Length is set. + // It shows in logs like this: + // + // java.net.SocketTimeoutException: Read timed out + // at org.apache.harmony.xnet.provider.jsse.NativeCrypto.SSL_read(Native Method) + // at org.apache.harmony.xnet.provider.jsse.OpenSSLSocketImpl$SSLInputStream.read(OpenSSLSocketImpl.java:687) + // ... + // + // Looks like this bug was fixed by switching to OkHttp implementation in this commit: + // https://android.googlesource.com/platform/libcore/+/2503556d17b28c7b4e6e514540a77df1627857d0 + connection.setInstanceFollowRedirects(p.followRedirects); + connection.setConnectTimeout(TIMEOUT_IN_MILLISECONDS); + connection.setReadTimeout(TIMEOUT_IN_MILLISECONDS); + connection.setUseCaches(false); + connection.setRequestMethod(p.httpMethod); + if (!TextUtils.isEmpty(p.basicAuthUser)) + { + final String encoded = Base64.encodeToString((p.basicAuthUser + ":" + p.basicAuthPassword).getBytes(), Base64.NO_WRAP); + connection.setRequestProperty("Authorization", "Basic " + encoded); + } + if (!TextUtils.isEmpty(p.userAgent)) + connection.setRequestProperty("User-Agent", p.userAgent); + + if (!TextUtils.isEmpty(p.cookies)) + connection.setRequestProperty("Cookie", p.cookies); + + if (!TextUtils.isEmpty(p.inputFilePath) || p.data != null) + { + // Send (POST, PUT...) data to the server. + if (TextUtils.isEmpty(p.contentType)) + throw new NullPointerException("Please set Content-Type for request."); + + // Work-around for situation when more than one consequent POST requests can lead to stable + // "java.net.ProtocolException: Unexpected status line:" on a client and Nginx HTTP 499 errors. + // The only found reference to this bug is http://stackoverflow.com/a/24303115/1209392 + connection.setRequestProperty("Connection", "close"); + connection.setRequestProperty("Content-Type", p.contentType); + if (!TextUtils.isEmpty(p.contentEncoding)) + connection.setRequestProperty("Content-Encoding", p.contentEncoding); + + connection.setDoOutput(true); + if (p.data != null) + { + connection.setFixedLengthStreamingMode(p.data.length); + final OutputStream os = connection.getOutputStream(); + try + { + os.write(p.data); + } + finally + { + os.close(); + } + if (p.debugMode) + Log.d(TAG, "Sent " + p.httpMethod + " with content of size " + p.data.length); + } + else + { + final File file = new File(p.inputFilePath); + connection.setFixedLengthStreamingMode((int) file.length()); + final BufferedInputStream istream = new BufferedInputStream(new FileInputStream(file), STREAM_BUFFER_SIZE); + final BufferedOutputStream ostream = new BufferedOutputStream(connection.getOutputStream(), STREAM_BUFFER_SIZE); + final byte[] buffer = new byte[STREAM_BUFFER_SIZE]; + int bytesRead; + while ((bytesRead = istream.read(buffer, 0, STREAM_BUFFER_SIZE)) > 0) + { + ostream.write(buffer, 0, bytesRead); + } + istream.close(); // IOException + ostream.close(); // IOException + if (p.debugMode) + Log.d(TAG, "Sent " + p.httpMethod + " with file of size " + file.length()); + } + } + // GET data from the server or receive response body + p.httpResponseCode = connection.getResponseCode(); + if (p.debugMode) + Log.d(TAG, "Received HTTP " + p.httpResponseCode + " from server."); + + if (p.httpResponseCode >= 300 && p.httpResponseCode < 400) + p.receivedUrl = connection.getHeaderField("Location"); + else + p.receivedUrl = connection.getURL().toString(); + + p.contentType = connection.getContentType(); + p.contentEncoding = connection.getContentEncoding(); + final Map> headers = connection.getHeaderFields(); + if (headers != null && headers.containsKey("Set-Cookie")) + { + // Multiple Set-Cookie headers are normalized in C++ code. + android.text.TextUtils.join(", ", headers.get("Set-Cookie")); + } + // This implementation receives any data only if we have HTTP::OK (200). + if (p.httpResponseCode == HttpURLConnection.HTTP_OK) + { + OutputStream ostream; + if (!TextUtils.isEmpty(p.outputFilePath)) + ostream = new BufferedOutputStream(new FileOutputStream(p.outputFilePath), STREAM_BUFFER_SIZE); + else + ostream = new ByteArrayOutputStream(STREAM_BUFFER_SIZE); + // TODO(AlexZ): Add HTTP resume support in the future for partially downloaded files + final BufferedInputStream istream = new BufferedInputStream(connection.getInputStream(), STREAM_BUFFER_SIZE); + final byte[] buffer = new byte[STREAM_BUFFER_SIZE]; + // gzip encoding is transparently enabled and we can't use Content-Length for + // body reading if server has gzipped it. + int bytesRead; + while ((bytesRead = istream.read(buffer, 0, STREAM_BUFFER_SIZE)) > 0) + { + // Read everything if Content-Length is not known in advance. + ostream.write(buffer, 0, bytesRead); + } + istream.close(); // IOException + ostream.close(); // IOException + if (ostream instanceof ByteArrayOutputStream) + p.data = ((ByteArrayOutputStream) ostream).toByteArray(); + } + } + finally + { + if (connection != null) + connection.disconnect(); + } + return p; + } + + public static class Params + { + public String url; + // Can be different from url in case of redirects. + public String receivedUrl; + public String httpMethod; + // Should be specified for any request whose method allows non-empty body. + // On return, contains received Content-Type or null. + public String contentType; + // Can be specified for any request whose method allows non-empty body. + // On return, contains received Content-Encoding or null. + public String contentEncoding; + public byte[] data; + // Send from input file if specified instead of data. + public String inputFilePath; + // Received data is stored here if not null or in data otherwise. + public String outputFilePath; + // Optionally client can override default HTTP User-Agent. + public String userAgent; + public String basicAuthUser; + public String basicAuthPassword; + public String cookies; + public int httpResponseCode = -1; + public boolean debugMode = false; + public boolean followRedirects = true; + + // Simple GET request constructor. + public Params(String url) + { + this.url = url; + httpMethod = "GET"; + } + + public void setData(byte[] data, String contentType, String httpMethod) + { + this.data = data; + this.contentType = contentType; + this.httpMethod = httpMethod; + } + + public void setInputFilePath(String path, String contentType, String httpMethod) + { + this.inputFilePath = path; + this.contentType = contentType; + this.httpMethod = httpMethod; + } + } +} diff --git a/editor/config_loader.cpp b/editor/config_loader.cpp index 69da08e3b2..b5ef7241a4 100644 --- a/editor/config_loader.cpp +++ b/editor/config_loader.cpp @@ -9,12 +9,12 @@ #include "std/fstream.hpp" #include "std/iterator.hpp" -#include "3party/Alohalytics/src/http_client.h" +#include "platform/http_client.hpp" #include "3party/pugixml/src/pugixml.hpp" namespace { -using alohalytics::HTTPClientPlatformWrapper; +using platform::HttpClient; auto const kConfigFileName = "editor.config"; auto const kHashFileName = "editor.config.hash"; @@ -28,20 +28,20 @@ string GetHashFilePath() { return GetPlatform().WritablePathForFile(kHashFileNam string RunSimpleHttpRequest(string const & url) { - HTTPClientPlatformWrapper request(url); + HttpClient request(url); bool result = false; try { - result = request.RunHTTPRequest(); + result = request.RunHttpRequest(); } catch (runtime_error const & ex) { - LOG(LWARNING, ("Exception from HTTPClientPlatformWrapper::RunHTTPRequest, message: ", ex.what())); + LOG(LWARNING, ("Exception from HttpClient::RunHttpRequest, message: ", ex.what())); } - if (result && !request.was_redirected() && request.error_code() == 200) // 200 - http status OK + if (result && !request.WasRedirected() && request.ErrorCode() == 200) // 200 - http status OK { - return request.server_response(); + return request.ServerResponse(); } return {}; diff --git a/editor/osm_auth.cpp b/editor/osm_auth.cpp index 01995bbf31..b6a27d10b0 100644 --- a/editor/osm_auth.cpp +++ b/editor/osm_auth.cpp @@ -1,5 +1,7 @@ #include "editor/osm_auth.hpp" +#include "platform/http_client.hpp" + #include "coding/url_encode.hpp" #include "base/assert.hpp" @@ -12,9 +14,8 @@ #include "private.h" #include "3party/liboauthcpp/include/liboauthcpp/liboauthcpp.h" -#include "3party/Alohalytics/src/http_client.h" -using alohalytics::HTTPClientPlatformWrapper; +using platform::HttpClient; namespace osm { @@ -52,18 +53,6 @@ string BuildPostRequest(map const & params) } return result; } - -// TODO(AlexZ): DebugPrint doesn't detect this overload. Fix it. -string DP(alohalytics::HTTPClientPlatformWrapper const & request) -{ - string str = "HTTP " + strings::to_string(request.error_code()) + " url [" + request.url_requested() + "]"; - if (request.was_redirected()) - str += " was redirected to [" + request.url_received() + "]"; - if (!request.server_response().empty()) - str += " response: " + request.server_response(); - return str; -} - } // namespace // static @@ -132,28 +121,28 @@ bool OsmOAuth::IsAuthorized() const noexcept{ return IsValid(m_tokenKeySecret); OsmOAuth::SessionID OsmOAuth::FetchSessionId(string const & subUrl) const { string const url = m_baseUrl + subUrl + "?cookie_test=true"; - HTTPClientPlatformWrapper request(url); - if (!request.RunHTTPRequest()) + HttpClient request(url); + if (!request.RunHttpRequest()) MYTHROW(NetworkError, ("FetchSessionId Network error while connecting to", url)); - if (request.was_redirected()) - MYTHROW(UnexpectedRedirect, ("Redirected to", request.url_received(), "from", url)); - if (request.error_code() != HTTP::OK) - MYTHROW(FetchSessionIdError, (DP(request))); + if (request.WasRedirected()) + MYTHROW(UnexpectedRedirect, ("Redirected to", request.UrlReceived(), "from", url)); + if (request.ErrorCode() != HTTP::OK) + MYTHROW(FetchSessionIdError, (DebugPrint(request))); - SessionID const sid = { request.combined_cookies(), FindAuthenticityToken(request.server_response()) }; + SessionID const sid = { request.CombinedCookies(), FindAuthenticityToken(request.ServerResponse()) }; if (sid.m_cookies.empty() || sid.m_token.empty()) - MYTHROW(FetchSessionIdError, ("Cookies and/or token are empty for request", DP(request))); + MYTHROW(FetchSessionIdError, ("Cookies and/or token are empty for request", DebugPrint(request))); return sid; } void OsmOAuth::LogoutUser(SessionID const & sid) const { - HTTPClientPlatformWrapper request(m_baseUrl + "/logout"); - request.set_cookies(sid.m_cookies); - if (!request.RunHTTPRequest()) - MYTHROW(NetworkError, ("LogoutUser Network error while connecting to", request.url_requested())); - if (request.error_code() != HTTP::OK) - MYTHROW(LogoutUserError, (DP(request))); + HttpClient request(m_baseUrl + "/logout"); + request.SetCookies(sid.m_cookies); + if (!request.RunHttpRequest()) + MYTHROW(NetworkError, ("LogoutUser Network error while connecting to", request.UrlRequested())); + if (request.ErrorCode() != HTTP::OK) + MYTHROW(LogoutUserError, (DebugPrint(request))); } bool OsmOAuth::LoginUserPassword(string const & login, string const & password, SessionID const & sid) const @@ -166,51 +155,51 @@ bool OsmOAuth::LoginUserPassword(string const & login, string const & password, {"commit", "Login"}, {"authenticity_token", sid.m_token} }; - HTTPClientPlatformWrapper request(m_baseUrl + "/login"); - request.set_body_data(BuildPostRequest(params), "application/x-www-form-urlencoded") - .set_cookies(sid.m_cookies) - .set_handle_redirects(false); - if (!request.RunHTTPRequest()) - MYTHROW(NetworkError, ("LoginUserPassword Network error while connecting to", request.url_requested())); + HttpClient request(m_baseUrl + "/login"); + request.SetBodyData(BuildPostRequest(params), "application/x-www-form-urlencoded") + .SetCookies(sid.m_cookies) + .SetHandleRedirects(false); + if (!request.RunHttpRequest()) + MYTHROW(NetworkError, ("LoginUserPassword Network error while connecting to", request.UrlRequested())); // At the moment, automatic redirects handling is buggy on Androids < 4.4. // set_handle_redirects(false) works only for Android code, iOS one (and curl) still automatically follow all redirects. - if (request.error_code() != HTTP::OK && request.error_code() != HTTP::Found) - MYTHROW(LoginUserPasswordServerError, (DP(request))); + if (request.ErrorCode() != HTTP::OK && request.ErrorCode() != HTTP::Found) + MYTHROW(LoginUserPasswordServerError, (DebugPrint(request))); // Not redirected page is a 100% signal that login and/or password are invalid. - if (!request.was_redirected()) + if (!request.WasRedirected()) return false; // Check if we were redirected to some 3rd party site. - if (request.url_received().find(m_baseUrl) != 0) - MYTHROW(UnexpectedRedirect, (DP(request))); + if (request.UrlReceived().find(m_baseUrl) != 0) + MYTHROW(UnexpectedRedirect, (DebugPrint(request))); // m_baseUrl + "/login" means login and/or password are invalid. - return request.server_response().find("/login") == string::npos; + return request.ServerResponse().find("/login") == string::npos; } bool OsmOAuth::LoginSocial(string const & callbackPart, string const & socialToken, SessionID const & sid) const { string const url = m_baseUrl + callbackPart + socialToken; - HTTPClientPlatformWrapper request(url); - request.set_cookies(sid.m_cookies) - .set_handle_redirects(false); - if (!request.RunHTTPRequest()) - MYTHROW(NetworkError, ("LoginSocial Network error while connecting to", request.url_requested())); - if (request.error_code() != HTTP::OK && request.error_code() != HTTP::Found) - MYTHROW(LoginSocialServerError, (DP(request))); + HttpClient request(url); + request.SetCookies(sid.m_cookies) + .SetHandleRedirects(false); + if (!request.RunHttpRequest()) + MYTHROW(NetworkError, ("LoginSocial Network error while connecting to", request.UrlRequested())); + if (request.ErrorCode() != HTTP::OK && request.ErrorCode() != HTTP::Found) + MYTHROW(LoginSocialServerError, (DebugPrint(request))); // Not redirected page is a 100% signal that social login has failed. - if (!request.was_redirected()) + if (!request.WasRedirected()) return false; // Check if we were redirected to some 3rd party site. - if (request.url_received().find(m_baseUrl) != 0) - MYTHROW(UnexpectedRedirect, (DP(request))); + if (request.UrlReceived().find(m_baseUrl) != 0) + MYTHROW(UnexpectedRedirect, (DebugPrint(request))); // m_baseUrl + "/login" means login and/or password are invalid. - return request.server_response().find("/login") == string::npos; + return request.ServerResponse().find("/login") == string::npos; } // Fakes a buttons press to automatically accept requested permissions. @@ -227,18 +216,18 @@ string OsmOAuth::SendAuthRequest(string const & requestTokenKey, SessionID const {"allow_write_notes", "yes"}, {"commit", "Save changes"} }; - HTTPClientPlatformWrapper request(m_baseUrl + "/oauth/authorize"); - request.set_body_data(BuildPostRequest(params), "application/x-www-form-urlencoded") - .set_cookies(sid.m_cookies) - .set_handle_redirects(false); - if (!request.RunHTTPRequest()) - MYTHROW(NetworkError, ("SendAuthRequest Network error while connecting to", request.url_requested())); + HttpClient request(m_baseUrl + "/oauth/authorize"); + request.SetBodyData(BuildPostRequest(params), "application/x-www-form-urlencoded") + .SetCookies(sid.m_cookies) + .SetHandleRedirects(false); + if (!request.RunHttpRequest()) + MYTHROW(NetworkError, ("SendAuthRequest Network error while connecting to", request.UrlRequested())); - string const callbackURL = request.url_received(); + string const callbackURL = request.UrlReceived(); string const vKey = "oauth_verifier="; auto const pos = callbackURL.find(vKey); if (pos == string::npos) - MYTHROW(SendAuthRequestError, ("oauth_verifier is not found", DP(request))); + MYTHROW(SendAuthRequestError, ("oauth_verifier is not found", DebugPrint(request))); auto const end = callbackURL.find("&", pos); return callbackURL.substr(pos + vKey.length(), end == string::npos ? end : end - pos - vKey.length()); @@ -250,16 +239,16 @@ TRequestToken OsmOAuth::FetchRequestToken() const 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()) - MYTHROW(NetworkError, ("FetchRequestToken Network error while connecting to", request.url_requested())); - if (request.error_code() != HTTP::OK) - MYTHROW(FetchRequestTokenServerError, (DP(request))); - if (request.was_redirected()) - MYTHROW(UnexpectedRedirect, ("Redirected to", request.url_received(), "from", request.url_requested())); + HttpClient request(requestTokenUrl + "?" + requestTokenQuery); + if (!request.RunHttpRequest()) + MYTHROW(NetworkError, ("FetchRequestToken Network error while connecting to", request.UrlRequested())); + if (request.ErrorCode() != HTTP::OK) + MYTHROW(FetchRequestTokenServerError, (DebugPrint(request))); + if (request.WasRedirected()) + MYTHROW(UnexpectedRedirect, ("Redirected to", request.UrlReceived(), "from", request.UrlRequested())); // Throws std::runtime_error. - OAuth::Token const reqToken = OAuth::Token::extract(request.server_response()); + OAuth::Token const reqToken = OAuth::Token::extract(request.ServerResponse()); return { reqToken.key(), reqToken.secret() }; } @@ -270,15 +259,15 @@ TKeySecret OsmOAuth::FinishAuthorization(TRequestToken const & requestToken, str OAuth::Client oauth(&consumer, &reqToken); string const accessTokenUrl = m_baseUrl + "/oauth/access_token"; string const queryString = oauth.getURLQueryString(OAuth::Http::Get, accessTokenUrl, "", true); - HTTPClientPlatformWrapper request(accessTokenUrl + "?" + queryString); - if (!request.RunHTTPRequest()) - MYTHROW(NetworkError, ("FinishAuthorization Network error while connecting to", request.url_requested())); - if (request.error_code() != HTTP::OK) - MYTHROW(FinishAuthorizationServerError, (DP(request))); - if (request.was_redirected()) - MYTHROW(UnexpectedRedirect, ("Redirected to", request.url_received(), "from", request.url_requested())); + HttpClient request(accessTokenUrl + "?" + queryString); + if (!request.RunHttpRequest()) + MYTHROW(NetworkError, ("FinishAuthorization Network error while connecting to", request.UrlRequested())); + if (request.ErrorCode() != HTTP::OK) + MYTHROW(FinishAuthorizationServerError, (DebugPrint(request))); + if (request.WasRedirected()) + MYTHROW(UnexpectedRedirect, ("Redirected to", request.UrlReceived(), "from", request.UrlRequested())); - OAuth::KeyValuePairs const responseData = OAuth::ParseKeyValuePairs(request.server_response()); + OAuth::KeyValuePairs const responseData = OAuth::ParseKeyValuePairs(request.ServerResponse()); // Throws std::runtime_error. OAuth::Token const accessToken = OAuth::Token::extract(responseData); return { accessToken.key(), accessToken.secret() }; @@ -350,16 +339,16 @@ bool OsmOAuth::ResetPassword(string const & email) const {"authenticity_token", sid.m_token}, {"commit", "Reset password"} }; - HTTPClientPlatformWrapper request(m_baseUrl + kForgotPasswordUrlPart); - request.set_body_data(BuildPostRequest(params), "application/x-www-form-urlencoded"); - request.set_cookies(sid.m_cookies); + HttpClient request(m_baseUrl + kForgotPasswordUrlPart); + request.SetBodyData(BuildPostRequest(params), "application/x-www-form-urlencoded"); + request.SetCookies(sid.m_cookies); - if (!request.RunHTTPRequest()) - MYTHROW(NetworkError, ("ResetPassword Network error while connecting to", request.url_requested())); - if (request.error_code() != HTTP::OK) - MYTHROW(ResetPasswordServerError, (DP(request))); + if (!request.RunHttpRequest()) + MYTHROW(NetworkError, ("ResetPassword Network error while connecting to", request.UrlRequested())); + if (request.ErrorCode() != HTTP::OK) + MYTHROW(ResetPasswordServerError, (DebugPrint(request))); - if (request.was_redirected() && request.url_received().find(m_baseUrl) != string::npos) + if (request.WasRedirected() && request.UrlReceived().find(m_baseUrl) != string::npos) return true; return false; } @@ -391,27 +380,27 @@ OsmOAuth::Response OsmOAuth::Request(string const & method, string const & httpM if (qPos != string::npos) url = url.substr(0, qPos); - HTTPClientPlatformWrapper request(url + "?" + query); + HttpClient request(url + "?" + query); if (httpMethod != "GET") - request.set_body_data(body, "application/xml", httpMethod); - if (!request.RunHTTPRequest()) + request.SetBodyData(body, "application/xml", httpMethod); + if (!request.RunHttpRequest()) MYTHROW(NetworkError, ("Request Network error while connecting to", url)); - if (request.was_redirected()) - MYTHROW(UnexpectedRedirect, ("Redirected to", request.url_received(), "from", url)); + if (request.WasRedirected()) + MYTHROW(UnexpectedRedirect, ("Redirected to", request.UrlReceived(), "from", url)); - return Response(request.error_code(), request.server_response()); + return Response(request.ErrorCode(), request.ServerResponse()); } OsmOAuth::Response OsmOAuth::DirectRequest(string const & method, bool api) const { string const url = api ? m_apiUrl + kApiVersion + method : m_baseUrl + method; - HTTPClientPlatformWrapper request(url); - if (!request.RunHTTPRequest()) + HttpClient request(url); + if (!request.RunHttpRequest()) MYTHROW(NetworkError, ("DirectRequest Network error while connecting to", url)); - if (request.was_redirected()) - MYTHROW(UnexpectedRedirect, ("Redirected to", request.url_received(), "from", url)); + if (request.WasRedirected()) + MYTHROW(UnexpectedRedirect, ("Redirected to", request.UrlReceived(), "from", url)); - return Response(request.error_code(), request.server_response()); + return Response(request.ErrorCode(), request.ServerResponse()); } string DebugPrint(OsmOAuth::Response const & code) diff --git a/editor/user_stats.cpp b/editor/user_stats.cpp index 17e505dd9a..3a71ea9884 100644 --- a/editor/user_stats.cpp +++ b/editor/user_stats.cpp @@ -1,5 +1,6 @@ #include "editor/user_stats.hpp" +#include "platform/http_client.hpp" #include "platform/platform.hpp" #include "platform/settings.hpp" @@ -9,11 +10,8 @@ #include "base/thread.hpp" #include "base/timer.hpp" -#include "3party/Alohalytics/src/http_client.h" #include "3party/pugixml/src/pugixml.hpp" -using TRequest = alohalytics::HTTPClientPlatformWrapper; - namespace { string const kUserStatsUrl = "https://editor-api.maps.me/user?format=xml"; @@ -91,21 +89,21 @@ bool UserStatsLoader::Update(string const & userName) } auto const url = kUserStatsUrl + "&name=" + UrlEncode(userName); - TRequest request(url); + platform::HttpClient request(url); - if (!request.RunHTTPRequest()) + if (!request.RunHttpRequest()) { LOG(LWARNING, ("Network error while connecting to", url)); return false; } - if (request.error_code() != 200) + if (request.ErrorCode() != 200) { - LOG(LWARNING, ("Server returned", request.error_code(), "for url", url)); + LOG(LWARNING, ("Server returned", request.ErrorCode(), "for url", url)); return false; } - auto const response = request.server_response(); + auto const response = request.ServerResponse(); pugi::xml_document document; if (!document.load_buffer(response.data(), response.size())) diff --git a/platform/apple_location_service.mm b/platform/apple_location_service.mm index c4568c26b1..0ec2ae0dc8 100644 --- a/platform/apple_location_service.mm +++ b/platform/apple_location_service.mm @@ -34,12 +34,6 @@ public: m_locationManager.desiredAccuracy = kCLLocationAccuracyBest; } - virtual ~AppleLocationService() - { - [m_locationManager release]; - [m_objCppWrapper release]; - } - void OnLocationUpdate(GpsInfo const & info) { m_observer.OnLocationUpdated(info); diff --git a/platform/http_client.cpp b/platform/http_client.cpp new file mode 100644 index 0000000000..dcd1727b69 --- /dev/null +++ b/platform/http_client.cpp @@ -0,0 +1,165 @@ +#include "platform/http_client.hpp" + +#include "base/string_utils.hpp" + +#include "std/sstream.hpp" + +namespace platform +{ +HttpClient & HttpClient::SetDebugMode(bool debug_mode) +{ + m_debugMode = debug_mode; + return *this; +} + +HttpClient & HttpClient::SetUrlRequested(string const & url) +{ + m_urlRequested = url; + return *this; +} + +HttpClient & HttpClient::SetHttpMethod(string const & method) +{ + m_httpMethod = method; + return *this; +} + +HttpClient & HttpClient::SetBodyFile(string const & body_file, string const & content_type, + string const & http_method /* = "POST" */, + string const & content_encoding /* = "" */) +{ + m_inputFile = body_file; + m_bodyData.clear(); + m_contentType = content_type; + m_httpMethod = http_method; + m_contentEncoding = content_encoding; + return *this; +} + +HttpClient & HttpClient::SetReceivedFile(string const & received_file) +{ + m_outputFile = received_file; + return *this; +} + +HttpClient & HttpClient::SetUserAgent(string const & user_agent) +{ + m_userAgent = user_agent; + return *this; +} + +HttpClient & HttpClient::SetUserAndPassword(string const & user, string const & password) +{ + m_basicAuthUser = user; + m_basicAuthPassword = password; + return *this; +} + +HttpClient & HttpClient::SetCookies(string const & cookies) +{ + m_cookies = cookies; + return *this; +} + +HttpClient & HttpClient::SetHandleRedirects(bool handle_redirects) +{ + m_handleRedirects = handle_redirects; + return *this; +} + +string const & HttpClient::UrlRequested() const +{ + return m_urlRequested; +} + +string const & HttpClient::UrlReceived() const +{ + return m_urlReceived; +} + +bool HttpClient::WasRedirected() const +{ + return m_urlRequested != m_urlReceived; +} + +int HttpClient::ErrorCode() const +{ + return m_errorCode; +} + +string const & HttpClient::ServerResponse() const +{ + return m_serverResponse; +} + +string const & HttpClient::HttpMethod() const +{ + return m_httpMethod; +} + +string HttpClient::CombinedCookies() const +{ + if (m_serverCookies.empty()) + return m_cookies; + + if (m_cookies.empty()) + return m_serverCookies; + + return m_serverCookies + "; " + m_cookies; +} + +string HttpClient::CookieByName(string name) const +{ + string const str = CombinedCookies(); + name += "="; + auto const cookie = str.find(name); + auto const eq = cookie + name.size(); + if (cookie != string::npos && str.size() > eq) + return str.substr(eq, str.find(';', eq) - eq); + + return {}; +} + +// static +string HttpClient::NormalizeServerCookies(string && cookies) +{ + istringstream is(cookies); + string str, result; + + // Split by ", ". Can have invalid tokens here, expires= can also contain a comma. + while (getline(is, str, ',')) + { + size_t const leading = str.find_first_not_of(" "); + if (leading != string::npos) + str.substr(leading).swap(str); + + // In the good case, we have '=' and it goes before any ' '. + auto const eq = str.find('='); + if (eq == string::npos) + continue; // It's not a cookie: no valid key value pair. + + auto const sp = str.find(' '); + if (sp != string::npos && eq > sp) + continue; // It's not a cookie: comma in expires date. + + // Insert delimiter. + if (!result.empty()) + result.append("; "); + + // Read cookie itself. + result.append(str, 0, str.find(";")); + } + return result; +} + +string DebugPrint(HttpClient const & request) +{ + ostringstream ostr; + ostr << "HTTP " << request.ErrorCode() << " url [" << request.UrlRequested() << "]"; + if (request.WasRedirected()) + ostr << " was redirected to [" << request.UrlReceived() << "]"; + if (!request.ServerResponse().empty()) + ostr << " response: " << request.ServerResponse(); + return ostr.str(); +} +} diff --git a/platform/http_client.hpp b/platform/http_client.hpp new file mode 100644 index 0000000000..11f77084d4 --- /dev/null +++ b/platform/http_client.hpp @@ -0,0 +1,130 @@ +/******************************************************************************* +The MIT License (MIT) + +Copyright (c) 2015 Alexander Zolotarev from Minsk, Belarus + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*******************************************************************************/ +#pragma once + +#include "base/macros.hpp" + +#include "std/string.hpp" + +namespace platform +{ +class HttpClient +{ +public: + static auto constexpr kNoError = -1; + + HttpClient() = default; + HttpClient(string const & url) : m_urlRequested(url) {} + + // Synchronous (blocking) call, should be implemented for each platform + // @returns true if connection was made and server returned something (200, 404, etc.). + // @note Implementations should transparently support all needed HTTP redirects. + // Implemented for each platform. + bool RunHttpRequest(); + + // Shared methods for all platforms, implemented at http_client.cpp + HttpClient & SetDebugMode(bool debug_mode); + HttpClient & SetUrlRequested(string const & url); + HttpClient & SetHttpMethod(string const & method); + // This method is mutually exclusive with set_body_data(). + HttpClient & SetBodyFile(string const & body_file, string const & content_type, + string const & http_method = "POST", + string const & content_encoding = ""); + // If set, stores server reply in file specified. + HttpClient & SetReceivedFile(string const & received_file); + HttpClient & SetUserAgent(string const & user_agent); + // This method is mutually exclusive with set_body_file(). + template + HttpClient & SetBodyData(StringT && body_data, string const & content_type, + string const & http_method = "POST", + string const & content_encoding = "") + { + m_bodyData = forward(body_data); + m_inputFile.clear(); + m_contentType = content_type; + m_httpMethod = http_method; + m_contentEncoding = content_encoding; + return *this; + } + // HTTP Basic Auth. + HttpClient & SetUserAndPassword(string const & user, string const & password); + // Set HTTP Cookie header. + HttpClient & SetCookies(string const & cookies); + // When set to true (default), clients never get 3XX codes from servers, redirects are handled automatically. + // TODO: "false" is now supported on Android only. + HttpClient & SetHandleRedirects(bool handle_redirects); + + string const & UrlRequested() const; + // @returns empty string in the case of error + string const & UrlReceived() const; + bool WasRedirected() const; + // Mix of HTTP errors (in case of successful connection) and system-dependent error codes, + // in the simplest success case use 'if (200 == client.error_code())' // 200 means OK in HTTP + int ErrorCode() const; + string const & ServerResponse() const; + string const & HttpMethod() const; + // Pass this getter's value to the set_cookies() method for easier cookies support in the next request. + string CombinedCookies() const; + // Returns cookie value or empty string if it's not present. + string CookieByName(string name) const; + +private: + // Internal helper to convert cookies like this: + // "first=value1; expires=Mon, 26-Dec-2016 12:12:32 GMT; path=/, second=value2; path=/, third=value3; " + // into this: + // "first=value1; second=value2; third=value3" + static string NormalizeServerCookies(string && cookies); + + string m_urlRequested; + // Contains final content's url taking redirects (if any) into an account. + string m_urlReceived; + int m_errorCode = kNoError; + string m_inputFile; + // Used instead of server_reply_ if set. + string m_outputFile; + // Data we received from the server if output_file_ wasn't initialized. + string m_serverResponse; + string m_contentType; + string m_contentTypeReceived; + string m_contentEncoding; + string m_contentEncodingReceived; + string m_userAgent; + string m_bodyData; + string m_httpMethod = "GET"; + string m_basicAuthUser; + string m_basicAuthPassword; + // All Set-Cookie values from server response combined in a Cookie format: + // cookie1=value1; cookie2=value2 + // TODO(AlexZ): Support encoding and expiration/path/domains etc. + string m_serverCookies; + // Cookies set by the client before request is run. + string m_cookies; + bool m_debugMode = false; + bool m_handleRedirects = true; + + DISALLOW_COPY_AND_MOVE(HttpClient); +}; + +string DebugPrint(HttpClient const & request); +} // namespace platform diff --git a/platform/http_client_apple.mm b/platform/http_client_apple.mm new file mode 100644 index 0000000000..4d063c08c2 --- /dev/null +++ b/platform/http_client_apple.mm @@ -0,0 +1,158 @@ +/******************************************************************************* +The MIT License (MIT) + +Copyright (c) 2015 Alexander Zolotarev from Minsk, Belarus + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*******************************************************************************/ + +#if ! __has_feature(objc_arc) +#error This file must be compiled with ARC. Either turn on ARC for the project or use -fobjc-arc flag +#endif + +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import + +#include // TARGET_OS_IPHONE +#if (TARGET_OS_IPHONE > 0) // Works for all iOS devices, including iPad. +extern NSString * gBrowserUserAgent; +#endif + +#include "platform/http_client.hpp" + +#include "base/logging.hpp" + +namespace platform +{ +// If we try to upload our data from the background fetch handler on iOS, we have ~30 seconds to do that gracefully. +static const double kTimeoutInSeconds = 24.0; + +// TODO(AlexZ): Rewrite to use async implementation for better redirects handling and ability to cancel request from destructor. +bool HttpClient::RunHttpRequest() +{ + NSMutableURLRequest * request = [NSMutableURLRequest requestWithURL: + [NSURL URLWithString:[NSString stringWithUTF8String:m_urlRequested.c_str()]] + cachePolicy:NSURLRequestReloadIgnoringLocalCacheData timeoutInterval:kTimeoutInSeconds]; + // We handle cookies manually. + request.HTTPShouldHandleCookies = NO; + + request.HTTPMethod = [NSString stringWithUTF8String:m_httpMethod.c_str()]; + if (!m_contentType.empty()) + [request setValue:[NSString stringWithUTF8String:m_contentType.c_str()] forHTTPHeaderField:@"Content-Type"]; + + if (!m_contentEncoding.empty()) + [request setValue:[NSString stringWithUTF8String:m_contentEncoding.c_str()] forHTTPHeaderField:@"Content-Encoding"]; + + if (!m_userAgent.empty()) + [request setValue:[NSString stringWithUTF8String:m_userAgent.c_str()] forHTTPHeaderField:@"User-Agent"]; + + if (!m_cookies.empty()) + [request setValue:[NSString stringWithUTF8String:m_cookies.c_str()] forHTTPHeaderField:@"Cookie"]; +#if (TARGET_OS_IPHONE > 0) + else if (gBrowserUserAgent) + [request setValue:gBrowserUserAgent forHTTPHeaderField:@"User-Agent"]; +#endif // TARGET_OS_IPHONE + + if (!m_basicAuthUser.empty()) + { + NSData * loginAndPassword = [[NSString stringWithUTF8String:(m_basicAuthUser + ":" + m_basicAuthPassword).c_str()] dataUsingEncoding:NSUTF8StringEncoding]; + [request setValue:[NSString stringWithFormat:@"Basic %@", [loginAndPassword base64EncodedStringWithOptions:0]] forHTTPHeaderField:@"Authorization"]; + } + + if (!m_bodyData.empty()) + { + request.HTTPBody = [NSData dataWithBytes:m_bodyData.data() length:m_bodyData.size()]; + if (m_debugMode) + LOG(LINFO, ("Uploading buffer of size", m_bodyData.size(), "bytes")); + } + else if (!m_inputFile.empty()) + { + NSError * err = nil; + NSString * path = [NSString stringWithUTF8String:m_inputFile.c_str()]; + const unsigned long long file_size = [[NSFileManager defaultManager] attributesOfItemAtPath:path error:&err].fileSize; + if (err) + { + m_errorCode = static_cast(err.code); + if (m_debugMode) + LOG(LERROR, ("Error: ", m_errorCode, [err.localizedDescription UTF8String])); + + return false; + } + request.HTTPBodyStream = [NSInputStream inputStreamWithFileAtPath:path]; + [request setValue:[NSString stringWithFormat:@"%llu", file_size] forHTTPHeaderField:@"Content-Length"]; + if (m_debugMode) + LOG(LINFO, ("Uploading file", m_inputFile, file_size, "bytes")); + } + + NSHTTPURLResponse * response = nil; + NSError * err = nil; + NSData * url_data = [NSURLConnection sendSynchronousRequest:request returningResponse:&response error:&err]; + + if (response) + { + m_errorCode = static_cast(response.statusCode); + m_urlReceived = [response.URL.absoluteString UTF8String]; + + NSString * content = [response.allHeaderFields objectForKey:@"Content-Type"]; + if (content) + m_contentTypeReceived = std::move([content UTF8String]); + + NSString * encoding = [response.allHeaderFields objectForKey:@"Content-Encoding"]; + if (encoding) + m_contentEncodingReceived = std::move([encoding UTF8String]); + + // Apple merges all Set-Cookie fields into one NSDictionary key delimited by commas. + NSString * cookies = [response.allHeaderFields objectForKey:@"Set-Cookie"]; + if (cookies) + m_serverCookies = NormalizeServerCookies(std::move([cookies UTF8String])); + + if (url_data) + { + if (m_outputFile.empty()) + m_serverResponse.assign(reinterpret_cast(url_data.bytes), url_data.length); + else + [url_data writeToFile:[NSString stringWithUTF8String:m_outputFile.c_str()] atomically:YES]; + + } + return true; + } + // Request has failed if we are here. + // MacOSX/iOS-specific workaround for HTTP 401 error bug. + // @see bit.ly/1TrHlcS for more details. + if (err.code == NSURLErrorUserCancelledAuthentication) + { + m_errorCode = 401; + return true; + } + + m_errorCode = static_cast(err.code); + if (m_debugMode) + LOG(LERROR, ("Error: ", m_errorCode, ':', [err.localizedDescription UTF8String], "while connecting to", m_urlRequested)); + + return false; +} +} // namespace platform diff --git a/platform/http_client_curl.cpp b/platform/http_client_curl.cpp new file mode 100644 index 0000000000..5f2665e403 --- /dev/null +++ b/platform/http_client_curl.cpp @@ -0,0 +1,272 @@ +/******************************************************************************* + The MIT License (MIT) + + Copyright (c) 2014 Alexander Zolotarev from Minsk, Belarus + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. + *******************************************************************************/ +#include "platform/http_client.hpp" +#include "platform/platform.hpp" + +#include "base/assert.hpp" +#include "base/exception.hpp" +#include "base/logging.hpp" +#include "base/string_utils.hpp" + +#include "boost/uuid/uuid_generators.hpp" +#include "boost/uuid/uuid_io.hpp" + +#include "std/array.hpp" +#include "std/fstream.hpp" +#include "std/sstream.hpp" +#include "std/vector.hpp" + +#include // popen, tmpnam + +#ifdef _MSC_VER +#define popen _popen +#define pclose _pclose +#else +#include // close +#endif + +namespace +{ +DECLARE_EXCEPTION(PipeCallError, RootException); + +struct ScopedRemoveFile +{ + ScopedRemoveFile() = default; + explicit ScopedRemoveFile(string const & fileName) : m_fileName(fileName) {} + + ~ScopedRemoveFile() + { + if (!m_fileName.empty()) + std::remove(m_fileName.c_str()); + } + + std::string m_fileName; +}; + +static string ReadFileAsString(string const & filePath) +{ + ifstream ifs(filePath, ifstream::in); + if (!ifs.is_open()) + return {}; + + return {istreambuf_iterator(ifs), istreambuf_iterator()}; +} + + +string RunCurl(string const & cmd) +{ + FILE * pipe = ::popen(cmd.c_str(), "r"); + ASSERT(pipe, ()); + array arr; + string result; + size_t read; + do + { + read = ::fread(arr.data(), 1, arr.size(), pipe); + if (read > 0) + { + result.append(arr.data(), read); + } + } while (read == arr.size()); + + auto const err = ::pclose(pipe); + // Exception will be cought in RunHTTPRequest + if (err) + throw PipeCallError("", "Error " + strings::to_string(err) + " while calling " + cmd); + + return result; +} + +string GetTmpFileName() +{ + boost::uuids::random_generator gen; + boost::uuids::uuid u = gen(); + + stringstream ss; + ss << u; + + ASSERT(!ss.str().empty(), ()); + + return GetPlatform().TmpPathForFile(ss.str()); +} + +typedef vector> Headers; + +Headers ParseHeaders(string const & raw) +{ + istringstream stream(raw); + HeadersT headers; + string line; + while (getline(stream, line)) + { + auto const cr = line.rfind('\r'); + if (cr != string::npos) + line.erase(cr); + + auto const delims = line.find(": "); + if (delims != string::npos) + headers.push_back(make_pair(line.substr(0, delims), line.substr(delims + 2))); + } + return headers; +} + +bool WriteToFile(string const & fileName, string const & data) +{ + ofstream ofs(fileName); + if(!ofs.is_open()) + { + LOG(LERROR, ("Failed to write into a temporary file.")); + return false; + } + + ofs << data; + return true; +} +} // namespace +// Used as a test stub for basic HTTP client implementation. +// Make sure that you have curl installed in the PATH. +// TODO(AlexZ): Not a production-ready implementation. +namespace platform +{ +// Extract HTTP headers via temporary file with -D switch. +// HTTP status code is extracted from curl output (-w switches). +// Redirects are handled recursively. TODO(AlexZ): avoid infinite redirects loop. +bool HttpClient::RunHttpRequest() +{ + ScopedRemoveFile headers_deleter(GetTmpFileName()); + ScopedRemoveFile body_deleter; + ScopedRemoveFile received_file_deleter; + + string cmd = "curl -s -w '%{http_code}' -X " + m_httpMethod + " -D '" + headers_deleter.m_fileName + "' "; + + if (!m_contentType.empty()) + cmd += "-H 'Content-Type: " + m_contentType + "' "; + + if (!m_contentEncoding.empty()) + cmd += "-H 'Content-Encoding: " + m_contentEncoding + "' "; + + if (!m_basicAuthUser.empty()) + cmd += "-u '" + m_basicAuthUser + ":" + m_basicAuthPassword + "' "; + + if (!m_cookies.empty()) + cmd += "-b '" + m_cookies + "' "; + + if (!m_bodyData.empty()) + { + body_deleter.m_fileName = GetTmpFileName(); + // POST body through tmp file to avoid breaking command line. + if (!WriteToFile(body_deleter.m_fileName, m_bodyData)) + return false; + + // TODO(AlexZ): Correctly clean up this internal var to avoid client confusion. + m_inputFile = body_deleter.m_fileName; + } + // Content-Length is added automatically by curl. + if (!m_inputFile.empty()) + cmd += "--data-binary '@" + m_inputFile + "' "; + + // Use temporary file to receive data from server. + // If user has specified file name to save data, it is not temporary and is not deleted automatically. + string rfile = m_outputFile; + if (rfile.empty()) + { + rfile = GetTmpFileName(); + received_file_deleter.m_fileName = rfile; + } + + cmd += "-o " + rfile + strings::to_string(" ") + "'" + m_urlRequested + "'"; + + + if (m_debugMode) + { + LOG(LINFO, ("Executing", cmd)); + } + + try + { + m_errorCode = stoi(RunCurl(cmd)); + } + catch (RootException const & ex) + { + LOG(LERROR, (ex.Msg())); + return false; + } + + HeadersT const headers = ParseHeaders(ReadFileAsString(headers_deleter.m_fileName)); + for (auto const & header : headers) + { + if (header.first == "Set-Cookie") + { + m_serverCookies += header.second + ", "; + } + else if (header.first == "Content-Type") + { + m_contentTypeReceived = header.second; + } + else if (header.first == "Content-Encoding") + { + m_contentEncodingReceived = header.second; + } + else if (header.first == "Location") + { + m_urlReceived = header.second; + } + } + m_serverCookies = NormalizeServerCookies(move(m_serverCookies)); + + if (m_urlReceived.empty()) + { + m_urlReceived = m_urlRequested; + // Load body contents in final request only (skip redirects). + // Sometimes server can reply with empty body, and it's ok. + if (m_outputFile.empty()) + m_serverResponse = ReadFileAsString(rfile); + } + else + { + // Handle HTTP redirect. + // TODO(AlexZ): Should we check HTTP redirect code here? + if (m_debugMode) + LOG(LINFO, ("HTTP redirect", m_errorCode, "to", m_urlReceived)); + + HttpClient redirect(m_urlReceived); + redirect.SetCookies(CombinedCookies()); + + if (!redirect.RunHttpRequest()) + { + m_errorCode = -1; + return false; + } + + m_errorCode = redirect.ErrorCode(); + m_urlReceived = redirect.UrlReceived(); + m_serverCookies = move(redirect.m_serverCookies); + m_serverResponse = move(redirect.m_serverResponse); + m_contentTypeReceived = move(redirect.m_contentTypeReceived); + m_contentEncodingReceived = move(redirect.m_contentEncodingReceived); + } + + return true; +} +} // namespace platform diff --git a/platform/http_thread_apple.mm b/platform/http_thread_apple.mm index 5f6f54e208..423b35db91 100644 --- a/platform/http_thread_apple.mm +++ b/platform/http_thread_apple.mm @@ -23,12 +23,10 @@ static id downloadIndicator = nil; { LOG(LDEBUG, ("ID:", [self hash], "Connection is destroyed")); [m_connection cancel]; - [m_connection release]; #ifdef OMIM_OS_IPHONE [downloadIndicator enableStandby]; [downloadIndicator disableDownloadIndicator]; #endif - [super dealloc]; } - (void) cancel @@ -66,7 +64,6 @@ static id downloadIndicator = nil; val = [[NSString alloc] initWithFormat: @"bytes=%qi-", beg]; } [request addValue:val forHTTPHeaderField:@"Range"]; - [val release]; } if (!pb.empty()) @@ -94,7 +91,6 @@ static id downloadIndicator = nil; if (m_connection == 0) { LOG(LERROR, ("Can't create connection for", url)); - [self release]; return nil; } else @@ -226,7 +222,6 @@ HttpThread * CreateNativeHttpThread(string const & url, void DeleteNativeHttpThread(HttpThread * request) { [request cancel]; - [request release]; } } // namespace downloader diff --git a/platform/ios_video_timer.mm b/platform/ios_video_timer.mm index b60b234687..ec3077d40e 100644 --- a/platform/ios_video_timer.mm +++ b/platform/ios_video_timer.mm @@ -52,7 +52,6 @@ public: // So we should check EStopped flag in 'perform' to skip pending call. m_state = EStopped; [m_displayLink invalidate]; - [m_objCppWrapper release]; m_displayLink = 0; } } @@ -89,11 +88,6 @@ public: return self; } -- (void)dealloc -{ - [super dealloc]; -} - - (void)perform { m_timer->perform(); diff --git a/platform/platform.pro b/platform/platform.pro index 24ff9fb2d6..66f5e60527 100644 --- a/platform/platform.pro +++ b/platform/platform.pro @@ -47,7 +47,15 @@ INCLUDEPATH += $$ROOT_DIR/3party/jansson/src macx-*|iphone* { HEADERS += http_thread_apple.h - OBJECTIVE_SOURCES += http_thread_apple.mm + OBJECTIVE_SOURCES += \ + http_thread_apple.mm \ + http_client_apple.mm \ + + QMAKE_OBJECTIVE_CFLAGS += -fobjc-arc +} + +linux*|win* { + SOURCES += http_client_curl.cpp } !win32* { @@ -66,6 +74,7 @@ HEADERS += \ get_text_by_id.hpp \ http_request.hpp \ http_thread_callback.hpp \ + http_client.hpp \ local_country_file.hpp \ local_country_file_utils.hpp \ location.hpp \ @@ -83,6 +92,7 @@ SOURCES += \ country_file.cpp \ file_logging.cpp \ get_text_by_id.cpp \ + http_client.cpp \ http_request.cpp \ local_country_file.cpp \ local_country_file_utils.cpp \ diff --git a/platform/platform_ios.mm b/platform/platform_ios.mm index 7173a8f653..892b351795 100644 --- a/platform/platform_ios.mm +++ b/platform/platform_ios.mm @@ -37,8 +37,6 @@ Platform::Platform() { - NSAutoreleasePool * pool = [[NSAutoreleasePool alloc] init]; - m_isTablet = (UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPad); NSBundle * bundle = [NSBundle mainBundle]; @@ -65,8 +63,6 @@ Platform::Platform() UIDevice * device = [UIDevice currentDevice]; NSLog(@"Device: %@, SystemName: %@, SystemVersion: %@", device.model, device.systemName, device.systemVersion); - - [pool release]; } Platform::EError Platform::MkDir(string const & dirName) const diff --git a/platform/platform_mac.mm b/platform/platform_mac.mm index 85025bd62f..062e06b64b 100644 --- a/platform/platform_mac.mm +++ b/platform/platform_mac.mm @@ -19,8 +19,6 @@ Platform::Platform() { - NSAutoreleasePool * pool = [[NSAutoreleasePool alloc] init]; - // get resources directory path string const resourcesPath = [[[NSBundle mainBundle] resourcePath] UTF8String]; string const bundlePath = [[[NSBundle mainBundle] bundlePath] UTF8String]; @@ -78,8 +76,6 @@ Platform::Platform() LOG(LDEBUG, ("Writable Directory:", m_writableDir)); LOG(LDEBUG, ("Tmp Directory:", m_tmpDir)); LOG(LDEBUG, ("Settings Directory:", m_settingsDir)); - - [pool release]; } string Platform::UniqueClientId() const diff --git a/routing/online_cross_fetcher.cpp b/routing/online_cross_fetcher.cpp index 5602a82899..f114b985f1 100644 --- a/routing/online_cross_fetcher.cpp +++ b/routing/online_cross_fetcher.cpp @@ -62,9 +62,9 @@ OnlineCrossFetcher::OnlineCrossFetcher(string const & serverURL, ms::LatLon cons void OnlineCrossFetcher::Do() { m_mwmPoints.clear(); - if (m_request.RunHTTPRequest() && m_request.error_code() == 200 && !m_request.was_redirected()) - ParseResponse(m_request.server_response(), m_mwmPoints); + if (m_request.RunHttpRequest() && m_request.ErrorCode() == 200 && !m_request.WasRedirected()) + ParseResponse(m_request.ServerResponse(), m_mwmPoints); else - LOG(LWARNING, ("Can't get OSRM server response. Code: ", m_request.error_code())); + LOG(LWARNING, ("Can't get OSRM server response. Code: ", m_request.ErrorCode())); } } // namespace routing diff --git a/routing/online_cross_fetcher.hpp b/routing/online_cross_fetcher.hpp index 0951830ebe..7543af4efe 100644 --- a/routing/online_cross_fetcher.hpp +++ b/routing/online_cross_fetcher.hpp @@ -1,6 +1,6 @@ #pragma once -#include "3party/Alohalytics/src/http_client.h" +#include "platform/http_client.hpp" #include "geometry/point2d.hpp" #include "geometry/latlon.hpp" @@ -47,7 +47,7 @@ public: vector const & GetMwmPoints() { return m_mwmPoints; } private: - alohalytics::HTTPClientPlatformWrapper m_request; + platform::HttpClient m_request; vector m_mwmPoints; }; } diff --git a/xcode/platform/platform.xcodeproj/project.pbxproj b/xcode/platform/platform.xcodeproj/project.pbxproj index 2c07e21ba3..e9895b9fb2 100644 --- a/xcode/platform/platform.xcodeproj/project.pbxproj +++ b/xcode/platform/platform.xcodeproj/project.pbxproj @@ -7,6 +7,9 @@ objects = { /* Begin PBXBuildFile section */ + 3D30587D1D8320E4004AC712 /* http_client.hpp in Headers */ = {isa = PBXBuildFile; fileRef = 3D30587B1D8320E4004AC712 /* http_client.hpp */; }; + 3D30587F1D880910004AC712 /* http_client_apple.mm in Sources */ = {isa = PBXBuildFile; fileRef = 3D30587E1D880910004AC712 /* http_client_apple.mm */; }; + 3D97F64B1D9C05E800380945 /* http_client.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 3D97F64A1D9C05E800380945 /* http_client.cpp */; }; 56EB1EDC1C6B6E6C0022D831 /* file_logging.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 56EB1ED81C6B6E6C0022D831 /* file_logging.cpp */; }; 56EB1EDD1C6B6E6C0022D831 /* file_logging.hpp in Headers */ = {isa = PBXBuildFile; fileRef = 56EB1ED91C6B6E6C0022D831 /* file_logging.hpp */; }; 56EB1EDE1C6B6E6C0022D831 /* mwm_traits.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 56EB1EDA1C6B6E6C0022D831 /* mwm_traits.cpp */; }; @@ -95,6 +98,9 @@ /* End PBXBuildFile section */ /* Begin PBXFileReference section */ + 3D30587B1D8320E4004AC712 /* http_client.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = http_client.hpp; sourceTree = ""; }; + 3D30587E1D880910004AC712 /* http_client_apple.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = http_client_apple.mm; sourceTree = ""; }; + 3D97F64A1D9C05E800380945 /* http_client.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = http_client.cpp; sourceTree = ""; }; 56EB1ED81C6B6E6C0022D831 /* file_logging.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = file_logging.cpp; sourceTree = ""; }; 56EB1ED91C6B6E6C0022D831 /* file_logging.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = file_logging.hpp; sourceTree = ""; }; 56EB1EDA1C6B6E6C0022D831 /* mwm_traits.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = mwm_traits.cpp; sourceTree = ""; }; @@ -296,6 +302,9 @@ 6753437A1A3F5CF500A0A8C3 /* platform */ = { isa = PBXGroup; children = ( + 3D97F64A1D9C05E800380945 /* http_client.cpp */, + 3D30587E1D880910004AC712 /* http_client_apple.mm */, + 3D30587B1D8320E4004AC712 /* http_client.hpp */, 56EB1ED81C6B6E6C0022D831 /* file_logging.cpp */, 56EB1ED91C6B6E6C0022D831 /* file_logging.hpp */, 56EB1EDA1C6B6E6C0022D831 /* mwm_traits.cpp */, @@ -406,6 +415,7 @@ 675343D81A3F5D5A00A0A8C3 /* video_timer.hpp in Headers */, 6741250F1B4C00CC00A3E828 /* local_country_file.hpp in Headers */, 675343CF1A3F5D5A00A0A8C3 /* preferred_languages.hpp in Headers */, + 3D30587D1D8320E4004AC712 /* http_client.hpp in Headers */, 56EB1EDD1C6B6E6C0022D831 /* file_logging.hpp in Headers */, 6741250D1B4C00CC00A3E828 /* local_country_file_utils.hpp in Headers */, 675343DA1A3F5D5A00A0A8C3 /* wifi_info.hpp in Headers */, @@ -549,8 +559,10 @@ 67A2526A1BB40E100063F8A8 /* platform_qt.cpp in Sources */, 675343D91A3F5D5A00A0A8C3 /* wifi_info_windows.cpp in Sources */, 56EB1EDE1C6B6E6C0022D831 /* mwm_traits.cpp in Sources */, + 3D30587F1D880910004AC712 /* http_client_apple.mm in Sources */, 67247FFD1C60BD6500EDE56A /* writable_dir_changer.cpp in Sources */, 6741250C1B4C00CC00A3E828 /* local_country_file_utils.cpp in Sources */, + 3D97F64B1D9C05E800380945 /* http_client.cpp in Sources */, 67AB92EA1B7B3E9100AB5194 /* get_text_by_id.cpp in Sources */, 671C62061AE9014C00076BD0 /* measurement_utils.cpp in Sources */, 675343B61A3F5D5A00A0A8C3 /* http_request.cpp in Sources */, @@ -725,7 +737,7 @@ 675343841A3F5CF500A0A8C3 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { - CLANG_ENABLE_OBJC_ARC = NO; + CLANG_ENABLE_OBJC_ARC = YES; COMBINE_HIDPI_IMAGES = YES; EXECUTABLE_PREFIX = lib; GCC_ENABLE_CPP_EXCEPTIONS = YES; @@ -739,7 +751,7 @@ 675343851A3F5CF500A0A8C3 /* Release */ = { isa = XCBuildConfiguration; buildSettings = { - CLANG_ENABLE_OBJC_ARC = NO; + CLANG_ENABLE_OBJC_ARC = YES; COMBINE_HIDPI_IMAGES = YES; EXECUTABLE_PREFIX = lib; GCC_ENABLE_CPP_EXCEPTIONS = YES;