From c4a7e985b75966e02bd1316201e0fd441b658410 Mon Sep 17 00:00:00 2001 From: Arsentiy Milchakov Date: Mon, 26 Sep 2016 13:53:00 +0300 Subject: [PATCH] http client was copied from alohalytics to our code with small changes --- android/jni/Android.mk | 42 +-- .../jni/com/mapswithme/core/jni_helper.cpp | 8 +- .../jni/com/mapswithme/core/jni_helper.hpp | 2 + .../jni/com/mapswithme/util/HttpClient.cpp | 303 ++++++++++++++++++ .../src/com/mapswithme/util/HttpClient.java | 244 ++++++++++++++ editor/config_loader.cpp | 10 +- editor/osm_auth.cpp | 47 +-- editor/user_stats.cpp | 6 +- platform/apple_location_service.mm | 6 - platform/http_client.hpp | 236 ++++++++++++++ platform/http_client_apple.mm | 165 ++++++++++ platform/http_client_curl.cpp | 261 +++++++++++++++ platform/http_thread_apple.mm | 5 - platform/ios_video_timer.mm | 6 - platform/platform.pro | 11 +- platform/platform_ios.mm | 4 - platform/platform_mac.mm | 7 +- routing/online_cross_fetcher.cpp | 2 +- routing/online_cross_fetcher.hpp | 4 +- .../platform.xcodeproj/project.pbxproj | 12 +- 20 files changed, 1298 insertions(+), 83 deletions(-) create mode 100644 android/jni/com/mapswithme/util/HttpClient.cpp create mode 100644 android/src/com/mapswithme/util/HttpClient.java create mode 100644 platform/http_client.hpp create mode 100644 platform/http_client_apple.mm create mode 100644 platform/http_client_curl.cpp diff --git a/android/jni/Android.mk b/android/jni/Android.mk index 5d72693732..4c31824c50 100644 --- a/android/jni/Android.mk +++ b/android/jni/Android.mk @@ -61,48 +61,50 @@ 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/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/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..42b90697de --- /dev/null +++ b/android/jni/com/mapswithme/util/HttpClient.cpp @@ -0,0 +1,303 @@ +/******************************************************************************* +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 "platform/http_client.hpp" + +#include "base/logging.hpp" +#include "base/assert.hpp" + +#include "std/string.hpp" + +namespace +{ +template +unique_ptr MakeScopedPointer(T * ptr, D deleter) +{ + return unique_ptr(ptr, deleter); +} + +// 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; +}; +} // namespace + +#define CLEAR_AND_RETURN_FALSE_ON_EXCEPTION \ + if (env->ExceptionCheck()) { \ + env->ExceptionDescribe(); \ + env->ExceptionClear(); \ + return false; \ + } + +//*********************************************************************** +// Exported functions implementation +//*********************************************************************** +namespace platform +{ +bool HttpClient::RunHttpRequest() +{ + ScopedEnv env(jni::GetJVM()); + + if (!env) + return false; + + auto const deleter = [&env](jobject o) { env->DeleteLocalRef(o); }; + + // Create and fill request params. + auto const jniUrl = MakeScopedPointer(jni::ToJavaString(env.get(), m_urlRequested), deleter); + CLEAR_AND_RETURN_FALSE_ON_EXCEPTION + + static jmethodID const httpParamsConstructor = + env->GetMethodID(g_httpParamsClazz, "", "(Ljava/lang/String;)V"); + + auto const httpParamsObject = + MakeScopedPointer(env->NewObject(g_httpParamsClazz, httpParamsConstructor, jniUrl.get()), deleter); + 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()) + { + auto const jniPostData = MakeScopedPointer(env->NewByteArray(m_bodyData.size()), deleter); + 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.")); + static jfieldID const httpMethodField = + env->GetFieldID(g_httpParamsClazz, "httpMethod", "Ljava/lang/String;"); + { + const auto jniHttpMethod = MakeScopedPointer(jni::ToJavaString(env.get(), m_httpMethod), deleter); + CLEAR_AND_RETURN_FALSE_ON_EXCEPTION + + env->SetObjectField(httpParamsObject.get(), httpMethodField, jniHttpMethod.get()); + CLEAR_AND_RETURN_FALSE_ON_EXCEPTION + } + + static jfieldID const contentTypeField = + env->GetFieldID(g_httpParamsClazz, "contentType", "Ljava/lang/String;"); + if (!m_contentType.empty()) + { + auto const jniContentType = MakeScopedPointer(jni::ToJavaString(env.get(), m_contentType), deleter); + CLEAR_AND_RETURN_FALSE_ON_EXCEPTION + + env->SetObjectField(httpParamsObject.get(), contentTypeField, jniContentType.get()); + CLEAR_AND_RETURN_FALSE_ON_EXCEPTION + } + + static jfieldID const contentEncodingField = + env->GetFieldID(g_httpParamsClazz, "contentEncoding", "Ljava/lang/String;"); + if (!m_contentEncoding.empty()) + { + auto const jniContentEncoding = MakeScopedPointer(jni::ToJavaString(env.get(), m_contentEncoding), deleter); + CLEAR_AND_RETURN_FALSE_ON_EXCEPTION + + env->SetObjectField(httpParamsObject.get(), contentEncodingField, jniContentEncoding.get()); + CLEAR_AND_RETURN_FALSE_ON_EXCEPTION + } + + if (!m_userAgent.empty()) + { + static jfieldID const userAgentField = + env->GetFieldID(g_httpParamsClazz, "userAgent", "Ljava/lang/String;"); + + auto const jniUserAgent = MakeScopedPointer(jni::ToJavaString(env.get(), m_userAgent), deleter); + CLEAR_AND_RETURN_FALSE_ON_EXCEPTION + + env->SetObjectField(httpParamsObject.get(), userAgentField, jniUserAgent.get()); + CLEAR_AND_RETURN_FALSE_ON_EXCEPTION + } + + if (!m_bodyFile.empty()) + { + static jfieldID const inputFilePathField = + env->GetFieldID(g_httpParamsClazz, "inputFilePath", "Ljava/lang/String;"); + + auto const jniInputFilePath = MakeScopedPointer(jni::ToJavaString(env.get(), m_bodyFile), deleter); + CLEAR_AND_RETURN_FALSE_ON_EXCEPTION + + env->SetObjectField(httpParamsObject.get(), inputFilePathField, jniInputFilePath.get()); + CLEAR_AND_RETURN_FALSE_ON_EXCEPTION + } + + if (!m_receivedFile.empty()) + { + static jfieldID const outputFilePathField = + env->GetFieldID(g_httpParamsClazz, "outputFilePath", "Ljava/lang/String;"); + + auto const jniOutputFilePath = MakeScopedPointer(jni::ToJavaString(env.get(), m_receivedFile), deleter); + CLEAR_AND_RETURN_FALSE_ON_EXCEPTION + + env->SetObjectField(httpParamsObject.get(), outputFilePathField, jniOutputFilePath.get()); + CLEAR_AND_RETURN_FALSE_ON_EXCEPTION + } + + if (!m_basicAuthUser.empty()) + { + static jfieldID const basicAuthUserField = + env->GetFieldID(g_httpParamsClazz, "basicAuthUser", "Ljava/lang/String;"); + + auto const jniBasicAuthUser = MakeScopedPointer(jni::ToJavaString(env.get(), m_basicAuthUser), deleter); + CLEAR_AND_RETURN_FALSE_ON_EXCEPTION + + env->SetObjectField(httpParamsObject.get(), basicAuthUserField, jniBasicAuthUser.get()); + CLEAR_AND_RETURN_FALSE_ON_EXCEPTION + } + + if (!m_basicAuthPassword.empty()) + { + static jfieldID const basicAuthPasswordField = + env->GetFieldID(g_httpParamsClazz, "basicAuthPassword", "Ljava/lang/String;"); + + auto const jniBasicAuthPassword = + MakeScopedPointer(jni::ToJavaString(env.get(), m_basicAuthPassword), deleter); + CLEAR_AND_RETURN_FALSE_ON_EXCEPTION + + env->SetObjectField(httpParamsObject.get(), basicAuthPasswordField, jniBasicAuthPassword.get()); + CLEAR_AND_RETURN_FALSE_ON_EXCEPTION + } + + static jfieldID const cookiesField = + env->GetFieldID(g_httpParamsClazz, "cookies", "Ljava/lang/String;"); + if (!m_cookies.empty()) + { + const auto jniCookies = MakeScopedPointer(jni::ToJavaString(env.get(), m_cookies), deleter); + CLEAR_AND_RETURN_FALSE_ON_EXCEPTION + + env->SetObjectField(httpParamsObject.get(), cookiesField, jniCookies.get()); + CLEAR_AND_RETURN_FALSE_ON_EXCEPTION + } + + 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 + + static jfieldID const receivedUrlField = + env->GetFieldID(g_httpParamsClazz, "receivedUrl", "Ljava/lang/String;"); + auto const jniReceivedUrl = + MakeScopedPointer(static_cast(env->GetObjectField(response, receivedUrlField)), deleter); + CLEAR_AND_RETURN_FALSE_ON_EXCEPTION + if (jniReceivedUrl) + m_urlReceived = jni::ToNativeString(env.get(), jniReceivedUrl.get()); + + // contentTypeField is already cached above. + auto const jniContentType = + MakeScopedPointer(static_cast(env->GetObjectField(response, contentTypeField)), deleter); + CLEAR_AND_RETURN_FALSE_ON_EXCEPTION + if (jniContentType) + m_contentTypeReceived = jni::ToNativeString(env.get(), jniContentType.get()); + + // contentEncodingField is already cached above. + auto const jniContentEncoding = + MakeScopedPointer(static_cast(env->GetObjectField(response, contentEncodingField)), deleter); + CLEAR_AND_RETURN_FALSE_ON_EXCEPTION + if (jniContentEncoding) + m_contentEncodingReceived = jni::ToNativeString(env.get(), jniContentEncoding.get()); + + // Note: cookies field is used not only to send Cookie header, but also to receive back + // Server-Cookie header. CookiesField is already cached above. + auto const jniServerCookies = + MakeScopedPointer(static_cast(env->GetObjectField(response, cookiesField)), deleter); + CLEAR_AND_RETURN_FALSE_ON_EXCEPTION + if (jniServerCookies) + m_serverCookies = + normalize_server_cookies(std::move(jni::ToNativeString(env.get(), jniServerCookies.get()))); + + // dataField is already cached above. + auto const jniData = + MakeScopedPointer(static_cast(env->GetObjectField(response, dataField)), deleter); + 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..ed7d5833a4 --- /dev/null +++ b/android/src/com/mapswithme/util/HttpClient.java @@ -0,0 +1,244 @@ +/******************************************************************************* + * 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.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(final Params p) throws IOException, NullPointerException + { + if (p.httpMethod == null) + throw new NullPointerException("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 (p.basicAuthUser != null) + { + final String encoded = Base64.encodeToString((p.basicAuthUser + ":" + p.basicAuthPassword).getBytes(), Base64.NO_WRAP); + connection.setRequestProperty("Authorization", "Basic " + encoded); + } + if (p.userAgent != null) + connection.setRequestProperty("User-Agent", p.userAgent); + + if (p.cookies != null) + connection.setRequestProperty("Cookie", p.cookies); + + if (p.inputFilePath != null || p.data != null) + { + // Send (POST, PUT...) data to the server. + if (p.contentType == null) + 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 (p.contentEncoding != null) + 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")) + { + p.cookies = ""; + for (final String value : headers.get("Set-Cookie")) + { + // Multiple Set-Cookie headers are normalized in C++ code. + if (value != null) + p.cookies += value + ", "; + } + } + // This implementation receives any data only if we have HTTP::OK (200). + if (p.httpResponseCode == HttpURLConnection.HTTP_OK) + { + OutputStream ostream; + if (p.outputFilePath != null) + 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 = null; + // Can be different from url in case of redirects. + public String receivedUrl = null; + public String httpMethod = null; + // Should be specified for any request whose method allows non-empty body. + // On return, contains received Content-Type or null. + public String contentType = null; + // Can be specified for any request whose method allows non-empty body. + // On return, contains received Content-Encoding or null. + public String contentEncoding = null; + public byte[] data = null; + // Send from input file if specified instead of data. + public String inputFilePath = null; + // Received data is stored here if not null or in data otherwise. + public String outputFilePath = null; + // Optionally client can override default HTTP User-Agent. + public String userAgent = null; + public String basicAuthUser = null; + public String basicAuthPassword = null; + public String cookies = null; + 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..6bd94f3e58 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,15 +28,15 @@ 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 diff --git a/editor/osm_auth.cpp b/editor/osm_auth.cpp index 01995bbf31..b85ee31929 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 { @@ -54,7 +55,7 @@ string BuildPostRequest(map const & params) } // TODO(AlexZ): DebugPrint doesn't detect this overload. Fix it. -string DP(alohalytics::HTTPClientPlatformWrapper const & request) +string DP(HttpClient const & request) { string str = "HTTP " + strings::to_string(request.error_code()) + " url [" + request.url_requested() + "]"; if (request.was_redirected()) @@ -132,8 +133,8 @@ 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)); @@ -148,9 +149,9 @@ OsmOAuth::SessionID OsmOAuth::FetchSessionId(string const & subUrl) const void OsmOAuth::LogoutUser(SessionID const & sid) const { - HTTPClientPlatformWrapper request(m_baseUrl + "/logout"); + HttpClient request(m_baseUrl + "/logout"); request.set_cookies(sid.m_cookies); - if (!request.RunHTTPRequest()) + if (!request.RunHttpRequest()) MYTHROW(NetworkError, ("LogoutUser Network error while connecting to", request.url_requested())); if (request.error_code() != HTTP::OK) MYTHROW(LogoutUserError, (DP(request))); @@ -166,11 +167,11 @@ bool OsmOAuth::LoginUserPassword(string const & login, string const & password, {"commit", "Login"}, {"authenticity_token", sid.m_token} }; - HTTPClientPlatformWrapper request(m_baseUrl + "/login"); + HttpClient 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()) + if (!request.RunHttpRequest()) MYTHROW(NetworkError, ("LoginUserPassword Network error while connecting to", request.url_requested())); // At the moment, automatic redirects handling is buggy on Androids < 4.4. @@ -193,10 +194,10 @@ bool OsmOAuth::LoginUserPassword(string const & login, string const & password, bool OsmOAuth::LoginSocial(string const & callbackPart, string const & socialToken, SessionID const & sid) const { string const url = m_baseUrl + callbackPart + socialToken; - HTTPClientPlatformWrapper request(url); + HttpClient request(url); request.set_cookies(sid.m_cookies) .set_handle_redirects(false); - if (!request.RunHTTPRequest()) + 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))); @@ -227,11 +228,11 @@ string OsmOAuth::SendAuthRequest(string const & requestTokenKey, SessionID const {"allow_write_notes", "yes"}, {"commit", "Save changes"} }; - HTTPClientPlatformWrapper request(m_baseUrl + "/oauth/authorize"); + HttpClient 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()) + if (!request.RunHttpRequest()) MYTHROW(NetworkError, ("SendAuthRequest Network error while connecting to", request.url_requested())); string const callbackURL = request.url_received(); @@ -250,8 +251,8 @@ 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()) + HttpClient 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))); @@ -270,8 +271,8 @@ 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()) + HttpClient 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))); @@ -350,11 +351,11 @@ bool OsmOAuth::ResetPassword(string const & email) const {"authenticity_token", sid.m_token}, {"commit", "Reset password"} }; - HTTPClientPlatformWrapper request(m_baseUrl + kForgotPasswordUrlPart); + HttpClient request(m_baseUrl + kForgotPasswordUrlPart); request.set_body_data(BuildPostRequest(params), "application/x-www-form-urlencoded"); request.set_cookies(sid.m_cookies); - if (!request.RunHTTPRequest()) + if (!request.RunHttpRequest()) MYTHROW(NetworkError, ("ResetPassword Network error while connecting to", request.url_requested())); if (request.error_code() != HTTP::OK) MYTHROW(ResetPasswordServerError, (DP(request))); @@ -391,10 +392,10 @@ 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()) + 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)); @@ -405,8 +406,8 @@ OsmOAuth::Response OsmOAuth::Request(string const & method, string const & httpM 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)); diff --git a/editor/user_stats.cpp b/editor/user_stats.cpp index 17e505dd9a..059a3b35df 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,10 +10,9 @@ #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; +using TRequest = platform::HttpClient; namespace { @@ -93,7 +93,7 @@ bool UserStatsLoader::Update(string const & userName) auto const url = kUserStatsUrl + "&name=" + UrlEncode(userName); TRequest request(url); - if (!request.RunHTTPRequest()) + if (!request.RunHttpRequest()) { LOG(LWARNING, ("Network error while connecting to", url)); return false; 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.hpp b/platform/http_client.hpp new file mode 100644 index 0000000000..ad21a0409e --- /dev/null +++ b/platform/http_client.hpp @@ -0,0 +1,236 @@ +/******************************************************************************* +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/sstream.hpp" +#include "std/string.hpp" + +namespace platform +{ +class HttpClient +{ + public: + enum { kNotInitialized = -1 }; + + HttpClient() = default; + HttpClient(string const & url) : m_urlRequested(url) {} + HttpClient & set_debug_mode(bool debug_mode) + { + m_debugMode = debug_mode; + return *this; + } + HttpClient & set_url_requested(string const & url) + { + m_urlRequested = url; + return *this; + } + HttpClient & set_http_method(string const & method) + { + m_httpMethod = method; + return *this; + } + // This method is mutually exclusive with set_body_data(). + HttpClient & set_body_file(string const & body_file, + string const & content_type, + string const & http_method = "POST", + string const & content_encoding = "") + { + m_bodyFile = body_file; + m_bodyData.clear(); + m_contentType = content_type; + m_httpMethod = http_method; + m_contentEncoding = content_encoding; + return *this; + } + // If set, stores server reply in file specified. + HttpClient & set_received_file(string const & received_file) + { + m_receivedFile = received_file; + return *this; + } + HttpClient & set_user_agent(string const & user_agent) + { + m_userAgent = user_agent; + return *this; + } + // This method is mutually exclusive with set_body_file(). + HttpClient & set_body_data(string const & body_data, + string const & content_type, + string const & http_method = "POST", + string const & content_encoding = "") + { + m_bodyData = body_data; + m_bodyFile.clear(); + m_contentType = content_type; + m_httpMethod = http_method; + m_contentEncoding = content_encoding; + return *this; + } + // Move version to avoid string copying. + // This method is mutually exclusive with set_body_file(). + HttpClient & set_body_data(string && body_data, + string const & content_type, + string const & http_method = "POST", + string const & content_encoding = "") + { + m_bodyData = move(body_data); + m_bodyFile.clear(); + m_contentType = content_type; + m_httpMethod = http_method; + m_contentEncoding = content_encoding; + return *this; + } + // HTTP Basic Auth. + HttpClient & set_user_and_password(string const & user, string const & password) + { + m_basicAuthUser = user; + m_basicAuthPassword = password; + return *this; + } + // Set HTTP Cookie header. + HttpClient & set_cookies(string const & cookies) + { + m_cookies = cookies; + return *this; + } + // 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 & set_handle_redirects(bool handle_redirects) + { + m_handleRedirects = handle_redirects; + return *this; + } + + // 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 + bool RunHttpRequest(); + + string const & url_requested() const { return m_urlRequested; } + // @returns empty string in the case of error + string const & url_received() const { return m_urlReceived; } + bool was_redirected() const { return m_urlRequested != m_urlReceived; } + // 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 error_code() const { return m_errorCode; } + string const & server_response() const { return m_serverResponse; } + string const & http_method() const { return m_httpMethod; } + // Pass this getter's value to the set_cookies() method for easier cookies support in the next request. + string combined_cookies() const + { + if (m_serverCookies.empty()) + { + return m_cookies; + } + if (m_cookies.empty()) + { + return m_serverCookies; + } + return m_serverCookies + "; " + m_cookies; + } + // Returns cookie value or empty string if it's not present. + string cookie_by_name(string name) const + { + string const str = combined_cookies(); + 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 string(); + } + +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 normalize_server_cookies(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 m_urlRequested; + // Contains final content's url taking redirects (if any) into an account. + string m_urlReceived; + int m_errorCode = kNotInitialized; + string m_bodyFile; + // Used instead of server_reply_ if set. + string m_receivedFile; + // 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); +}; +} // namespace platform diff --git a/platform/http_client_apple.mm b/platform/http_client_apple.mm new file mode 100644 index 0000000000..9f06f6d5af --- /dev/null +++ b/platform/http_client_apple.mm @@ -0,0 +1,165 @@ +/******************************************************************************* +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() +{ + @autoreleasepool + { + 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]; +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" + // base64Encoding selector below was deprecated in iOS 7+, but we still need it to support 5.1+ versions. + [request setValue:[NSString stringWithFormat:@"Basic %@", [loginAndPassword base64Encoding]] forHTTPHeaderField:@"Authorization"]; +#pragma clang diagnostic pop + } + + 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_bodyFile.empty()) + { + NSError * err = nil; + NSString * path = [NSString stringWithUTF8String:m_bodyFile.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_bodyFile, 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 = normalize_server_cookies(std::move([cookies UTF8String])); + + if (url_data) + { + if (m_receivedFile.empty()) + m_serverResponse.assign(reinterpret_cast(url_data.bytes), url_data.length); + else + [url_data writeToFile:[NSString stringWithUTF8String:m_receivedFile.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; + } // @autoreleasepool +} +} // namespace platform diff --git a/platform/http_client_curl.cpp b/platform/http_client_curl.cpp new file mode 100644 index 0000000000..bdf116a3b1 --- /dev/null +++ b/platform/http_client_curl.cpp @@ -0,0 +1,261 @@ +/******************************************************************************* + 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_file(fileName) {} + + ~ScopedRemoveFile() + { + if (!m_file.empty()) + std::remove(m_file.c_str()); + } + + std::string m_file; +}; + +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> HeadersT; + +HeadersT 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; +} +} // 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_file + "' "; + + 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_file = GetTmpFileName(); + // POST body through tmp file to avoid breaking command line. + if (!(ofstream(body_deleter.m_file) << m_bodyData).good()) + { + LOG(LERROR, ("Failed to write into a temporary file.")); + return false; + } + // TODO(AlexZ): Correctly clean up this internal var to avoid client confusion. + m_bodyFile = body_deleter.m_file; + } + // Content-Length is added automatically by curl. + if (!m_bodyFile.empty()) + cmd += "--data-binary '@" + m_bodyFile + "' "; + + // 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_receivedFile; + if (rfile.empty()) + { + rfile = GetTmpFileName(); + received_file_deleter.m_file = 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_file)); + 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 = normalize_server_cookies(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_receivedFile.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.set_cookies(combined_cookies()); + + if (!redirect.RunHttpRequest()) + { + m_errorCode = -1; + return false; + } + + m_errorCode = redirect.error_code(); + m_urlReceived = redirect.url_received(); + 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..b623316b02 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 \ 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..1d509cf925 100644 --- a/platform/platform_mac.mm +++ b/platform/platform_mac.mm @@ -19,8 +19,8 @@ Platform::Platform() { - NSAutoreleasePool * pool = [[NSAutoreleasePool alloc] init]; - + @autoreleasepool + { // get resources directory path string const resourcesPath = [[[NSBundle mainBundle] resourcePath] UTF8String]; string const bundlePath = [[[NSBundle mainBundle] bundlePath] UTF8String]; @@ -78,8 +78,7 @@ Platform::Platform() LOG(LDEBUG, ("Writable Directory:", m_writableDir)); LOG(LDEBUG, ("Tmp Directory:", m_tmpDir)); LOG(LDEBUG, ("Settings Directory:", m_settingsDir)); - - [pool release]; + } // @autoreleasepool } string Platform::UniqueClientId() const diff --git a/routing/online_cross_fetcher.cpp b/routing/online_cross_fetcher.cpp index 5602a82899..27e6c36f0e 100644 --- a/routing/online_cross_fetcher.cpp +++ b/routing/online_cross_fetcher.cpp @@ -62,7 +62,7 @@ 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()) + if (m_request.RunHttpRequest() && m_request.error_code() == 200 && !m_request.was_redirected()) ParseResponse(m_request.server_response(), m_mwmPoints); else LOG(LWARNING, ("Can't get OSRM server response. Code: ", m_request.error_code())); 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..0d9bad508f 100644 --- a/xcode/platform/platform.xcodeproj/project.pbxproj +++ b/xcode/platform/platform.xcodeproj/project.pbxproj @@ -7,6 +7,8 @@ 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 */; }; 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 +97,8 @@ /* 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 = ""; }; 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 +300,8 @@ 6753437A1A3F5CF500A0A8C3 /* platform */ = { isa = PBXGroup; children = ( + 3D30587E1D880910004AC712 /* http_client_apple.mm */, + 3D30587B1D8320E4004AC712 /* http_client.hpp */, 56EB1ED81C6B6E6C0022D831 /* file_logging.cpp */, 56EB1ED91C6B6E6C0022D831 /* file_logging.hpp */, 56EB1EDA1C6B6E6C0022D831 /* mwm_traits.cpp */, @@ -406,6 +412,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,6 +556,7 @@ 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 */, 67AB92EA1B7B3E9100AB5194 /* get_text_by_id.cpp in Sources */, @@ -725,7 +733,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 +747,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;