diff --git a/examples/Demo/Builds/Android/src/com/juce/jucedemo/JuceDemo.java b/examples/Demo/Builds/Android/src/com/juce/jucedemo/JuceDemo.java index 8832507f6e..8f6bf26df7 100644 --- a/examples/Demo/Builds/Android/src/com/juce/jucedemo/JuceDemo.java +++ b/examples/Demo/Builds/Android/src/com/juce/jucedemo/JuceDemo.java @@ -666,38 +666,103 @@ public final class JuceDemo extends Activity public static final HTTPStream createHTTPStream (String address, boolean isPost, byte[] postData, String headers, int timeOutMs, int[] statusCode, - StringBuffer responseHeaders) + StringBuffer responseHeaders, + int numRedirectsToFollow) { - try + // timeout parameter of zero for HttpUrlConnection is a blocking connect (negative value for juce::URL) + if (timeOutMs < 0) + timeOutMs = 0; + else if (timeOutMs == 0) + timeOutMs = 30000; + + // headers - if not empty, this string is appended onto the headers that are used for the request. It must therefore be a valid set of HTML header directives, separated by newlines. + // So convert headers string to an array, with an element for each line + String headerLines[] = headers.split("\\n"); + + for (;;) { - HttpURLConnection connection = (HttpURLConnection) (new URL(address) - .openConnection()); - if (connection != null) + try { - try + HttpURLConnection connection = (HttpURLConnection) (new URL(address).openConnection()); + + if (connection != null) { - if (isPost) + try { - connection.setRequestMethod("POST"); - connection.setConnectTimeout(timeOutMs); - connection.setDoOutput(true); - connection.setChunkedStreamingMode(0); - OutputStream out = connection.getOutputStream(); - out.write(postData); - out.flush(); - } + connection.setInstanceFollowRedirects (false); + connection.setConnectTimeout (timeOutMs); + connection.setReadTimeout (timeOutMs); - return new HTTPStream (connection, statusCode, responseHeaders); - } - catch (Throwable e) - { - connection.disconnect(); + // Set request headers + for (int i = 0; i < headerLines.length; ++i) + { + int pos = headerLines[i].indexOf (":"); + + if (pos > 0 && pos < headerLines[i].length()) + { + String field = headerLines[i].substring (0, pos); + String value = headerLines[i].substring (pos + 1); + + if (value.length() > 0) + connection.setRequestProperty (field, value); + } + } + + if (isPost) + { + connection.setRequestMethod ("POST"); + connection.setDoOutput (true); + + if (postData != null) + { + OutputStream out = connection.getOutputStream(); + out.write(postData); + out.flush(); + } + } + + HTTPStream httpStream = new HTTPStream (connection, statusCode, responseHeaders); + + // Process redirect & continue as necessary + int status = statusCode[0]; + + if (--numRedirectsToFollow >= 0 + && (status == 301 || status == 302 || status == 303 || status == 307)) + { + // Assumes only one occurrence of "Location" + int pos1 = responseHeaders.indexOf ("Location:") + 10; + int pos2 = responseHeaders.indexOf ("\n", pos1); + + if (pos2 > pos1) + { + String newLocation = responseHeaders.substring(pos1, pos2); + // Handle newLocation whether it's absolute or relative + URL baseUrl = new URL (address); + URL newUrl = new URL (baseUrl, newLocation); + String transformedNewLocation = newUrl.toString(); + + if (transformedNewLocation != address) + { + address = transformedNewLocation; + // Clear responseHeaders before next iteration + responseHeaders.delete (0, responseHeaders.length()); + continue; + } + } + } + + return httpStream; + } + catch (Throwable e) + { + connection.disconnect(); + } } } - } - catch (Throwable e) {} + catch (Throwable e) {} - return null; + return null; + } } public final void launchURL (String url) diff --git a/examples/Demo/JuceLibraryCode/AppConfig.h b/examples/Demo/JuceLibraryCode/AppConfig.h index 7287e3426c..af5e76555e 100644 --- a/examples/Demo/JuceLibraryCode/AppConfig.h +++ b/examples/Demo/JuceLibraryCode/AppConfig.h @@ -53,6 +53,10 @@ extern bool juceDemoRepaintDebuggingActive; //#define JUCE_WASAPI #endif +#ifndef JUCE_WASAPI_EXCLUSIVE + //#define JUCE_WASAPI_EXCLUSIVE +#endif + #ifndef JUCE_DIRECTSOUND //#define JUCE_DIRECTSOUND #endif diff --git a/modules/juce_core/native/java/JuceAppActivity.java b/modules/juce_core/native/java/JuceAppActivity.java index 882d0e0c12..533f222b5c 100644 --- a/modules/juce_core/native/java/JuceAppActivity.java +++ b/modules/juce_core/native/java/JuceAppActivity.java @@ -666,38 +666,103 @@ public final class JuceAppActivity extends Activity public static final HTTPStream createHTTPStream (String address, boolean isPost, byte[] postData, String headers, int timeOutMs, int[] statusCode, - StringBuffer responseHeaders) + StringBuffer responseHeaders, + int numRedirectsToFollow) { - try + // timeout parameter of zero for HttpUrlConnection is a blocking connect (negative value for juce::URL) + if (timeOutMs < 0) + timeOutMs = 0; + else if (timeOutMs == 0) + timeOutMs = 30000; + + // headers - if not empty, this string is appended onto the headers that are used for the request. It must therefore be a valid set of HTML header directives, separated by newlines. + // So convert headers string to an array, with an element for each line + String headerLines[] = headers.split("\\n"); + + for (;;) { - HttpURLConnection connection = (HttpURLConnection) (new URL(address) - .openConnection()); - if (connection != null) + try { - try + HttpURLConnection connection = (HttpURLConnection) (new URL(address).openConnection()); + + if (connection != null) { - if (isPost) + try { - connection.setRequestMethod("POST"); - connection.setConnectTimeout(timeOutMs); - connection.setDoOutput(true); - connection.setChunkedStreamingMode(0); - OutputStream out = connection.getOutputStream(); - out.write(postData); - out.flush(); - } + connection.setInstanceFollowRedirects (false); + connection.setConnectTimeout (timeOutMs); + connection.setReadTimeout (timeOutMs); - return new HTTPStream (connection, statusCode, responseHeaders); - } - catch (Throwable e) - { - connection.disconnect(); + // Set request headers + for (int i = 0; i < headerLines.length; ++i) + { + int pos = headerLines[i].indexOf (":"); + + if (pos > 0 && pos < headerLines[i].length()) + { + String field = headerLines[i].substring (0, pos); + String value = headerLines[i].substring (pos + 1); + + if (value.length() > 0) + connection.setRequestProperty (field, value); + } + } + + if (isPost) + { + connection.setRequestMethod ("POST"); + connection.setDoOutput (true); + + if (postData != null) + { + OutputStream out = connection.getOutputStream(); + out.write(postData); + out.flush(); + } + } + + HTTPStream httpStream = new HTTPStream (connection, statusCode, responseHeaders); + + // Process redirect & continue as necessary + int status = statusCode[0]; + + if (--numRedirectsToFollow >= 0 + && (status == 301 || status == 302 || status == 303 || status == 307)) + { + // Assumes only one occurrence of "Location" + int pos1 = responseHeaders.indexOf ("Location:") + 10; + int pos2 = responseHeaders.indexOf ("\n", pos1); + + if (pos2 > pos1) + { + String newLocation = responseHeaders.substring(pos1, pos2); + // Handle newLocation whether it's absolute or relative + URL baseUrl = new URL (address); + URL newUrl = new URL (baseUrl, newLocation); + String transformedNewLocation = newUrl.toString(); + + if (transformedNewLocation != address) + { + address = transformedNewLocation; + // Clear responseHeaders before next iteration + responseHeaders.delete (0, responseHeaders.length()); + continue; + } + } + } + + return httpStream; + } + catch (Throwable e) + { + connection.disconnect(); + } } } - } - catch (Throwable e) {} + catch (Throwable e) {} - return null; + return null; + } } public final void launchURL (String url) diff --git a/modules/juce_core/native/juce_android_JNIHelpers.h b/modules/juce_core/native/juce_android_JNIHelpers.h index 8e8b07d3ad..8f47eb1c09 100644 --- a/modules/juce_core/native/juce_android_JNIHelpers.h +++ b/modules/juce_core/native/juce_android_JNIHelpers.h @@ -378,7 +378,7 @@ struct AndroidThreadScope METHOD (setClipboardContent, "setClipboardContent", "(Ljava/lang/String;)V") \ METHOD (excludeClipRegion, "excludeClipRegion", "(Landroid/graphics/Canvas;FFFF)V") \ METHOD (renderGlyph, "renderGlyph", "(CLandroid/graphics/Paint;Landroid/graphics/Matrix;Landroid/graphics/Rect;)[I") \ - STATICMETHOD (createHTTPStream, "createHTTPStream", "(Ljava/lang/String;Z[BLjava/lang/String;I[ILjava/lang/StringBuffer;)L" JUCE_ANDROID_ACTIVITY_CLASSPATH "$HTTPStream;") \ + STATICMETHOD (createHTTPStream, "createHTTPStream", "(Ljava/lang/String;Z[BLjava/lang/String;I[ILjava/lang/StringBuffer;I)L" JUCE_ANDROID_ACTIVITY_CLASSPATH "$HTTPStream;") \ METHOD (launchURL, "launchURL", "(Ljava/lang/String;)V") \ METHOD (showMessageBox, "showMessageBox", "(Ljava/lang/String;Ljava/lang/String;J)V") \ METHOD (showOkCancelBox, "showOkCancelBox", "(Ljava/lang/String;Ljava/lang/String;J)V") \ diff --git a/modules/juce_core/native/juce_android_Network.cpp b/modules/juce_core/native/juce_android_Network.cpp index 29a8cb5919..46d13fe10c 100644 --- a/modules/juce_core/native/juce_android_Network.cpp +++ b/modules/juce_core/native/juce_android_Network.cpp @@ -70,7 +70,7 @@ class WebInputStream : public InputStream public: WebInputStream (String address, bool isPost, const MemoryBlock& postData, URL::OpenStreamProgressCallback* progressCallback, void* progressCallbackContext, - const String& headers, int timeOutMs, StringPairArray* responseHeaders) + const String& headers, int timeOutMs, StringPairArray* responseHeaders, const int numRedirectsToFollow) : statusCode (0) { if (! address.contains ("://")) @@ -103,7 +103,8 @@ public: javaString (headers).get(), (jint) timeOutMs, statusCodeArray, - responseHeaderBuffer.get())); + responseHeaderBuffer.get(), + (jint) numRedirectsToFollow)); jint* const statusCodeElements = env->GetIntArrayElements (statusCodeArray, 0); statusCode = statusCodeElements[0]; diff --git a/modules/juce_core/native/juce_linux_Network.cpp b/modules/juce_core/native/juce_linux_Network.cpp index 6871aa5469..d077668592 100644 --- a/modules/juce_core/native/juce_linux_Network.cpp +++ b/modules/juce_core/native/juce_linux_Network.cpp @@ -74,12 +74,13 @@ class WebInputStream : public InputStream public: WebInputStream (const String& address_, bool isPost_, const MemoryBlock& postData_, URL::OpenStreamProgressCallback* progressCallback, void* progressCallbackContext, - const String& headers_, int timeOutMs_, StringPairArray* responseHeaders) + const String& headers_, int timeOutMs_, StringPairArray* responseHeaders, + const int maxRedirects) : statusCode (0), socketHandle (-1), levelsOfRedirection (0), address (address_), headers (headers_), postData (postData_), position (0), - finished (false), isPost (isPost_), timeOutMs (timeOutMs_) + finished (false), isPost (isPost_), timeOutMs (timeOutMs_), numRedirectsToFollow (maxRedirects) { - statusCode = createConnection (progressCallback, progressCallbackContext); + statusCode = createConnection (progressCallback, progressCallbackContext, numRedirectsToFollow); if (responseHeaders != nullptr && ! isError()) { @@ -147,7 +148,7 @@ public: { closeSocket(); position = 0; - statusCode = createConnection (0, 0); + statusCode = createConnection (0, 0, numRedirectsToFollow); } skipNextBytes (wantedPos - position); @@ -168,24 +169,27 @@ private: bool finished; const bool isPost; const int timeOutMs; + const int numRedirectsToFollow; - void closeSocket() + void closeSocket (bool resetLevelsOfRedirection = true) { if (socketHandle >= 0) close (socketHandle); socketHandle = -1; - levelsOfRedirection = 0; + if (resetLevelsOfRedirection) + levelsOfRedirection = 0; } - int createConnection (URL::OpenStreamProgressCallback* progressCallback, void* progressCallbackContext) + int createConnection (URL::OpenStreamProgressCallback* progressCallback, void* progressCallbackContext, + const int numRedirectsToFollow) { - closeSocket(); + closeSocket (false); uint32 timeOutTime = Time::getMillisecondCounter(); if (timeOutMs == 0) - timeOutTime += 60000; + timeOutTime += 30000; else if (timeOutMs < 0) timeOutTime = 0xffffffff; else @@ -273,28 +277,28 @@ private: const int status = responseHeader.fromFirstOccurrenceOf (" ", false, false) .substring (0, 3).getIntValue(); - //int contentLength = findHeaderItem (lines, "Content-Length:").getIntValue(); - //bool isChunked = findHeaderItem (lines, "Transfer-Encoding:").equalsIgnoreCase ("chunked"); - String location (findHeaderItem (headerLines, "Location:")); - if (status >= 300 && status < 400 + if (++levelsOfRedirection <= numRedirectsToFollow + && status >= 300 && status < 400 && location.isNotEmpty() && location != address) { - if (! location.startsWithIgnoreCase ("http://")) - location = "http://" + location; - - if (++levelsOfRedirection <= 3) + if (! (location.startsWithIgnoreCase ("http://") + || location.startsWithIgnoreCase ("https://") + || location.startsWithIgnoreCase ("ftp://"))) { - address = location; - return createConnection (progressCallback, progressCallbackContext); + // The following is a bit dodgy. Ideally, we should do a proper transform of the relative URI to a target URI + if (location.startsWithChar ('/')) + location = URL (address).withNewSubPath (location).toString (true); + else + location = address + "/" + location; } + + address = location; + return createConnection (progressCallback, progressCallbackContext, numRedirectsToFollow); } - else - { - levelsOfRedirection = 0; - return status; - } + + return status; } closeSocket(); @@ -363,10 +367,14 @@ private: writeValueIfNotPresent (header, userHeaders, "Connection:", "close"); if (isPost) + { writeValueIfNotPresent (header, userHeaders, "Content-Length:", String ((int) postData.getSize())); - - header << "\r\n" << userHeaders - << "\r\n" << postData; + header << userHeaders << "\r\n" << postData; + } + else + { + header << "\r\n" << userHeaders << "\r\n"; + } return header.getMemoryBlock(); } diff --git a/modules/juce_core/native/juce_mac_Network.mm b/modules/juce_core/native/juce_mac_Network.mm index 130759e6d6..a0ddef5729 100644 --- a/modules/juce_core/native/juce_mac_Network.mm +++ b/modules/juce_core/native/juce_mac_Network.mm @@ -115,7 +115,7 @@ bool JUCE_CALLTYPE Process::openEmailWithAttachments (const String& targetEmailA class URLConnectionState : public Thread { public: - URLConnectionState (NSURLRequest* req) + URLConnectionState (NSURLRequest* req, const int maxRedirects) : Thread ("http connection"), contentLength (-1), delegate (nil), @@ -126,7 +126,9 @@ public: statusCode (0), initialised (false), hasFailed (false), - hasFinished (false) + hasFinished (false), + numRedirectsToFollow (maxRedirects), + numRedirects (0) { static DelegateClass cls; delegate = [cls.createInstance() init]; @@ -215,6 +217,19 @@ public: } } + NSURLRequest* willSendRequest (NSURLRequest* newRequest, NSURLResponse* redirectResponse) + { + if (redirectResponse != nullptr) + { + if (numRedirects >= numRedirectsToFollow) + return nil; // Cancel redirect and allow connection to continue + + ++numRedirects; + } + + return newRequest; + } + void didFailWithError (NSError* error) { DBG (nsStringToJuce ([error description])); (void) error; @@ -263,6 +278,8 @@ public: NSDictionary* headers; int statusCode; bool initialised, hasFailed, hasFinished; + const int numRedirectsToFollow; + int numRedirects; private: //============================================================================== @@ -278,7 +295,7 @@ private: addMethod (@selector (connection:didSendBodyData:totalBytesWritten:totalBytesExpectedToWrite:), connectionDidSendBodyData, "v@:@iii"); addMethod (@selector (connectionDidFinishLoading:), connectionDidFinishLoading, "v@:@"); - addMethod (@selector (connection:willSendRequest:redirectResponse:), willSendRequest, "@@:@@"); + addMethod (@selector (connection:willSendRequest:redirectResponse:), willSendRequest, "@@:@@@"); registerClass(); } @@ -302,9 +319,9 @@ private: getState (self)->didReceiveData (newData); } - static NSURLRequest* willSendRequest (id, SEL, NSURLConnection*, NSURLRequest* request, NSURLResponse*) + static NSURLRequest* willSendRequest (id self, SEL, NSURLConnection*, NSURLRequest* request, NSURLResponse* response) { - return request; + return getState (self)->willSendRequest (request, response); } static void connectionDidSendBodyData (id self, SEL, NSURLConnection*, NSInteger, NSInteger totalBytesWritten, NSInteger totalBytesExpected) @@ -328,23 +345,27 @@ class WebInputStream : public InputStream public: WebInputStream (const String& address_, bool isPost_, const MemoryBlock& postData_, URL::OpenStreamProgressCallback* progressCallback, void* progressCallbackContext, - const String& headers_, int timeOutMs_, StringPairArray* responseHeaders) + const String& headers_, int timeOutMs_, StringPairArray* responseHeaders, + const int numRedirectsToFollow_) : statusCode (0), address (address_), headers (headers_), postData (postData_), position (0), - finished (false), isPost (isPost_), timeOutMs (timeOutMs_) + finished (false), isPost (isPost_), timeOutMs (timeOutMs_), numRedirectsToFollow (numRedirectsToFollow_) { JUCE_AUTORELEASEPOOL { createConnection (progressCallback, progressCallbackContext); - if (responseHeaders != nullptr && connection != nullptr && connection->headers != nil) + if (connection != nullptr && connection->headers != nil) { statusCode = connection->statusCode; - NSEnumerator* enumerator = [connection->headers keyEnumerator]; + if (responseHeaders != nullptr) + { + NSEnumerator* enumerator = [connection->headers keyEnumerator]; - while (NSString* key = [enumerator nextObject]) - responseHeaders->set (nsStringToJuce (key), - nsStringToJuce ((NSString*) [connection->headers objectForKey: key])); + while (NSString* key = [enumerator nextObject]) + responseHeaders->set (nsStringToJuce (key), + nsStringToJuce ((NSString*) [connection->headers objectForKey: key])); + } } } } @@ -403,6 +424,7 @@ private: bool finished; const bool isPost; const int timeOutMs; + const int numRedirectsToFollow; void createConnection (URL::OpenStreamProgressCallback* progressCallback, void* progressCallbackContext) { @@ -434,7 +456,7 @@ private: [req setHTTPBody: [NSData dataWithBytes: postData.getData() length: postData.getSize()]]; - connection = new URLConnectionState (req); + connection = new URLConnectionState (req, numRedirectsToFollow); if (! connection->start (progressCallback, progressCallbackContext)) connection = nullptr; diff --git a/modules/juce_core/native/juce_win32_Network.cpp b/modules/juce_core/native/juce_win32_Network.cpp index f0b6061b41..7e555cc529 100644 --- a/modules/juce_core/native/juce_win32_Network.cpp +++ b/modules/juce_core/native/juce_win32_Network.cpp @@ -40,12 +40,12 @@ class WebInputStream : public InputStream public: WebInputStream (const String& address_, bool isPost_, const MemoryBlock& postData_, URL::OpenStreamProgressCallback* progressCallback, void* progressCallbackContext, - const String& headers_, int timeOutMs_, StringPairArray* responseHeaders) + const String& headers_, int timeOutMs_, StringPairArray* responseHeaders, int numRedirectsToFollow) : statusCode (0), connection (0), request (0), address (address_), headers (headers_), postData (postData_), position (0), finished (false), isPost (isPost_), timeOutMs (timeOutMs_) { - for (int maxRedirects = 10; --maxRedirects >= 0;) + while (numRedirectsToFollow-- >= 0) { createConnection (progressCallback, progressCallbackContext); @@ -88,9 +88,22 @@ public: { statusCode = (int) status; - if (status == 301 || status == 302 || status == 303 || status == 307) + if (numRedirectsToFollow >= 0 + && (statusCode == 301 || statusCode == 302 || statusCode == 303 || statusCode == 307)) { - const String newLocation (headers["Location"]); + String newLocation (headers["Location"]); + + // Check whether location is a relative URI - this is an incomplete test for relative path, + // but we'll use it for now (valid protocols for this implementation are http, https & ftp) + if (! (newLocation.startsWithIgnoreCase ("http://") + || newLocation.startsWithIgnoreCase ("https://") + || newLocation.startsWithIgnoreCase ("ftp://"))) + { + if (newLocation.startsWithChar ('/')) + newLocation = URL (address).withNewSubPath (newLocation).toString (true); + else + newLocation = address + "/" + newLocation; + } if (newLocation.isNotEmpty() && newLocation != address) { diff --git a/modules/juce_core/network/juce_URL.cpp b/modules/juce_core/network/juce_URL.cpp index a4cc02e726..f6f8620f9b 100644 --- a/modules/juce_core/network/juce_URL.cpp +++ b/modules/juce_core/network/juce_URL.cpp @@ -334,7 +334,8 @@ InputStream* URL::createInputStream (const bool usePostCommand, String headers, const int timeOutMs, StringPairArray* const responseHeaders, - int* statusCode) const + int* statusCode, + const int numRedirectsToFollow) const { MemoryBlock headersAndPostData; @@ -350,7 +351,8 @@ InputStream* URL::createInputStream (const bool usePostCommand, ScopedPointer wi (new WebInputStream (toString (! usePostCommand), usePostCommand, headersAndPostData, progressCallback, progressCallbackContext, - headers, timeOutMs, responseHeaders)); + headers, timeOutMs, responseHeaders, + numRedirectsToFollow)); if (statusCode != nullptr) *statusCode = wi->statusCode; diff --git a/modules/juce_core/network/juce_URL.h b/modules/juce_core/network/juce_URL.h index 697ce0c5de..d3a87c1344 100644 --- a/modules/juce_core/network/juce_URL.h +++ b/modules/juce_core/network/juce_URL.h @@ -266,6 +266,8 @@ public: in the response will be stored in this array @param statusCode if this is non-null, it will get set to the http status code, if one is known, or 0 if a code isn't available + @param numRedirectsToFollow specifies the number of redirects that will be followed before + returning a response (ignored for Android which follows up to 5 redirects) @returns an input stream that the caller must delete, or a null pointer if there was an error trying to open it. */ @@ -275,7 +277,8 @@ public: String extraHeaders = String(), int connectionTimeOutMs = 0, StringPairArray* responseHeaders = nullptr, - int* statusCode = nullptr) const; + int* statusCode = nullptr, + int numRedirectsToFollow = 5) const; //==============================================================================