Browse Source

Adding WebRequest to Web subsystem.

Jay Sistar 10 years ago
parent
commit
82ce20ab5d

+ 34 - 4
Source/Atomic/Web/Web.cpp

@@ -32,6 +32,7 @@
 #ifndef EMSCRIPTEN
 #ifndef EMSCRIPTEN
 #include "../Web/WebInternalConfig.h"
 #include "../Web/WebInternalConfig.h"
 #include <asio/io_service.hpp>
 #include <asio/io_service.hpp>
+#include <curl/curl.h>
 #endif
 #endif
 
 
 #include "../DebugNew.h"
 #include "../DebugNew.h"
@@ -43,6 +44,7 @@ struct WebPrivate
 {
 {
 #ifndef EMSCRIPTEN
 #ifndef EMSCRIPTEN
     asio::io_service service;
     asio::io_service service;
+    CURLM *curlm;
 #endif
 #endif
 };
 };
 
 
@@ -50,31 +52,59 @@ Web::Web(Context* context) :
     Object(context),
     Object(context),
     d(new WebPrivate())
     d(new WebPrivate())
 {
 {
+#ifndef EMSCRIPTEN
+    d->curlm = curl_multi_init();
+#endif
     SubscribeToEvent(E_UPDATE, HANDLER(Web, internalUpdate));
     SubscribeToEvent(E_UPDATE, HANDLER(Web, internalUpdate));
 }
 }
 
 
 Web::~Web()
 Web::~Web()
 {
 {
     UnsubscribeFromEvent(E_UPDATE);
     UnsubscribeFromEvent(E_UPDATE);
+#ifndef EMSCRIPTEN
+    curl_multi_cleanup(d->curlm);
+#endif
     delete d;
     delete d;
 }
 }
 
 
 void Web::internalUpdate(StringHash eventType, VariantMap& eventData)
 void Web::internalUpdate(StringHash eventType, VariantMap& eventData)
 {
 {
 #ifndef EMSCRIPTEN
 #ifndef EMSCRIPTEN
+    int runningHandles;
+    curl_multi_perform(d->curlm, &runningHandles);
+
+    CURLMsg *msg;
+    int msgsLeft;
+    while ((msg = curl_multi_info_read(d->curlm, &msgsLeft)))
+    {
+        if (msg->msg != CURLMSG_DONE)
+        {
+            continue;
+        }
+
+        WebRequest *wr;
+        curl_easy_getinfo(msg->easy_handle, CURLINFO_PRIVATE, &wr);
+        if (wr != NULL)
+        {
+            WebRequest::internalNotify(wr, int(msg->data.result));
+        }
+    }
+
     d->service.reset();
     d->service.reset();
     d->service.poll();
     d->service.poll();
 #endif
 #endif
 }
 }
 
 
-SharedPtr<WebRequest> Web::MakeWebRequest(const String& url, const String& verb, const Vector<String>& headers,
-    const String& postData)
+SharedPtr<WebRequest> Web::MakeWebRequest(const String& verb, const String& url, double requestContentSize)
 {
 {
     PROFILE(MakeWebRequest);
     PROFILE(MakeWebRequest);
 
 
     // The initialization of the request will take time, can not know at this point if it has an error or not
     // The initialization of the request will take time, can not know at this point if it has an error or not
-    SharedPtr<WebRequest> request(new WebRequest(url, verb, headers, postData));
-    return request;
+    SharedPtr<WebRequest> webRequest(new WebRequest(context_, verb, url, requestContentSize));
+#ifndef EMSCRIPTEN
+    webRequest->setup(&d->service, d->curlm);
+#endif
+    return webRequest;
 }
 }
 
 
 SharedPtr<WebSocket> Web::MakeWebSocket(const String& url)
 SharedPtr<WebSocket> Web::MakeWebSocket(const String& url)

+ 5 - 3
Source/Atomic/Web/Web.h

@@ -45,10 +45,12 @@ public:
     /// Destruct.
     /// Destruct.
     ~Web();
     ~Web();
 
 
+    // Note that the use of "double" here is strictly to make automatic binding
+    // work properly. It should accually be a "long long" or "size_t", and the
+    // value will be floored to an integer before it's used.
+
     /// Perform an HTTP request to the specified URL. Empty verb defaults to a GET request. Return a request object which can be used to read the response data.
     /// Perform an HTTP request to the specified URL. Empty verb defaults to a GET request. Return a request object which can be used to read the response data.
