@@ -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) | |||
@@ -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 | |||
@@ -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) | |||
@@ -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") \ | |||
@@ -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]; | |||
@@ -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(); | |||
} | |||
@@ -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; | |||
@@ -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) | |||
{ | |||
@@ -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<WebInputStream> wi (new WebInputStream (toString (! usePostCommand), | |||
usePostCommand, headersAndPostData, | |||
progressCallback, progressCallbackContext, | |||
headers, timeOutMs, responseHeaders)); | |||
headers, timeOutMs, responseHeaders, | |||
numRedirectsToFollow)); | |||
if (statusCode != nullptr) | |||
*statusCode = wi->statusCode; | |||
@@ -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; | |||
//============================================================================== | |||