-    SharedPtr<WebRequest> MakeWebRequest
-        (const String& url, const String& verb = String::EMPTY, const Vector<String>& headers = Vector<String>(),
-            const String& postData = String::EMPTY);
+    SharedPtr<WebRequest> MakeWebRequest(const String& verb, const String& url, double requestContentSize = 0.0);
     /// Perform an WebSocket request to the specified URL. Return a WebSocket object which can be used to comunicate with the server.
     /// Perform an WebSocket request to the specified URL. Return a WebSocket object which can be used to comunicate with the server.
     SharedPtr<WebSocket> MakeWebSocket(const String& url);
     SharedPtr<WebSocket> MakeWebSocket(const String& url);
 
 

+ 1 - 0
Source/Atomic/Web/WebInternalConfig.h

@@ -33,3 +33,4 @@
 #define ASIO_STANDALONE
 #define ASIO_STANDALONE
 #define ASIO_HAS_STD_ARRAY
 #define ASIO_HAS_STD_ARRAY
 #define ASIO_HAS_CSTDINT
 #define ASIO_HAS_CSTDINT
+#define CURL_STATICLIB

+ 329 - 190
Source/Atomic/Web/WebRequest.cpp

@@ -1,6 +1,5 @@
 //
 //
 // Copyright (c) 2014-2015, THUNDERBEAST GAMES LLC All rights reserved
 // Copyright (c) 2014-2015, THUNDERBEAST GAMES LLC All rights reserved
-// Copyright (c) 2008-2015 the Urho3D project.
 //
 //
 // Permission is hereby granted, free of charge, to any person obtaining a copy
 // Permission is hereby granted, free of charge, to any person obtaining a copy
 // of this software and associated documentation files (the "Software"), to deal
 // of this software and associated documentation files (the "Software"), to deal
@@ -24,276 +23,416 @@
 #include "../Precompiled.h"
 #include "../Precompiled.h"
 
 
 #include "../Core/Profiler.h"
 #include "../Core/Profiler.h"
+#include "../Container/HashMap.h"
+#include "../IO/BufferQueue.h"
 #include "../IO/Log.h"
 #include "../IO/Log.h"
 #include "../Web/WebRequest.h"
 #include "../Web/WebRequest.h"
 
 
-#include "../DebugNew.h"
-
-// !!! WARNING! Use HttpRequest from Network if you don't want your interface
-//              to change. This file mimics HttpRequest for now, but will be
-//              changing, and it is here as a placeholder only!
-
 #ifdef EMSCRIPTEN
 #ifdef EMSCRIPTEN
 
 
+#include "../DebugNew.h"
 // Add code to use an XMLHttpRequest or ActiveX XMLHttpRequest here.
 // Add code to use an XMLHttpRequest or ActiveX XMLHttpRequest here.
 
 
 #else
 #else
 
 
-#include <Civetweb/include/civetweb.h>
+#include "../Web/WebInternalConfig.h"
+#include <asio.hpp>
+#include <functional>
+#include <curl/curl.h>
 
 
+#include "../DebugNew.h"
 
 
 namespace Atomic
 namespace Atomic
 {
 {
 
 
-static const unsigned ERROR_BUFFER_SIZE = 256;
-static const unsigned READ_BUFFER_SIZE = 65536; // Must be a power of two
-
-WebRequest::WebRequest(const String& url, const String& verb, const Vector<String>& headers, const String& postData) :
-    url_(url.Trimmed()),
-    verb_(!verb.Empty() ? verb : "GET"),
-    headers_(headers),
-    postData_(postData),
-    state_(HTTP_INITIALIZING),
-    httpReadBuffer_(new unsigned char[READ_BUFFER_SIZE]),
-    readBuffer_(new unsigned char[READ_BUFFER_SIZE]),
-    readPosition_(0),
-    writePosition_(0)
-{
-    // Size of response is unknown, so just set maximum value. The position will also be changed
-    // to maximum value once the request is done, signaling end for Deserializer::IsEof().
-    size_ = M_MAX_UNSIGNED;
-
-    LOGERROR("DO NOT USE WebRequest YET! Use the Network subsystem's HttpRequest. The WebRequest interface is under development and will change soon!");
-    LOGDEBUG("HTTP " + verb_ + " request to URL " + url_);
-
-    // Start the worker thread to actually create the connection and read the response data.
-    Run();
-}
-
-WebRequest::~WebRequest()
-{
-    Stop();
-}
-
-void WebRequest::ThreadFunction()
+struct WebRequestInternalState
 {
 {
-    String protocol = "http";
-    String host;
-    String path = "/";
-    int port = 80;
-
-    unsigned protocolEnd = url_.Find("://");
-    if (protocolEnd != String::NPOS)
+    /// The WebRequest external state.
+    WebRequest& es;
+    /// The WebRequest external state to force it to stay around.
+    SharedPtr<WebRequest> es_hold;
+    /// The work queue.
+    asio::io_service* service;
+    /// URL.
+    String url;
+    /// Verb.
+    String verb;
+    /// Response headers.
+    HashMap<StringHash, Pair<String, String>> responseHeaders;
+    /// Upload stream.
+    SharedPtr<Object> upload;
+    /// Download stream.
+    SharedPtr<Object> download;
+    /// Request Headers.
+    curl_slist* headers = NULL;
+    /// Connection state.
+    WebRequestState state;
+    /// cURL multi handle.
+    CURLM* curlm;
+    /// cURL easy handle.
+    CURL* curl;
+    /// A flag to know if the request has contents (has data to upload).
+    curl_off_t requestContentSize;
+    /// A flag to know if the operation has been aborted.
+    bool isAborted;
+    /// A flag to know if the easy handle has been added to the Web class's multi handle.
+    bool isAddedToMulti;
+    /// Error string. Empty if no error.
+    char error[CURL_ERROR_SIZE];
+
+    WebRequestInternalState(WebRequest &es_) :
+        es(es_)
     {
     {
-        protocol = url_.Substring(0, protocolEnd);
-        host = url_.Substring(protocolEnd + 3);
+        LOGDEBUG("Create WebRequestInternalState");
     }
     }
-    else
-        host = url_;
 
 
-    unsigned pathStart = host.Find('/');
-    if (pathStart != String::NPOS)
+    ~WebRequestInternalState()
     {
     {
-        path = host.Substring(pathStart);
-        host = host.Substring(0, pathStart);
+        LOGDEBUG("Destroy WebRequestInternalState");
     }
     }
 
 
-    unsigned portStart = host.Find(':');
-    if (portStart != String::NPOS)
+    static int onProgress(void *clientp, curl_off_t dltotal, curl_off_t dlnow, curl_off_t ultotal, curl_off_t ulnow)
     {
     {
-        port = ToInt(host.Substring(portStart + 1));
-        host = host.Substring(0, portStart);
-    }
-
-    char errorBuffer[ERROR_BUFFER_SIZE];
-    memset(errorBuffer, 0, sizeof(errorBuffer));
+        WebRequestInternalState *is_(reinterpret_cast<WebRequestInternalState*>(clientp));
+        if (is_->isAborted)
+        {
+            // This should probably be CURL_XFERINFO_ABORT, but that doesn't
+            // exist. It probably would be the same numeric value, if it did.
+            // The docs say that it just has to be a nonzero to abort.
+            return CURL_READFUNC_ABORT;
+        }
 
 
-    String headersStr;
-    for (unsigned i = 0; i < headers_.Size(); ++i)
-    {
-        // Trim and only add non-empty header strings
-        String header = headers_[i].Trimmed();
-        if (header.Length())
-            headersStr += header + "\r\n";
+        VariantMap eventData;
+        eventData.Insert(MakePair(StringHash("down_total"), Variant((double)dltotal)));
+        eventData.Insert(MakePair(StringHash("down_loaded"), Variant((double)dlnow)));
+        eventData.Insert(MakePair(StringHash("up_total"), Variant((double)ultotal)));
+        eventData.Insert(MakePair(StringHash("up_loaded"), Variant((double)ulnow)));
+        is_->es.SendEvent("progress", eventData);
+        return 0;
     }
     }
 
 
-    // Initiate the connection. This may block due to DNS query
-    /// \todo SSL mode will not actually work unless Civetweb's SSL mode is initialized with an external SSL DLL
-    mg_connection* connection = 0;
-    if (postData_.Empty())
-    {
-        connection = mg_download(host.CString(), port, protocol.Compare("https", false) ? 0 : 1, errorBuffer, sizeof(errorBuffer),
-            "%s %s HTTP/1.1\r\n"
-            "Host: %s\r\n"
-            "Connection: close\r\n"
-            "%s"
-            "\r\n", verb_.CString(), path.CString(), host.CString(), headersStr.CString());
-    }
-    else
+    static size_t onHeader(char *ptr, size_t size, size_t nmemb, void *userdata)
     {
     {
-        connection = mg_download(host.CString(), port, protocol.Compare("https", false) ? 0 : 1, errorBuffer, sizeof(errorBuffer),
-            "%s %s HTTP/1.1\r\n"
-            "Host: %s\r\n"
-            "Connection: close\r\n"
-            "%s"
-            "Content-Length: %d\r\n"
-            "\r\n"
-            "%s", verb_.CString(), path.CString(), host.CString(), headersStr.CString(), postData_.Length(), postData_.CString());
+        WebRequestInternalState *is_(reinterpret_cast<WebRequestInternalState*>(userdata));
+        if (is_->isAborted)
+        {
+            is_->state = HTTP_CLOSED;
+            // This should probably be CURL_HEADERFUNC_ABORT, but that doesn't
+            // exist. It probably would be the same numeric value, if it did.
+            // The docs say that it just has to be a number of bytes that is
+            // not "size * nmemb" to abort.
+            return CURL_READFUNC_ABORT;
+        }
+
+        // Find the size in bytes.
+        size_t real_size(size * nmemb);
+
+        // Check for some known values.
+        if (real_size == 2 && ptr[0] == '\r' && ptr[1] == '\n')
+        {
+            return real_size;
+        }
+        if (real_size > 5 && !strncmp(ptr, "HTTP/", 5))
+        {
+            return real_size;
+        }
+
+        // Get the header key and value, and add them to the map.
+        unsigned int key_end = 0;
+        unsigned int value_begin = 2;
+        while (value_begin < real_size)
+        {
+            if (ptr[key_end] == ':' && ptr[key_end + 1] == ' ')
+            {
+                break;
+            }
+            ++key_end;
+            ++value_begin;
+        }
+        if (value_begin == real_size)
+        {
+            String key(ptr, (unsigned int)real_size);
+            is_->responseHeaders.InsertNew(key.ToUpper(), MakePair(key, String()));
+        }
+        else
+        {
+            String key(ptr, (unsigned int)key_end);
+            is_->responseHeaders.InsertNew(key.ToUpper(), MakePair(key, String(ptr + value_begin, (unsigned int)real_size - value_begin - 2)));
+        }
+
+        return real_size;
     }
     }
 
 
+    static size_t onWrite(char *ptr, size_t size, size_t nmemb, void *userdata)
     {
     {
-        MutexLock lock(mutex_);
-        state_ = connection ? HTTP_OPEN : HTTP_ERROR;
-
-        // If no connection could be made, store the error and exit
-        if (state_ == HTTP_ERROR)
+        WebRequestInternalState *is_(reinterpret_cast<WebRequestInternalState*>(userdata));
+        is_->state = HTTP_OPEN;
+        if (is_->isAborted)
         {
         {
-            error_ = String(&errorBuffer[0]);
-            return;
+            is_->state = HTTP_CLOSED;
+            // This should probably be CURL_WRITEFUNC_ABORT, but that doesn't
+            // exist. It probably would be the same numeric value, if it did.
+            // The docs say that it just has to be a number of bytes that is
+            // not "size * nmemb" to abort.
+            return CURL_READFUNC_ABORT;
         }
         }
+
+        // Find the size in bytes.
+        size_t real_size(size * nmemb);
+
+        // Write the date into the download buffer queue.
+        Serializer* download(dynamic_cast<Serializer*>(is_->download.Get()));
+        download->Write(ptr, (unsigned int)real_size);
+
+        // Emit a "download_chunk" event.
+        VariantMap eventData;
+        eventData.Insert(MakePair(StringHash("download"), Variant(is_->download)));
+        eventData.Insert(MakePair(StringHash("size"), Variant((unsigned int)real_size)));
+        is_->es.SendEvent("download_chunk", eventData);
+
+        return real_size;
     }
     }
 
 
-    // Loop while should run, read data from the connection, copy to the main thread buffer if there is space
-    while (shouldRun_)
+    static size_t onRead(char *buffer, size_t size, size_t nitems, void *instream)
     {
     {
-        // Read less than full buffer to be able to distinguish between full and empty ring buffer. Reading may block
-        int bytesRead = mg_read(connection, httpReadBuffer_.Get(), READ_BUFFER_SIZE / 4);
-        if (bytesRead <= 0)
-            break;
+        WebRequestInternalState *is_(reinterpret_cast<WebRequestInternalState*>(instream));
+        is_->state = HTTP_OPEN;
+        if (is_->isAborted)
+        {
+            is_->state = HTTP_CLOSED;
+            return CURL_READFUNC_ABORT;
+        }
 
 
-        mutex_.Acquire();
+        // Find the size in bytes.
+        size_t real_size(size * nitems);
 
 
-        // Wait until enough space in the main thread's ring buffer
-        for (;;)
+        // Read as much as we can from the upload buffer queue.
+        Deserializer* upload(dynamic_cast<Deserializer*>(is_->upload.Get()));
+        size_t size_queued(upload->GetSize());
+        size_t size_left(real_size);
+        if ((size_left > 0) && (size_queued > 0))
         {
         {
-            unsigned spaceInBuffer = READ_BUFFER_SIZE - ((writePosition_ - readPosition_) & (READ_BUFFER_SIZE - 1));
-            if ((int)spaceInBuffer > bytesRead || !shouldRun_)
-                break;
+            size_t read_size(std::min(size_queued, size_left));
+            upload->Read(buffer, (unsigned int)read_size);
+            size_left -= read_size;
+        }
 
 
-            mutex_.Release();
-            Time::Sleep(5);
-            mutex_.Acquire();
+        // If we still have bytes to fill, then emit a "upload_chunk" event.
+        if (size_left > 0)
+        {
+            VariantMap eventData;
+            eventData.Insert(MakePair(StringHash("upload"), Variant(is_->upload)));
+            eventData.Insert(MakePair(StringHash("size"), Variant((unsigned int)size_left)));
+            is_->es.SendEvent("upload_chunk", eventData);
         }
         }
 
 
-        if (!shouldRun_)
+        // Read as much as we can from the upload buffer queue (again).
+        size_queued = upload->GetSize();
+        size_left = real_size;
+        if ((size_left > 0) && (size_queued > 0))
         {
         {
-            mutex_.Release();
-            break;
+            size_t read_size(std::min(size_queued, size_left));
+            upload->Read(buffer, (unsigned int)read_size);
+            size_left -= read_size;
         }
         }
 
 
-        if (writePosition_ + bytesRead <= READ_BUFFER_SIZE)
-            memcpy(readBuffer_.Get() + writePosition_, httpReadBuffer_.Get(), (size_t)bytesRead);
-        else
+        // If we still have bytes to fill, then something went wrong, so we should abort.
+        if (size_left > 0)
         {
         {
-            // Handle ring buffer wrap
-            unsigned part1 = READ_BUFFER_SIZE - writePosition_;
-            unsigned part2 = bytesRead - part1;
-            memcpy(readBuffer_.Get() + writePosition_, httpReadBuffer_.Get(), part1);
-            memcpy(readBuffer_.Get(), httpReadBuffer_.Get() + part1, part2);
+            is_->isAborted = true;
+            return CURL_READFUNC_ABORT;
         }
         }
 
 
-        writePosition_ += bytesRead;
-        writePosition_ &= READ_BUFFER_SIZE - 1;
+        return real_size;
+    }
 
 
-        mutex_.Release();
+    void onEnd(int code)
+    {
+        VariantMap eventData;
+        if (code != CURLE_OK)
+        {
+            state = HTTP_ERROR;
+            eventData.Insert(MakePair(StringHash("error"), Variant(String(error, (unsigned int)strnlen(error, sizeof(error))))));
+        }
+        else
+        {
+            state = HTTP_CLOSED;
+            eventData.Insert(MakePair(StringHash("download"), Variant(download)));
+            eventData.Insert(MakePair(StringHash("upload"), Variant(upload)));
+        }
+        es.SendEvent("complete", eventData);
     }
     }
+};
+
+WebRequest::WebRequest(Context* context, const String& verb, const String& url, double requestContentSize) :
+    Object(context),
+    is_(new WebRequestInternalState(*this))
+{
+    is_->url = url.Trimmed();
+    is_->verb = verb;
+    is_->upload = new BufferQueue(context);
+    is_->download = new BufferQueue(context);
+    is_->state = HTTP_INITIALIZING;
+    is_->curlm = NULL;
+    is_->curl = NULL;
+    is_->requestContentSize = curl_off_t(std::floor(requestContentSize));
+    is_->isAborted = false;
+    is_->isAddedToMulti = false;
+
+}
 
 
-    // Close the connection
-    mg_close_connection(connection);
+WebRequest::~WebRequest()
+{
+    LOGDEBUG("Destroy WebRequest");
 
 
+    curl_slist_free_all(is_->headers);
+    if (is_->curlm == NULL)
     {
     {
-        MutexLock lock(mutex_);
-        state_ = HTTP_CLOSED;
+        return;
     }
     }
+    curl_easy_cleanup(is_->curl);
+    delete is_;
 }
 }
 
 
-unsigned WebRequest::Read(void* dest, unsigned size)
+void WebRequest::setup(asio::io_service *service, CURLM *curlm)
 {
 {
-    mutex_.Acquire();
+    LOGDEBUG("Create WebRequest");
 
 
-    unsigned char* destPtr = (unsigned char*)dest;
-    unsigned sizeLeft = size;
-    unsigned totalRead = 0;
+    is_->service = service;
+    is_->curlm = curlm;
+    is_->curl = curl_easy_init();
 
 
-    for (;;)
-    {
-        unsigned bytesAvailable;
+    LOGDEBUG("HTTP " + is_->verb + " request to URL " + is_->url);
 
 
-        for (;;)
-        {
-            bytesAvailable = CheckEofAndAvailableSize();
-            if (bytesAvailable || IsEof())
-                break;
-            // While no bytes and connection is still open, block until has some data
-            mutex_.Release();
-            Time::Sleep(5);
-            mutex_.Acquire();
-        }
+    curl_easy_setopt(is_->curl, CURLOPT_ERRORBUFFER, is_->error);
+    is_->error[0] = '\0';
 
 
-        if (bytesAvailable)
-        {
-            if (bytesAvailable > sizeLeft)
-                bytesAvailable = sizeLeft;
+    curl_easy_setopt(is_->curl, CURLOPT_URL, is_->url.CString());
 
 
-            if (readPosition_ + bytesAvailable <= READ_BUFFER_SIZE)
-                memcpy(destPtr, readBuffer_.Get() + readPosition_, bytesAvailable);
-            else
-            {
-                // Handle ring buffer wrap
-                unsigned part1 = READ_BUFFER_SIZE - readPosition_;
-                unsigned part2 = bytesAvailable - part1;
-                memcpy(destPtr, readBuffer_.Get() + readPosition_, part1);
-                memcpy(destPtr + part1, readBuffer_.Get(), part2);
-            }
+    // All callbacks must look at is_->isAborted flag!
 
 
-            readPosition_ += bytesAvailable;
-            readPosition_ &= READ_BUFFER_SIZE - 1;
-            sizeLeft -= bytesAvailable;
-            totalRead += bytesAvailable;
-            destPtr += bytesAvailable;
-        }
+    curl_easy_setopt(is_->curl, CURLOPT_HEADERFUNCTION, &WebRequestInternalState::onHeader);
+    curl_easy_setopt(is_->curl, CURLOPT_HEADERDATA, is_);
+
+    curl_easy_setopt(is_->curl, CURLOPT_WRITEFUNCTION, &WebRequestInternalState::onWrite);
+    curl_easy_setopt(is_->curl, CURLOPT_WRITEDATA, is_);
+
+    curl_easy_setopt(is_->curl, CURLOPT_NOPROGRESS, 0L);
+    curl_easy_setopt(is_->curl, CURLOPT_XFERINFOFUNCTION, &WebRequestInternalState::onProgress);
+    curl_easy_setopt(is_->curl, CURLOPT_XFERINFODATA, is_);
+
+    curl_easy_setopt(is_->curl, CURLOPT_CUSTOMREQUEST, is_->verb.CString());
 
 
-        if (!sizeLeft || !bytesAvailable)
-            break;
+    curl_easy_setopt(is_->curl, CURLOPT_PRIVATE, this);
+
+    curl_easy_setopt(is_->curl, CURLOPT_READFUNCTION, &WebRequestInternalState::onRead);
+    curl_easy_setopt(is_->curl, CURLOPT_READDATA, is_);
+
+    if (is_->requestContentSize)
+    {
+        curl_easy_setopt(is_->curl, CURLOPT_UPLOAD, 1L);
+        curl_easy_setopt(is_->curl, CURLOPT_INFILESIZE_LARGE, is_->requestContentSize);
+    }
+}
+
+void WebRequest::internalNotify(WebRequest *wr, int code)
+{
+    wr->is_->onEnd(code);
+    if (wr->is_->isAddedToMulti)
+    {
+        curl_multi_remove_handle(wr->is_->curlm, wr->is_->curl);
+        wr->is_->isAddedToMulti = false;
+        wr->is_->es_hold.Reset();
     }
     }
+}
 
 
-    // Check for end-of-file once more after reading the bytes
-    CheckEofAndAvailableSize();
-    mutex_.Release();
-    return totalRead;
+void WebRequest::Abort()
+{
+    is_->isAborted = true;
 }
 }
 
 
-unsigned WebRequest::Seek(unsigned position)
+const String& WebRequest::GetURL() const
 {
 {
-    return position_;
+    return is_->url;
 }
 }
 
 
 String WebRequest::GetError() const
 String WebRequest::GetError() const
 {
 {
-    MutexLock lock(mutex_);
-    const_cast<WebRequest*>(this)->CheckEofAndAvailableSize();
-    return error_;
+    return String(is_->error);
 }
 }
 
 
 WebRequestState WebRequest::GetState() const
 WebRequestState WebRequest::GetState() const
 {
 {
-    MutexLock lock(mutex_);
-    const_cast<WebRequest*>(this)->CheckEofAndAvailableSize();
-    return state_;
+    return is_->state;
+}
+
+String WebRequest::GetVerb() const
+{
+    return is_->verb;
+}
+
+bool WebRequest::HasDownloadChunkEvent()
+{
+    return true; // cURL based implementations always support the "download_chunk" event.
+}
+
+void WebRequest::SetRequestHeader(const String& key, const String& value)
+{
+    // Trim and only add non-empty header strings.
+    String header;
+    header += key.Trimmed();
+    header += ": ";
+    header += value;
+    if (header.Length())
+    {
+        is_->headers = curl_slist_append(is_->headers, header.CString());
+    }
+}
+
+void WebRequest::Send()
+{
+    if (!is_->isAddedToMulti && !is_->isAborted)
+    {
+        is_->es_hold = this;
+        curl_easy_setopt(is_->curl, CURLOPT_HTTPHEADER, is_->headers);
+        curl_multi_add_handle(is_->curlm, is_->curl);
+        is_->isAddedToMulti = true;
+    }
+}
+
+StringVector WebRequest::GetResponseHeaderKeys()
+{
+    StringVector keys;
+    for (auto it(is_->responseHeaders.Begin()),
+        itEnd(is_->responseHeaders.End()); it != itEnd; ++it)
+    {
+        keys.Push(it->second_.first_);
+    }
+    return keys;
 }
 }
 
 
-unsigned WebRequest::GetAvailableSize() const
+String WebRequest::GetResponseHeader(const String& header)
 {
 {
-    MutexLock lock(mutex_);
-    return const_cast<WebRequest*>(this)->CheckEofAndAvailableSize();
+    auto it(is_->responseHeaders.Find(header.ToUpper()));
+    if (it == is_->responseHeaders.End())
+    {
+        return "";
+    }
+    return it->second_.second_;
 }
 }
 
 
-unsigned WebRequest::CheckEofAndAvailableSize()
+String WebRequest::GetAllResponseHeaders()
 {
 {
-    unsigned bytesAvailable = (writePosition_ - readPosition_) & (READ_BUFFER_SIZE - 1);
-    if (state_ == HTTP_ERROR || (state_ == HTTP_CLOSED && !bytesAvailable))
-        position_ = M_MAX_UNSIGNED;
-    return bytesAvailable;
+    String allHeaders;
+    for (auto it(is_->responseHeaders.Begin()),
+        itEnd(is_->responseHeaders.End()); it != itEnd; ++it)
+    {
+        allHeaders += it->second_.first_;
+        allHeaders += ": ";
+        allHeaders += it->second_.second_;
+        allHeaders += "\r\n";
+    }
+    return allHeaders;
 }
 }
 
 
 }
 }

+ 47 - 47
Source/Atomic/Web/WebRequest.h

@@ -23,11 +23,21 @@
 
 
 #pragma once
 #pragma once
 
 
+#include "../Core/Object.h"
+#include "../Container/Str.h"
 #include "../Container/ArrayPtr.h"
 #include "../Container/ArrayPtr.h"
-#include "../Core/Mutex.h"
-#include "../Container/RefCounted.h"
-#include "../Core/Thread.h"
-#include "../IO/Deserializer.h"
+
+namespace asio
+{
+class io_service;
+}
+
+#ifndef EMSCRIPTEN
+extern "C"
+{
+typedef void CURLM;
+}
+#endif
 
 
 namespace Atomic
 namespace Atomic
 {
 {
@@ -41,67 +51,57 @@ enum WebRequestState
     HTTP_CLOSED
     HTTP_CLOSED
 };
 };
 
 
+struct WebRequestInternalState;
+
 /// An HTTP connection with response data stream.
 /// An HTTP connection with response data stream.
-class WebRequest : public RefCounted, public Deserializer, public Thread
+class WebRequest : public Object
 {
 {
-    REFCOUNTED(WebRequest)
+    friend class Web;
+
+    OBJECT(WebRequest)
 
 
 public:
 public:
     /// Construct with parameters.
     /// Construct with parameters.
-    WebRequest(const String& url, const String& verb, const Vector<String>& headers, const String& postData);
-    /// Destruct. Release the connection object.
+    WebRequest(Context* context, const String& verb, const String& url, double requestContentSize);
+    /// Destruct. Release the connection.
     ~WebRequest();
     ~WebRequest();
 
 
-    /// Process the connection in the worker thread until closed.
-    virtual void ThreadFunction();
-
-    /// Read response data from the HTTP connection and return number of bytes actually read. While the connection is open, will block while trying to read the specified size. To avoid blocking, only read up to as many bytes as GetAvailableSize() returns.
-    virtual unsigned Read(void* dest, unsigned size);
-    /// Set position from the beginning of the stream. Not supported.
-    virtual unsigned Seek(unsigned position);
-
     /// Return URL used in the request.
     /// Return URL used in the request.
-    const String& GetURL() const { return url_; }
-
-    /// Return verb used in the request. Default GET if empty verb specified on construction.
-    const String& GetVerb() const { return verb_; }
+    const String& GetURL() const;
 
 
     /// Return error. Only non-empty in the error state.
     /// Return error. Only non-empty in the error state.
     String GetError() const;
     String GetError() const;
     /// Return connection state.
     /// Return connection state.
     WebRequestState GetState() const;
     WebRequestState GetState() const;
-    /// Return amount of bytes in the read buffer.
-    unsigned GetAvailableSize() const;
+    /// Get the HTTP verb for this request.
+    String GetVerb() const;
+
+    /// Abort the WebRequest.
+    void Abort();
 
 
     /// Return whether connection is in the open state.
     /// Return whether connection is in the open state.
     bool IsOpen() const { return GetState() == HTTP_OPEN; }
     bool IsOpen() const { return GetState() == HTTP_OPEN; }
 
 
+    /// Return whether "download_chunk" event will be fired or if only "complete" will be.
+    bool HasDownloadChunkEvent();
+
+    /// Set an HTTP request header (only works before Send has been called).
+    void SetRequestHeader(const String& header, const String& value);
+    /// Start sending the request.
+    void Send();
+    /// Get an HTTP response header.
+    StringVector GetResponseHeaderKeys();
+    /// Get an HTTP response header.
+    String GetResponseHeader(const String& header);
+    /// Get all HTTP response headers. Using GetResponseHeaderKeys() and GetResponseHeader() is more efficient than using this function.
+    String GetAllResponseHeaders();
+
 private:
 private:
-    /// Check for end of the data stream and return available size in buffer. Must only be called when the mutex is held by the main thread.
-    unsigned CheckEofAndAvailableSize();
-
-    /// URL.
-    String url_;
-    /// Verb.
-    String verb_;
-    /// Error string. Empty if no error.
-    String error_;
-    /// Headers.
-    Vector<String> headers_;
-    /// POST data.
-    String postData_;
-    /// Connection state.
-    WebRequestState state_;
-    /// Mutex for synchronizing the worker and the main thread.
-    mutable Mutex mutex_;
-    /// Read buffer for the worker thread.
-    SharedArrayPtr<unsigned char> httpReadBuffer_;
-    /// Read buffer for the main thread.
-    SharedArrayPtr<unsigned char> readBuffer_;
-    /// Read buffer read cursor.
-    unsigned readPosition_;
-    /// Read buffer write cursor.
-    unsigned writePosition_;
+#ifndef EMSCRIPTEN
+    void setup(asio::io_service *service, CURLM* curlm);
+    static void internalNotify(WebRequest *wr, int code);
+#endif
+    WebRequestInternalState* is_;
 };
 };
 
 
 }
 }