Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Browse files

Merge commit '14e42a1ea3ad4a15621f2e909777ae7901fd90b5' into stable

  • Loading branch information...
commit 50ec45ff723c359765b412d5067c9e9c44139abd 2 parents 70a04dd + 14e42a1
@snej snej authored
Showing with 914 additions and 281 deletions.
  1. +3 −1 Listener/TDHTTPConnection.m
  2. +2 −1  Listener/TDHTTPResponse.h
  3. +19 −7 Listener/TDHTTPResponse.m
  4. +12 −4 README.md
  5. +2 −1  Source/ChangeTracker/TDChangeTracker.m
  6. +70 −27 Source/ChangeTracker/TDConnectionChangeTracker.m
  7. +4 −0 Source/TDCanonicalJSON.h
  8. +6 −2 Source/TDCanonicalJSON.m
  9. +93 −50 Source/TDCollateJSON.m
  10. +7 −0 Source/TDDatabase+Attachments.h
  11. +40 −18 Source/TDDatabase+Attachments.m
  12. +4 −5 Source/TDDatabase.h
  13. +3 −12 Source/TDDatabase.m
  14. +18 −11 Source/TDDatabase_Tests.m
  15. +17 −2 Source/TDMultipartDocumentReader.h
  16. +81 −1 Source/TDMultipartDocumentReader.m
  17. +1 −2  Source/TDMultipartWriter.h
  18. +25 −20 Source/TDMultipartWriter.m
  19. +18 −6 Source/TDPusher.m
  20. +3 −0  Source/TDRemoteRequest.h
  21. +49 −9 Source/TDRemoteRequest.m
  22. +28 −0 Source/TDReplicatorManager.m
  23. +3 −2 Source/TDReplicator_Tests.m
  24. +94 −46 Source/TDRouter+Handlers.m
  25. +9 −2 Source/TDRouter.h
  26. +164 −34 Source/TDRouter.m
  27. +44 −8 Source/TDRouter_Tests.m
  28. +7 −0 Source/TDServer.h
  29. +11 −0 Source/TDServer.m
  30. +4 −0 Source/TDURLProtocol.h
  31. +35 −3 Source/TDURLProtocol.m
  32. +2 −0  Source/TDView.h
  33. +10 −3 Source/TDView.m
  34. +1 −1  Source/TDView_Tests.m
  35. +24 −2 TouchDB.xcodeproj/project.pbxproj
  36. +1 −1  vendor/MYUtilities
View
4 Listener/TDHTTPConnection.m
@@ -72,7 +72,9 @@ - (BOOL)supportsMethod:(NSString *)method atPath:(NSString *)path {
// Create a TDRouter:
TDRouter* router = [[TDRouter alloc] initWithServer: ((TDHTTPServer*)config.server).tdServer
- request: urlRequest];
+ request: urlRequest
+ isLocal: NO];
+ router.processRanges = NO; // The HTTP server framework does this already
TDHTTPResponse* response = [[[TDHTTPResponse alloc] initWithRouter: router
forConnection: self] autorelease];
View
3  Listener/TDHTTPResponse.h
@@ -19,7 +19,8 @@
BOOL _askedIfChunked;
BOOL _chunked;
BOOL _delayedHeaders;
- NSMutableData* _data; // Data received, waiting to be read by the connection
+ NSData* _data; // Data received, waiting to be read by the connection
+ BOOL _dataMutable; // Is _data an NSMutableData?
UInt64 _dataOffset; // Offset in response of 1st byte of _data
UInt64 _offset; // Offset in response for next readData
}
View
26 Listener/TDHTTPResponse.m
@@ -36,6 +36,8 @@ @implementation TDHTTPResponse
- (id) initWithRouter: (TDRouter*)router forConnection:(TDHTTPConnection*)connection {
self = [super init];
if (self) {
+ EnableLog(YES);
+ EnableLogTo(TDListenerVerbose, YES);
_router = [router retain];
_connection = connection;
router.onResponseReady = ^(TDResponse* r) {
@@ -130,10 +132,17 @@ - (NSDictionary *) httpHeaders {
- (void) onDataAvailable: (NSData*)data finished: (BOOL)finished {
@synchronized(self) {
LogTo(TDListenerVerbose, @"%@ adding %u bytes", self, (unsigned)data.length);
- if (_data)
- [_data appendData: data];
- else
- _data = [data mutableCopy];
+ if (!_data) {
+ _data = [data copy];
+ _dataMutable = NO;
+ } else {
+ if (!_dataMutable) {
+ [_data autorelease];
+ _data = [_data mutableCopy];
+ _dataMutable = YES;
+ }
+ [(NSMutableData*)_data appendData: data];
+ }
if (finished)
[self onFinished];
else if (_chunked)
@@ -206,7 +215,7 @@ - (void) onFinished {
LogTo(TDListenerVerbose, @"%@ Finished!", self);
- if (!_chunked || _offset == 0) {
+ if ((!_chunked || _offset == 0) && ![_router.request.HTTPMethod isEqualToString: @"HEAD"]) {
// Response finished immediately, before the connection asked for any data, so we're free
// to massage the response:
LogTo(TDListenerVerbose, @"%@ prettifying response body", self);
@@ -216,8 +225,11 @@ - (void) onFinished {
BOOL pretty = [_router boolQuery: @"pretty"];
#endif
if (pretty) {
- [_data release];
- _data = [_response.body.asPrettyJSON mutableCopy];
+ NSString* contentType = [_response.headers objectForKey: @"Content-Type"];
+ if ([contentType hasPrefix: @"application/json"] && _data.length < 100000) {
+ [_data release];
+ _data = [_response.body.asPrettyJSON mutableCopy];
+ }
}
}
[_connection responseHasAvailableData: self];
View
16 README.md
@@ -17,7 +17,11 @@ By "_suitable for embedding into mobile apps_", I mean that it meets the followi
And by "_mobile apps_" I'm focusing on iOS and [Android][11], although there's no reason we couldn't extend this to other platforms like Windows Phone. And it's not limited to mobile OSs -- the Objective-C implementation runs on Mac OS as well, and on Linux and other Unix-like OSs via [GNUstep][12].
-More documentation is available on the [wiki][2].
+## More Information
+
+* There's lots more information on the [wiki][2].
+* There's a "Grocery Sync" [demo app][18] for iOS, that implements a simple shared to-do list.
+* Or if you want to ask questions or get help, join the [mailing list][17].
## Platforms ##
@@ -43,7 +47,7 @@ More documentation is available on the [wiki][2].
## Development Status ##
-TouchDB went beta in June 2012. The current stable release is [version 0.9][16].
+TouchDB went beta in June 2012. The current stable release is beta 3, [version 0.92][16].
We don't have a formal schedule for 1.0, but expect the blessed event by the end of summer.
@@ -51,6 +55,8 @@ We don't have a formal schedule for 1.0, but expect the blessed event by the end
### On a Mac ###
+(You might prefer to just [download][16] the latest stable release. But if you want to build it yourself...)
+
For full details see the [wiki page][7]. The basic steps are:
1. Clone the TouchDB repository to your local disk.
@@ -65,7 +71,7 @@ Please refer to the files [BUILDING.txt][14] and [SETUP.txt][15] in the `GNUstep
[2]: https://github.com/couchbaselabs/TouchDB-iOS/wiki
[3]: http://couchbase.com
[4]: https://github.com/couchbaselabs/CouchCocoa
-[5]: https://github.com/touchbaselabs/fmdb
+[5]: https://github.com/couchbaselabs/fmdb
[6]: https://bitbucket.org/snej/myutilities/overview
[7]: https://github.com/couchbaselabs/TouchDB-iOS/wiki/Building-TouchDB
[8]: https://github.com/ccgus/
@@ -76,4 +82,6 @@ Please refer to the files [BUILDING.txt][14] and [SETUP.txt][15] in the `GNUstep
[13]: http://wiki.gnustep.org/index.php/Platform_compatibility
[14]: https://github.com/couchbaselabs/TouchDB-iOS/blob/master/GNUstep/BUILDING.txt
[15]: https://github.com/couchbaselabs/TouchDB-iOS/blob/master/GNUstep/SETUP.txt
-[16]: https://github.com/couchbaselabs/TouchDB-iOS/downloads
+[16]: https://github.com/couchbaselabs/TouchDB-iOS/downloads
+[17]: https://groups.google.com/forum/?fromgroups#!forum/mobile-couchbase
+[18]: https://github.com/couchbaselabs/iOS-Couchbase-Demo
View
3  Source/ChangeTracker/TDChangeTracker.m
@@ -149,8 +149,9 @@ - (void) stopped {
- (void) failedWithError: (NSError*)error {
// If the error may be transient (flaky network, server glitch), retry:
if (TDMayBeTransientError(error)) {
- NSTimeInterval retryDelay = kInitialRetryDelay * (1 << MIN(_retryCount-1, 31U));
+ NSTimeInterval retryDelay = kInitialRetryDelay * (1 << MIN(_retryCount, 16U));
retryDelay = MIN(retryDelay, kMaxRetryDelay);
+ ++_retryCount;
Log(@"%@: Connection error, retrying in %.1f sec: %@",
self, retryDelay, error.localizedDescription);
[self performSelector: @selector(retry) withObject: nil afterDelay: retryDelay];
View
97 Source/ChangeTracker/TDConnectionChangeTracker.m
@@ -17,11 +17,15 @@
#import "TDConnectionChangeTracker.h"
#import "TDAuthorizer.h"
+#import "TDRemoteRequest.h"
#import "TDMisc.h"
#import "TDStatus.h"
#import "MYURLUtils.h"
+static SecTrustRef CopyTrustWithPolicy(SecTrustRef trust, SecPolicyRef policy);
+
+
@implementation TDConnectionChangeTracker
- (BOOL) start {
@@ -105,10 +109,36 @@ - (void)connection:(NSURLConnection *)connection
willSendRequestForAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge
{
id<NSURLAuthenticationChallengeSender> sender = challenge.sender;
- NSString* authMethod = [[challenge protectionSpace] authenticationMethod];
+ NSURLProtectionSpace* space = challenge.protectionSpace;
+ NSString* authMethod = space.authenticationMethod;
+
+ // Is this challenge for the DB hostname with the "." appended (the one in the URL request)?
+ BOOL challengeIsForDottedHost = NO;
+ NSString* host = space.host;
+ if ([host hasSuffix: @"."] && !space.isProxy) {
+ NSString* hostWithoutDot = [host substringToIndex: host.length - 1];
+ challengeIsForDottedHost = ([hostWithoutDot caseInsensitiveCompare: _databaseURL.host] == 0);
+ }
+
if ($equal(authMethod, NSURLAuthenticationMethodServerTrust)) {
- // TODO: Check trust of server cert
- [sender performDefaultHandlingForAuthenticationChallenge: challenge];
+ // Verify trust of SSL server cert:
+ SecTrustRef trust = challenge.protectionSpace.serverTrust;
+ if (challengeIsForDottedHost) {
+ // Update the policy with the correct original hostname (without the "." suffix):
+ host = _databaseURL.host;
+ SecPolicyRef policy = SecPolicyCreateSSL(YES, (CFStringRef)host);
+ trust = CopyTrustWithPolicy(trust, policy);
+ CFRelease(policy);
+ } else {
+ CFRetain(trust);
+ }
+ if ([TDRemoteRequest checkTrust: trust forHost: host]) {
+ [sender useCredential: [NSURLCredential credentialForTrust: trust]
+ forAuthenticationChallenge: challenge];
+ } else {
+ [sender cancelAuthenticationChallenge: challenge];
+ }
+ CFRelease(trust);
return;
}
@@ -118,30 +148,26 @@ - (void)connection:(NSURLConnection *)connection
return;
}
- NSURLProtectionSpace* space = challenge.protectionSpace;
- NSString* host = space.host;
- if (challenge.previousFailureCount == 0 && [host hasSuffix: @"."] && !space.isProxy) {
- NSString* hostWithoutDot = [host substringToIndex: host.length - 1];
- if ([hostWithoutDot caseInsensitiveCompare: _databaseURL.host] == 0) {
- // Challenge is for the hostname with the "." appended. Try without it:
- host = hostWithoutDot;
- NSURLProtectionSpace* newSpace = [[NSURLProtectionSpace alloc]
- initWithHost: host
- port: space.port
- protocol: space.protocol
- realm: space.realm
- authenticationMethod: space.authenticationMethod];
- NSURLCredential* cred = [[NSURLCredentialStorage sharedCredentialStorage]
- defaultCredentialForProtectionSpace: newSpace];
- [newSpace release];
- if (cred) {
- LogTo(ChangeTracker, @"%@: Using credential '%@' for "
- "{host=<%@>, port=%d, protocol=%@ realm=%@ method=%@}",
- self, cred.user, host, (int)space.port, space.protocol, space.realm,
- space.authenticationMethod);
- [sender useCredential: cred forAuthenticationChallenge: challenge];
- return;
- }
+ if (challengeIsForDottedHost && challenge.previousFailureCount == 0) {
+ // Look up a credential for the original hostname without the "." suffix:
+ host = _databaseURL.host;
+ NSURLProtectionSpace* newSpace = [[NSURLProtectionSpace alloc]
+ initWithHost: host
+ port: space.port
+ protocol: space.protocol
+ realm: space.realm
+ authenticationMethod: space.authenticationMethod];
+ NSURLCredential* cred = [[NSURLCredentialStorage sharedCredentialStorage]
+ defaultCredentialForProtectionSpace: newSpace];
+ [newSpace release];
+ if (cred) {
+ // Found a credential, so use it:
+ LogTo(ChangeTracker, @"%@: Using credential '%@' for "
+ "{host=<%@>, port=%d, protocol=%@ realm=%@ method=%@}",
+ self, cred.user, host, (int)space.port, space.protocol, space.realm,
+ space.authenticationMethod);
+ [sender useCredential: cred forAuthenticationChallenge: challenge];
+ return;
}
}
@@ -218,3 +244,20 @@ - (void)connectionDidFinishLoading:(NSURLConnection *)connection {
}
@end
+
+
+static SecTrustRef CopyTrustWithPolicy(SecTrustRef trust, SecPolicyRef policy) {
+#if TARGET_OS_IPHONE
+ CFIndex nCerts = SecTrustGetCertificateCount(trust);
+ CFMutableArrayRef certs = CFArrayCreateMutable(NULL, nCerts, &kCFTypeArrayCallBacks);
+ for (CFIndex i = 0; i < nCerts; ++i)
+ CFArrayAppendValue(certs, SecTrustGetCertificateAtIndex(trust, i));
+ OSStatus err = SecTrustCreateWithCertificates(certs, policy, &trust);
+ CAssertEq(err, noErr);
+ return trust;
+#else
+ SecTrustSetPolicies(trust, policy);
+ CFRetain(trust);
+ return trust;
+#endif
+}
View
4 Source/TDCanonicalJSON.h
@@ -41,4 +41,8 @@
/** Convenience method that instantiates a TDCanonicalJSON object and uses it to encode the object, returning a string. */
+ (NSString*) canonicalString: (id)rootObject;
+
+/** Returns a dictionary's keys in the same order in which they would be written out in canonical JSON. */
++ (NSArray*) orderedKeys: (NSDictionary*)dict;
+
@end
View
8 Source/TDCanonicalJSON.m
@@ -136,11 +136,15 @@ static NSComparisonResult compareCanonStrings( id s1, id s2, void *context) {
}
++ (NSArray*) orderedKeys: (NSDictionary*)dict {
+ return [[dict allKeys] sortedArrayUsingFunction: &compareCanonStrings context: NULL];
+}
+
+
- (void) encodeDictionary: (NSDictionary*)dict {
[_output appendString: @"{"];
- NSArray* keys = [[dict allKeys] sortedArrayUsingFunction: &compareCanonStrings context: NULL];
BOOL first = YES;
- for (NSString* key in keys) {
+ for (NSString* key in [[self class] orderedKeys: dict]) {
Assert([key isKindOfClass: [NSString class]], @"Can't encode %@ as dict key in JSON",
[key class]);
if (_ignoreKeyPrefix && [key hasPrefix: _ignoreKeyPrefix]
View
143 Source/TDCollateJSON.m
@@ -185,6 +185,26 @@ static int compareStringsUnicode(const char** in1, const char** in2) {
}
+static double readNumber(const char* start, const char* end, char** endOfNumber) {
+ CAssert(end > start);
+ // First copy the string into a zero-terminated buffer so we can safely call strtod:
+ size_t len = end - start;
+ char buf[50];
+ char* str = (len < sizeof(buf)) ? buf : malloc(len + 1);
+ if (!str)
+ return 0.0;
+ memcpy(str, start, len);
+ str[len] = '\0';
+
+ char* endInStr;
+ double result = strtod(str, &endInStr);
+ *endOfNumber = (char*)start + (endInStr - str);
+ if (len >= sizeof(buf))
+ free(str);
+ return result;
+}
+
+
int TDCollateJSON(void *context,
int len1, const void * chars1,
int len2, const void * chars2)
@@ -217,7 +237,15 @@ int TDCollateJSON(void *context,
break;
case kNumber: {
char* next1, *next2;
- int diff = dcmp( strtod(str1, &next1), strtod(str2, &next2) );
+ int diff;
+ if (depth == 0) {
+ // At depth 0, be careful not to fall off the end of the input, because there
+ // won't be any delimiters (']' or '}') after the number!
+ diff = dcmp( readNumber(str1, chars1 + len1, &next1),
+ readNumber(str2, chars2 + len2, &next2) );
+ } else {
+ diff = dcmp( strtod(str1, &next1), strtod(str2, &next2) );
+ }
if (diff)
return diff; // Numbers don't match
str1 = next1;
@@ -285,73 +313,88 @@ static void testEscape(const char* source, char decoded) {
testEscape("\\u0000", 0);
}
+static int collate(void *mode, const void * str1, const void * str2) {
+ // Be evil and put numeric garbage past the ends of str1 and str2 (see bug #138):
+ size_t len1 = strlen(str1), len2 = strlen(str2);
+ char buf1[len1 + 3], buf2[len2 + 3];
+ strcpy(buf1, str1);
+ strcat(buf1, "99");
+ strcpy(buf2, str2);
+ strcat(buf2, "88");
+ return TDCollateJSON(mode, (int)len1, buf1, (int)len2, buf2);
+}
+
TestCase(TDCollateScalars) {
RequireTestCase(TDCollateConvertEscape);
void* mode = kTDCollateJSON_Unicode;
- CAssertEq(TDCollateJSON(mode, 0, "true", 0, "false"), 1);
- CAssertEq(TDCollateJSON(mode, 0, "false", 0, "true"), -1);
- CAssertEq(TDCollateJSON(mode, 0, "null", 0, "17"), -1);
- CAssertEq(TDCollateJSON(mode, 0, "123", 0, "1"), 1);
- CAssertEq(TDCollateJSON(mode, 0, "123", 0, "0123.0"), 0);
- CAssertEq(TDCollateJSON(mode, 0, "123", 0, "\"123\""), -1);
- CAssertEq(TDCollateJSON(mode, 0, "\"1234\"", 0, "\"123\""), 1);
- CAssertEq(TDCollateJSON(mode, 0, "\"1234\"", 0, "\"1235\""), -1);
- CAssertEq(TDCollateJSON(mode, 0, "\"1234\"", 0, "\"1234\""), 0);
- CAssertEq(TDCollateJSON(mode, 0, "\"12\\/34\"", 0, "\"12/34\""), 0);
- CAssertEq(TDCollateJSON(mode, 0, "\"\\/1234\"", 0, "\"/1234\""), 0);
- CAssertEq(TDCollateJSON(mode, 0, "\"1234\\/\"", 0, "\"1234/\""), 0);
+ CAssertEq(collate(mode, "true", "false"), 1);
+ CAssertEq(collate(mode, "false", "true"), -1);
+ CAssertEq(collate(mode, "null", "17"), -1);
+ CAssertEq(collate(mode, "1", "1"), 0);
+ CAssertEq(collate(mode, "123", "1"), 1);
+ CAssertEq(collate(mode, "123", "0123.0"), 0);
+ CAssertEq(collate(mode, "123", "\"123\""), -1);
+ CAssertEq(collate(mode, "\"1234\"", "\"123\""), 1);
+ CAssertEq(collate(mode, "\"1234\"", "\"1235\""), -1);
+ CAssertEq(collate(mode, "\"1234\"", "\"1234\""), 0);
+ CAssertEq(collate(mode, "\"12\\/34\"", "\"12/34\""), 0);
+ CAssertEq(collate(mode, "\"\\/1234\"", "\"/1234\""), 0);
+ CAssertEq(collate(mode, "\"1234\\/\"", "\"1234/\""), 0);
#ifndef GNUSTEP // FIXME: GNUstep doesn't support Unicode collation yet
- CAssertEq(TDCollateJSON(mode, 0, "\"a\"", 0, "\"A\""), -1);
- CAssertEq(TDCollateJSON(mode, 0, "\"A\"", 0, "\"aa\""), -1);
- CAssertEq(TDCollateJSON(mode, 0, "\"B\"", 0, "\"aa\""), 1);
+ CAssertEq(collate(mode, "\"a\"", "\"A\""), -1);
+ CAssertEq(collate(mode, "\"A\"", "\"aa\""), -1);
+ CAssertEq(collate(mode, "\"B\"", "\"aa\""), 1);
#endif
}
TestCase(TDCollateASCII) {
RequireTestCase(TDCollateConvertEscape);
void* mode = kTDCollateJSON_ASCII;
- CAssertEq(TDCollateJSON(mode, 0, "true", 0, "false"), 1);
- CAssertEq(TDCollateJSON(mode, 0, "false", 0, "true"), -1);
- CAssertEq(TDCollateJSON(mode, 0, "null", 0, "17"), -1);
- CAssertEq(TDCollateJSON(mode, 0, "123", 0, "1"), 1);
- CAssertEq(TDCollateJSON(mode, 0, "123", 0, "0123.0"), 0);
- CAssertEq(TDCollateJSON(mode, 0, "123", 0, "\"123\""), -1);
- CAssertEq(TDCollateJSON(mode, 0, "\"1234\"", 0, "\"123\""), 1);
- CAssertEq(TDCollateJSON(mode, 0, "\"1234\"", 0, "\"1235\""), -1);
- CAssertEq(TDCollateJSON(mode, 0, "\"1234\"", 0, "\"1234\""), 0);
- CAssertEq(TDCollateJSON(mode, 0, "\"12\\/34\"", 0, "\"12/34\""), 0);
- CAssertEq(TDCollateJSON(mode, 0, "\"\\/1234\"", 0, "\"/1234\""), 0);
- CAssertEq(TDCollateJSON(mode, 0, "\"1234\\/\"", 0, "\"1234/\""), 0);
- CAssertEq(TDCollateJSON(mode, 0, "\"A\"", 0, "\"a\""), -1);
- CAssertEq(TDCollateJSON(mode, 0, "\"B\"", 0, "\"a\""), -1);
+ CAssertEq(collate(mode, "true", "false"), 1);
+ CAssertEq(collate(mode, "false", "true"), -1);
+ CAssertEq(collate(mode, "null", "17"), -1);
+ CAssertEq(collate(mode, "123", "1"), 1);
+ CAssertEq(collate(mode, "123", "0123.0"), 0);
+ CAssertEq(collate(mode, "123", "\"123\""), -1);
+ CAssertEq(collate(mode, "\"1234\"", "\"123\""), 1);
+ CAssertEq(collate(mode, "\"1234\"", "\"1235\""), -1);
+ CAssertEq(collate(mode, "\"1234\"", "\"1234\""), 0);
+ CAssertEq(collate(mode, "\"12\\/34\"", "\"12/34\""), 0);
+ CAssertEq(collate(mode, "\"\\/1234\"", "\"/1234\""), 0);
+ CAssertEq(collate(mode, "\"1234\\/\"", "\"1234/\""), 0);
+ CAssertEq(collate(mode, "\"A\"", "\"a\""), -1);
+ CAssertEq(collate(mode, "\"B\"", "\"a\""), -1);
}
TestCase(TDCollateRaw) {
void* mode = kTDCollateJSON_Raw;
- CAssertEq(TDCollateJSON(mode, 0, "false", 0, "17"), 1);
- CAssertEq(TDCollateJSON(mode, 0, "false", 0, "true"), -1);
- CAssertEq(TDCollateJSON(mode, 0, "null", 0, "true"), -1);
- CAssertEq(TDCollateJSON(mode, 0, "[\"A\"]", 0, "\"A\""), -1);
- CAssertEq(TDCollateJSON(mode, 0, "\"A\"", 0, "\"a\""), -1);
- CAssertEq(TDCollateJSON(mode, 0, "[\"b\"]", 0, "[\"b\",\"c\",\"a\"]"), -1);
+ CAssertEq(collate(mode, "false", "17"), 1);
+ CAssertEq(collate(mode, "false", "true"), -1);
+ CAssertEq(collate(mode, "null", "true"), -1);
+ CAssertEq(collate(mode, "[\"A\"]", "\"A\""), -1);
+ CAssertEq(collate(mode, "\"A\"", "\"a\""), -1);
+ CAssertEq(collate(mode, "[\"b\"]", "[\"b\",\"c\",\"a\"]"), -1);
}
TestCase(TDCollateArrays) {
void* mode = kTDCollateJSON_Unicode;
- CAssertEq(TDCollateJSON(mode, 0, "[]", 0, "\"foo\""), 1);
- CAssertEq(TDCollateJSON(mode, 0, "[]", 0, "[]"), 0);
- CAssertEq(TDCollateJSON(mode, 0, "[true]", 0, "[true]"), 0);
- CAssertEq(TDCollateJSON(mode, 0, "[false]", 0, "[null]"), 1);
- CAssertEq(TDCollateJSON(mode, 0, "[]", 0, "[null]"), -1);
- CAssertEq(TDCollateJSON(mode, 0, "[123]", 0, "[45]"), 1);
- CAssertEq(TDCollateJSON(mode, 0, "[123]", 0, "[45,67]"), 1);
- CAssertEq(TDCollateJSON(mode, 0, "[123.4,\"wow\"]", 0, "[123.40,789]"), 1);
+ CAssertEq(collate(mode, "[]", "\"foo\""), 1);
+ CAssertEq(collate(mode, "[]", "[]"), 0);
+ CAssertEq(collate(mode, "[true]", "[true]"), 0);
+ CAssertEq(collate(mode, "[false]", "[null]"), 1);
+ CAssertEq(collate(mode, "[]", "[null]"), -1);
+ CAssertEq(collate(mode, "[123]", "[45]"), 1);
+ CAssertEq(collate(mode, "[123]", "[45,67]"), 1);
+ CAssertEq(collate(mode, "[123.4,\"wow\"]", "[123.40,789]"), 1);
+ CAssertEq(collate(mode, "[5,\"wow\"]", "[5,\"wow\"]"), 0);
+ CAssertEq(collate(mode, "[5,\"wow\"]", "1"), 1);
+ CAssertEq(collate(mode, "1", "[5,\"wow\"]"), -1);
}
TestCase(TDCollateNestedArrays) {
void* mode = kTDCollateJSON_Unicode;
- CAssertEq(TDCollateJSON(mode, 0, "[[]]", 0, "[]"), 1);
- CAssertEq(TDCollateJSON(mode, 0, "[1,[2,3],4]", 0, "[1,[2,3.1],4,5,6]"), -1);
+ CAssertEq(collate(mode, "[[]]", "[]"), 1);
+ CAssertEq(collate(mode, "[1,[2,3],4]", "[1,[2,3.1],4,5,6]"), -1);
}
TestCase(TDCollateUnicodeStrings) {
@@ -359,9 +402,9 @@ static void testEscape(const char* source, char decoded) {
// That includes "\unnnn" for non-ASCII chars, and "\t", "\b", etc.
RequireTestCase(TDCollateConvertEscape);
void* mode = kTDCollateJSON_Unicode;
- CAssertEq(TDCollateJSON(mode, 0, encode(@"fréd"), 0, encode(@"fréd")), 0);
- CAssertEq(TDCollateJSON(mode, 0, encode(@"ømø"), 0, encode(@"omo")), 1);
- CAssertEq(TDCollateJSON(mode, 0, encode(@"\t"), 0, encode(@" ")), -1);
- CAssertEq(TDCollateJSON(mode, 0, encode(@"\001"), 0, encode(@" ")), -1);
+ CAssertEq(collate(mode, encode(@"fréd"), encode(@"fréd")), 0);
+ CAssertEq(collate(mode, encode(@"ømø"), encode(@"omo")), 1);
+ CAssertEq(collate(mode, encode(@"\t"), encode(@" ")), -1);
+ CAssertEq(collate(mode, encode(@"\001"), encode(@" ")), -1);
}
#endif
View
7 Source/TDDatabase+Attachments.h
@@ -52,6 +52,13 @@ typedef enum {
encoding: (TDAttachmentEncoding*)outEncoding
status: (TDStatus*)outStatus;
+/** Returns the location of an attachment's file in the blob store. */
+- (NSString*) getAttachmentPathForSequence: (SequenceNumber)sequence
+ named: (NSString*)filename
+ type: (NSString**)outType
+ encoding: (TDAttachmentEncoding*)outEncoding
+ status: (TDStatus*)outStatus;
+
/** Uses the "digest" field of the attachment dict to look up the attachment in the store and return a file URL to it. DO NOT MODIFY THIS FILE! */
- (NSURL*) fileForAttachmentDict: (NSDictionary*)attachmentDict;
View
58 Source/TDDatabase+Attachments.m
@@ -175,16 +175,16 @@ - (NSData*) decodeAttachment: (NSData*)attachment encoding: (TDAttachmentEncodin
}
-/** Returns the content and MIME type of an attachment */
-- (NSData*) getAttachmentForSequence: (SequenceNumber)sequence
- named: (NSString*)filename
- type: (NSString**)outType
- encoding: (TDAttachmentEncoding*)outEncoding
- status: (TDStatus*)outStatus
+/** Returns the location of an attachment's file in the blob store. */
+- (NSString*) getAttachmentPathForSequence: (SequenceNumber)sequence
+ named: (NSString*)filename
+ type: (NSString**)outType
+ encoding: (TDAttachmentEncoding*)outEncoding
+ status: (TDStatus*)outStatus
{
Assert(sequence > 0);
Assert(filename);
- NSData* contents = nil;
+ NSString* filePath = nil;
FMResultSet* r = [_fmdb executeQuery:
@"SELECT key, type, encoding FROM attachments WHERE sequence=? AND filename=?",
$object(sequence), filename];
@@ -204,24 +204,46 @@ - (NSData*) getAttachmentForSequence: (SequenceNumber)sequence
*outStatus = kTDStatusCorruptError;
return nil;
}
- contents = [_attachments blobForKey: *(TDBlobKey*)keyData.bytes];
- if (!contents) {
- Warn(@"%@: Failed to load attachment %lld.'%@'", self, sequence, filename);
- *outStatus = kTDStatusCorruptError;
- return nil;
- }
+ filePath = [_attachments pathForKey: *(TDBlobKey*)keyData.bytes];
*outStatus = kTDStatusOK;
if (outType)
*outType = [r stringForColumnIndex: 1];
- TDAttachmentEncoding encoding = [r intForColumnIndex: 2];
- if (outEncoding)
- *outEncoding = encoding;
- else
- contents = [self decodeAttachment: contents encoding: encoding];
+ *outEncoding = [r intForColumnIndex: 2];
} @finally {
[r close];
}
+ return filePath;
+}
+
+
+/** Returns the content and MIME type of an attachment */
+- (NSData*) getAttachmentForSequence: (SequenceNumber)sequence
+ named: (NSString*)filename
+ type: (NSString**)outType
+ encoding: (TDAttachmentEncoding*)outEncoding
+ status: (TDStatus*)outStatus
+{
+ TDAttachmentEncoding encoding;
+ NSString* filePath = [self getAttachmentPathForSequence: sequence
+ named: filename
+ type: outType
+ encoding: &encoding
+ status: outStatus];
+ if (!filePath)
+ return nil;
+ NSError* error;
+ NSData* contents = [NSData dataWithContentsOfFile: filePath options: NSDataReadingMappedIfSafe
+ error: &error];
+ if (!contents) {
+ Warn(@"%@: Failed to load attachment %lld.'%@' -- %@", self, sequence, filename, error);
+ *outStatus = kTDStatusCorruptError;
+ return nil;
+ }
+ if (outEncoding)
+ *outEncoding = encoding;
+ else
+ contents = [self decodeAttachment: contents encoding: encoding];
return contents;
}
View
9 Source/TDDatabase.h
@@ -25,7 +25,7 @@ extern NSString* const TDDatabaseWillBeDeletedNotification;
/** Filter block, used in changes feeds and replication. */
-typedef BOOL (^TDFilterBlock) (TDRevision* revision);
+typedef BOOL (^TDFilterBlock) (TDRevision* revision, NSDictionary* params);
/** Options for what metadata to include in document bodies */
@@ -38,7 +38,7 @@ enum {
kTDIncludeLocalSeq = 16, // adds '_local_seq' property
kTDLeaveAttachmentsEncoded = 32, // i.e. don't decode
kTDBigAttachmentsFollow = 64, // i.e. add 'follows' key instead of data for big ones
- kTDNoBody = 128 // omit regular doc body properties
+ kTDNoBody = 128, // omit regular doc body properties
};
@@ -134,8 +134,6 @@ extern const TDChangesOptions kDefaultTDChangesOptions;
- (TDRevisionList*) getAllRevisionsOfDocumentID: (NSString*)docID
onlyCurrent: (BOOL)onlyCurrent;
-- (NSArray*) getConflictingRevisionIDsOfDocID: (NSString*)docID;
-
/** Returns IDs of local revisions of the same document, that have a lower generation number.
Does not return revisions whose bodies have been compacted away, or deletion markers. */
- (NSArray*) getPossibleAncestorRevisionIDs: (TDRevision*)rev
@@ -159,7 +157,8 @@ extern const TDChangesOptions kDefaultTDChangesOptions;
- (TDRevisionList*) changesSinceSequence: (SequenceNumber)lastSequence
options: (const TDChangesOptions*)options
- filter: (TDFilterBlock)filter;
+ filter: (TDFilterBlock)filter
+ params: (NSDictionary*)filterParams;
/** Define or clear a named filter function. These aren't used directly by TDDatabase, but they're looked up by TDRouter when a _changes request has a ?filter parameter. */
- (void) defineFilter: (NSString*)filterName asBlock: (TDFilterBlock)filterBlock;
View
15 Source/TDDatabase.m
@@ -421,7 +421,7 @@ - (NSDictionary*) extraPropertiesForRevision: (TDRevision*)rev options: (TDConte
id localSeq=nil, revs=nil, revsInfo=nil, conflicts=nil;
if (options & kTDIncludeLocalSeq)
localSeq = $object(sequence);
-
+
if (options & kTDIncludeRevs) {
revs = [self getRevisionHistoryDict: rev];
}
@@ -615,16 +615,6 @@ - (TDRevisionList*) getAllRevisionsOfDocumentID: (NSString*)docID
}
-- (NSArray*) getConflictingRevisionIDsOfDocID: (NSString*)docID {
- SInt64 docNumericID = [self getDocNumericID: docID];
- if (docNumericID < 0)
- return nil;
- FMResultSet* r = [_fmdb executeQuery: @"SELECT revid FROM revs WHERE doc_id=? AND current "
- "ORDER BY revid DESC OFFSET 1", $object(docNumericID)];
- return revIDsFromResultSet(r);
-}
-
-
- (NSArray*) getPossibleAncestorRevisionIDs: (TDRevision*)rev limit: (unsigned)limit {
int generation = rev.generation;
if (generation <= 1)
@@ -740,6 +730,7 @@ - (NSDictionary*) getRevisionHistoryDict: (TDRevision*)rev {
- (TDRevisionList*) changesSinceSequence: (SequenceNumber)lastSequence
options: (const TDChangesOptions*)options
filter: (TDFilterBlock)filter
+ params: (NSDictionary*)filterParams
{
// http://wiki.apache.org/couchdb/HTTP_database_API#Changes
if (!options) options = &kDefaultTDChangesOptions;
@@ -774,7 +765,7 @@ - (TDRevisionList*) changesSinceSequence: (SequenceNumber)lastSequence
intoRevision: rev
options: options->contentOptions];
}
- if (!filter || filter(rev))
+ if (!filter || filter(rev, filterParams))
[changes addRev: rev];
[rev release];
}
View
29 Source/TDDatabase_Tests.m
@@ -124,18 +124,21 @@
CAssertEq(status, kTDStatusConflict);
// Check the changes feed, with and without filters:
- TDRevisionList* changes = [db changesSinceSequence: 0 options: NULL filter: NULL];
+ TDRevisionList* changes = [db changesSinceSequence: 0 options: NULL filter: NULL params: nil];
Log(@"Changes = %@", changes);
CAssertEq(changes.count, 1u);
+
+ TDFilterBlock filter = ^BOOL(TDRevision *revision, NSDictionary* params) {
+ NSString* status = [params objectForKey: @"status"];
+ return [[revision.properties objectForKey: @"status"] isEqual: status];
+ };
- changes = [db changesSinceSequence: 0 options: NULL filter:^BOOL(TDRevision *revision) {
- return [[revision.properties objectForKey: @"status"] isEqual: @"updated!"];
- }];
+ changes = [db changesSinceSequence: 0 options: NULL
+ filter: filter params: $dict({@"status", @"updated!"})];
CAssertEq(changes.count, 1u);
- changes = [db changesSinceSequence: 0 options: NULL filter:^BOOL(TDRevision *revision) {
- return [[revision.properties objectForKey: @"status"] isEqual: @"not updated!"];
- }];
+ changes = [db changesSinceSequence: 0 options: NULL
+ filter: filter params: $dict({@"status", @"not updated!"})];
CAssertEq(changes.count, 0u);
// Delete it:
@@ -157,7 +160,7 @@
CAssertNil(readRev);
// Check the changes feed again after the deletion:
- changes = [db changesSinceSequence: 0 options: NULL filter: NULL];
+ changes = [db changesSinceSequence: 0 options: NULL filter: NULL params: nil];
Log(@"Changes = %@", changes);
CAssertEq(changes.count, 1u);
@@ -337,13 +340,17 @@ static void verifyHistory(TDDatabase* db, TDRevision* rev, NSArray* history) {
// Make sure the revision with the higher revID wins the conflict:
TDRevision* current = [db getDocumentWithID: rev.docID revisionID: nil options: 0];
CAssertEqual(current, conflict);
-
+
+ // Check that the list of conflicts is accurate:
+ TDRevisionList* conflictingRevs = [db getAllRevisionsOfDocumentID: rev.docID onlyCurrent: YES];
+ CAssertEqual(conflictingRevs.allRevisions, $array(conflict, rev));
+
// Get the _changes feed and verify only the winner is in it:
TDChangesOptions options = kDefaultTDChangesOptions;
- TDRevisionList* changes = [db changesSinceSequence: 0 options: &options filter: NULL];
+ TDRevisionList* changes = [db changesSinceSequence: 0 options: &options filter: NULL params: nil];
CAssertEqual(changes.allRevisions, $array(conflict, other));
options.includeConflicts = YES;
- changes = [db changesSinceSequence: 0 options: &options filter: NULL];
+ changes = [db changesSinceSequence: 0 options: &options filter: NULL params: nil];
CAssertEqual(changes.allRevisions, $array(rev, conflict, other));
}
View
19 Source/TDMultipartDocumentReader.h
@@ -8,10 +8,13 @@
#import "TDMultipartReader.h"
#import <TouchDB/TDStatus.h>
-@class TDDatabase, TDRevision, TDBlobStoreWriter;
+@class TDDatabase, TDRevision, TDBlobStoreWriter, TDMultipartDocumentReader;
-@interface TDMultipartDocumentReader : NSObject <TDMultipartReaderDelegate>
+typedef void(^TDMultipartDocumentReaderCompletionBlock)(TDMultipartDocumentReader*);
+
+
+@interface TDMultipartDocumentReader : NSObject <TDMultipartReaderDelegate, NSStreamDelegate>
{
@private
TDDatabase* _database;
@@ -22,13 +25,21 @@
NSMutableDictionary* _attachmentsByName; // maps attachment name --> TDBlobStoreWriter
NSMutableDictionary* _attachmentsByDigest; // maps attachment MD5 --> TDBlobStoreWriter
NSMutableDictionary* _document;
+ TDMultipartDocumentReaderCompletionBlock _completionBlock;
}
+// synchronous:
+ (NSDictionary*) readData: (NSData*)data
ofType: (NSString*)contentType
toDatabase: (TDDatabase*)database
status: (TDStatus*)outStatus;
+// asynchronous:
++ (TDStatus) readStream: (NSInputStream*)stream
+ ofType: (NSString*)contentType
+ toDatabase: (TDDatabase*)database
+ then: (TDMultipartDocumentReaderCompletionBlock)completionBlock;
+
- (id) initWithDatabase: (TDDatabase*)database;
@property (readonly, nonatomic) TDStatus status;
@@ -39,6 +50,10 @@
- (BOOL) appendData: (NSData*)data;
+- (TDStatus) readStream: (NSInputStream*)stream
+ ofType: (NSString*)contentType
+ then: (TDMultipartDocumentReaderCompletionBlock)completionBlock;
+
- (BOOL) finish;
@end
View
82 Source/TDMultipartDocumentReader.m
@@ -20,6 +20,7 @@
#import "TDBase64.h"
#import "TDMisc.h"
#import "CollectionUtils.h"
+#import "MYStreamUtils.h"
@implementation TDMultipartDocumentReader
@@ -30,6 +31,10 @@ + (NSDictionary*) readData: (NSData*)data
toDatabase: (TDDatabase*)database
status: (TDStatus*)outStatus
{
+ if (data.length == 0) {
+ *outStatus = kTDStatusBadJSON;
+ return nil;
+ }
NSDictionary* result = nil;
TDMultipartDocumentReader* reader = [[self alloc] initWithDatabase: database];
if ([reader setContentType: contentType]
@@ -44,7 +49,6 @@ + (NSDictionary*) readData: (NSData*)data
}
-
- (id) initWithDatabase: (TDDatabase*)database
{
Assert(database);
@@ -64,6 +68,7 @@ - (void) dealloc {
[_document release];
[_attachmentsByName autorelease];
[_attachmentsByDigest autorelease];
+ [_completionBlock release];
[super dealloc];
}
@@ -135,6 +140,81 @@ - (BOOL) finish {
}
+#pragma mark - ASYNCHRONOUS MODE:
+
+
++ (TDStatus) readStream: (NSInputStream*)stream
+ ofType: (NSString*)contentType
+ toDatabase: (TDDatabase*)database
+ then: (TDMultipartDocumentReaderCompletionBlock)onCompletion
+{
+ TDMultipartDocumentReader* reader = [[[self alloc] initWithDatabase: database] autorelease];
+ return [reader readStream: stream ofType: contentType then: onCompletion];
+}
+
+
+- (TDStatus) readStream: (NSInputStream*)stream
+ ofType: (NSString*)contentType
+ then: (TDMultipartDocumentReaderCompletionBlock)completionBlock
+{
+ if ([self setContentType: contentType]) {
+ LogTo(SyncVerbose, @"%@: Reading from input stream...", self);
+ [self retain]; // balanced by release in -finishAsync:
+ _completionBlock = [completionBlock copy];
+ [stream open];
+ stream.delegate = self;
+ [stream scheduleInRunLoop: [NSRunLoop currentRunLoop] forMode: NSRunLoopCommonModes];
+ }
+ return _status;
+}
+
+
+- (void) stream: (NSInputStream*)stream handleEvent: (NSStreamEvent)eventCode {
+ BOOL finish = NO;
+ switch (eventCode) {
+ case NSStreamEventHasBytesAvailable:
+ finish = ![self readFromStream: stream];
+ break;
+ case NSStreamEventEndEncountered:
+ finish = YES;
+ break;
+ case NSStreamEventErrorOccurred:
+ Warn(@"%@: error reading from stream: %@", self, stream.streamError);
+ _status = kTDStatusUpstreamError;
+ finish = YES;
+ break;
+ default:
+ break;
+ }
+ if (finish)
+ [self finishAsync: stream];
+}
+
+
+- (BOOL) readFromStream: (NSInputStream*)stream {
+ BOOL readOK = [stream my_readData: ^(NSData *data) {
+ [self appendData: data];
+ }];
+ if (!readOK) {
+ Warn(@"%@: error reading from stream: %@", self, stream.streamError);
+ _status = kTDStatusUpstreamError;
+ }
+ return !TDStatusIsError(_status);
+}
+
+
+- (void) finishAsync: (NSInputStream*)stream {
+ stream.delegate = nil;
+ [stream close];
+ if (!TDStatusIsError(_status))
+ [self finish];
+ _completionBlock(self);
+ [_completionBlock release];
+ _completionBlock = nil;
+ [self release]; // balances -retain in -readStream:
+}
+
+
#pragma mark - MIME PARSER CALLBACKS:
View
3  Source/TDMultipartWriter.h
@@ -16,9 +16,8 @@
@private
NSString* _boundary;
NSString* _contentType;
- NSData* _separatorData;
+ NSData* _finalBoundary;
NSDictionary* _nextPartsHeaders;
- BOOL _addedFinalBoundary;
}
/** Initializes an instance.
View
45 Source/TDMultipartWriter.m
@@ -27,11 +27,11 @@ - (id) initWithContentType: (NSString*)type boundary: (NSString*)boundary {
if (self) {
_contentType = [type copy];
_boundary = [(boundary ?: TDCreateUUID()) copy];
- NSString* separatorStr = $sprintf(@"\r\n--%@\r\n\r\n", _boundary);
- _separatorData = [[separatorStr dataUsingEncoding: NSUTF8StringEncoding] retain];
- // Account for the final boundary to be written by -open. Add it in now, because the
+ // Account for the final boundary to be written by -opened. Add its length now, because the
// client is probably going to ask for my .length *before* it calls -open.
- _length += _separatorData.length - 2;
+ NSString* finalBoundaryStr = $sprintf(@"\r\n--%@--", _boundary);
+ _finalBoundary = [[finalBoundaryStr dataUsingEncoding: NSUTF8StringEncoding] retain];
+ _length += _finalBoundary.length;
}
return self;
}
@@ -39,7 +39,7 @@ - (id) initWithContentType: (NSString*)type boundary: (NSString*)boundary {
- (void)dealloc {
[_boundary release];
- [_separatorData release];
+ [_finalBoundary release];
[super dealloc];
}
@@ -58,29 +58,34 @@ - (void) setNextPartsHeaders: (NSDictionary*)headers {
- (void) addInput: (id)part length:(UInt64)length {
- NSData* separator = _separatorData;
- if (_nextPartsHeaders.count) {
- NSMutableString* headers = [NSMutableString stringWithFormat: @"\r\n--%@\r\n", _boundary];
- for (NSString* name in _nextPartsHeaders) {
- [headers appendFormat: @"%@: %@\r\n", name, [_nextPartsHeaders objectForKey: name]];
- }
- [headers appendString: @"\r\n"];
- separator = [headers dataUsingEncoding: NSUTF8StringEncoding];
- [self setNextPartsHeaders: nil];
+ NSMutableString* headers = [NSMutableString stringWithFormat: @"\r\n--%@\r\n", _boundary];
+ [headers appendFormat: @"Content-Length: %llu\r\n", length];
+ for (NSString* name in _nextPartsHeaders) {
+ // Strip any CR or LF in the header value. This isn't real quoting, just enough to ensure
+ // a spoofer can't add bogus headers by putting CRLF into a header value!
+ NSMutableString* value = [[_nextPartsHeaders objectForKey: name] mutableCopy];
+ [value replaceOccurrencesOfString: @"\r" withString: @""
+ options: 0 range: NSMakeRange(0, value.length)];
+ [value replaceOccurrencesOfString: @"\n" withString: @""
+ options: 0 range: NSMakeRange(0, value.length)];
+ [headers appendFormat: @"%@: %@\r\n", name, value];
+ [value release];
}
+ [headers appendString: @"\r\n"];
+ NSData* separator = [headers dataUsingEncoding: NSUTF8StringEncoding];
+ [self setNextPartsHeaders: nil];
+
[super addInput: separator length: separator.length];
[super addInput: part length: length];
}
- (void) opened {
- if (!_addedFinalBoundary) {
+ if (_finalBoundary) {
// Append the final boundary:
- NSString* trailerStr = $sprintf(@"\r\n--%@--", _boundary);
- NSData* trailerData = [trailerStr dataUsingEncoding: NSUTF8StringEncoding];
- [super addInput: trailerData length: 0];
+ [super addInput: _finalBoundary length: 0];
// _length was already adjusted for this in -init
- _addedFinalBoundary = YES;
+ setObj(&_finalBoundary, nil);
}
[super opened];
}
@@ -100,7 +105,7 @@ - (void) openForURLRequest: (NSMutableURLRequest*)request;
TestCase(TDMultipartWriter) {
- NSString* expectedOutput = @"\r\n--BOUNDARY\r\n\r\n<part the first>\r\n--BOUNDARY\r\nContent-Type: something\r\n\r\n<2nd part>\r\n--BOUNDARY--";
+ NSString* expectedOutput = @"\r\n--BOUNDARY\r\nContent-Length: 16\r\n\r\n<part the first>\r\n--BOUNDARY\r\nContent-Length: 10\r\nContent-Type: something\r\n\r\n<2nd part>\r\n--BOUNDARY--";
RequireTestCase(TDMultiStreamWriter);
for (unsigned bufSize = 1; bufSize < expectedOutput.length+1; ++bufSize) {
TDMultipartWriter* mp = [[[TDMultipartWriter alloc] initWithContentType: @"foo/bar"
View
24 Source/TDPusher.m
@@ -20,6 +20,7 @@
#import "TDMultipartUploader.h"
#import "TDInternal.h"
#import "TDMisc.h"
+#import "TDCanonicalJSON.h"
static int findCommonAncestor(TDRevision* rev, NSArray* possibleIDs);
@@ -82,7 +83,9 @@ - (void) beginReplicating {
options.includeConflicts = YES;
// Process existing changes since the last push:
TDRevisionList* changes = [_db changesSinceSequence: [_lastSequence longLongValue]
- options: &options filter: filter];
+ options: &options
+ filter: filter
+ params: _filterParameters];
if (changes.count > 0)
[self processInbox: changes];
@@ -126,7 +129,7 @@ - (void) dbChanged: (NSNotification*)n {
return;
TDRevision* rev = [userInfo objectForKey: @"rev"];
TDFilterBlock filter = self.filter;
- if (filter && !filter(rev))
+ if (filter && !filter(rev, _filterParameters))
return;
[self addToInbox: rev];
}
@@ -233,10 +236,12 @@ - (void) processInbox: (TDRevisionList*)changes {
- (BOOL) uploadMultipartRevision: (TDRevision*)rev {
- // Find all the attachments with "follows" instead of a body, and put 'em in a multipart stream:
+ // Find all the attachments with "follows" instead of a body, and put 'em in a multipart stream.
+ // It's important to scan the _attachments entries in the same order in which they will appear
+ // in the JSON, because CouchDB expects the MIME bodies to appear in that same order (see #133).
TDMultipartWriter* bodyStream = nil;
NSDictionary* attachments = [rev.properties objectForKey: @"_attachments"];
- for (NSString* attachmentName in attachments) {
+ for (NSString* attachmentName in [TDCanonicalJSON orderedKeys: attachments]) {
NSDictionary* attachment = [attachments objectForKey: attachmentName];
if ([attachment objectForKey: @"follows"]) {
if (!bodyStream) {
@@ -244,10 +249,17 @@ - (BOOL) uploadMultipartRevision: (TDRevision*)rev {
bodyStream = [[[TDMultipartWriter alloc] initWithContentType: @"multipart/related"
boundary: nil] autorelease];
[bodyStream setNextPartsHeaders: $dict({@"Content-Type", @"application/json"})];
- [bodyStream addData: rev.asJSON];
+ // Use canonical JSON encoder so that _attachments keys will be written in the
+ // same order that this for loop is processing the attachments.
+ NSData* json = [TDCanonicalJSON canonicalData: rev.properties];
+ [bodyStream addData: json];
}
NSString* disposition = $sprintf(@"attachment; filename=%@", TDQuoteString(attachmentName));
- [bodyStream setNextPartsHeaders: $dict({@"Content-Disposition", disposition})];
+ NSString* contentType = [attachment objectForKey: @"type"];
+ NSString* contentEncoding = [attachment objectForKey: @"encoding"];
+ [bodyStream setNextPartsHeaders: $dict({@"Content-Disposition", disposition},
+ {@"Content-Type", contentType},
+ {@"Content-Encoding", contentEncoding})];
[bodyStream addFileURL: [_db fileForAttachmentDict: attachment]];
}
}
View
3  Source/TDRemoteRequest.h
@@ -58,6 +58,9 @@ typedef void (^TDRemoteRequestCompletionBlock)(id result, NSError* error);
// The value to use for the User-Agent HTTP header.
+ (NSString*) userAgentHeader;
+// Shared subroutines to handle NSURLAuthenticationMethodServerTrust challenges
++ (BOOL) checkTrust: (SecTrustRef)trust forHost: (NSString*)host;
+
@end
View
58 Source/TDRemoteRequest.m
@@ -70,9 +70,10 @@ - (id) initWithMethod: (NSString*)method
}
- (void) setAuthorizer: (id<TDAuthorizer>)authorizer {
- setObj(&_authorizer, authorizer);
- [_request setValue: [authorizer authorizeURLRequest: _request forRealm: nil]
- forHTTPHeaderField: @"Authorization"];
+ if (ifSetObj(&_authorizer, authorizer)) {
+ [_request setValue: [authorizer authorizeURLRequest: _request forRealm: nil]
+ forHTTPHeaderField: @"Authorization"];
+ }
}
@@ -95,7 +96,7 @@ - (void) start {
// Retaining myself shouldn't be necessary, because NSURLConnection is documented as retaining
// its delegate while it's running. But GNUstep doesn't (currently) do this, so for
// compatibility I retain myself until the connection completes (see -clearConnection.)
- // TEMP: Remove this and the [self autorelease] below when I get the fix from GNUstep.
+ // TODO: Remove this and the [self autorelease] below when I get the fix from GNUstep.
[self retain];
}
@@ -184,22 +185,61 @@ - (bool) retryWithCredential {
- (void)connection:(NSURLConnection *)connection
willSendRequestForAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge
{
- NSString* authMethod = [[challenge protectionSpace] authenticationMethod];
+ id<NSURLAuthenticationChallengeSender> sender = challenge.sender;
+ NSURLProtectionSpace* space = challenge.protectionSpace;
+ NSString* authMethod = space.authenticationMethod;
LogTo(RemoteRequest, @"Got challenge: %@ (%@)", challenge, authMethod);
if ($equal(authMethod, NSURLAuthenticationMethodHTTPBasic)) {
_challenged = true;
if (challenge.previousFailureCount == 0) {
- NSURLCredential* cred = [_request.URL my_credentialForRealm: challenge.protectionSpace.realm
+ NSURLCredential* cred = [_request.URL my_credentialForRealm: space.realm
authenticationMethod: authMethod];
if (cred) {
- [challenge.sender useCredential: cred forAuthenticationChallenge:challenge];
+ [sender useCredential: cred forAuthenticationChallenge:challenge];
return;
}
}
} else if ($equal(authMethod, NSURLAuthenticationMethodServerTrust)) {
- // TODO: Check trust of server cert
+ SecTrustRef trust = space.serverTrust;
+ if ([[self class] checkTrust: trust forHost: space.host]) {
+ [sender useCredential: [NSURLCredential credentialForTrust: trust]
+ forAuthenticationChallenge: challenge];
+ } else {
+ [sender cancelAuthenticationChallenge: challenge];
+ }
}
- [challenge.sender performDefaultHandlingForAuthenticationChallenge: challenge];
+ [sender performDefaultHandlingForAuthenticationChallenge: challenge];
+}
+
+
++ (BOOL) checkTrust: (SecTrustRef)trust forHost: (NSString*)host {
+ SecTrustResultType trustResult;
+ OSStatus err = SecTrustEvaluate(trust, &trustResult);
+ if (err == noErr && (trustResult == kSecTrustResultProceed ||
+ trustResult == kSecTrustResultUnspecified)) {
+ return YES;
+ } else {
+ Warn(@"TouchDB: SSL server <%@> not trusted (err=%d, trustResult=%u); cert chain follows:",
+ host, (int)err, (unsigned)trustResult);
+#if TARGET_OS_IPHONE
+ for (CFIndex i = 0; i < SecTrustGetCertificateCount(trust); ++i) {
+ SecCertificateRef cert = SecTrustGetCertificateAtIndex(trust, i);
+ CFStringRef subject = SecCertificateCopySubjectSummary(cert);
+ Warn(@" %@", subject);
+ CFRelease(subject);
+ }
+#else
+ NSArray* trustProperties = NSMakeCollectable(SecTrustCopyProperties(trust));
+ for (NSDictionary* property in trustProperties) {
+ Warn(@" %@: error = %@",
+ [property objectForKey: kSecPropertyTypeTitle],
+ [property objectForKey: kSecPropertyTypeError]);
+ }
+ [trustProperties release];
+#endif
+ return NO;
+ }
+
}
View
28 Source/TDReplicatorManager.m
@@ -215,9 +215,37 @@ - (BOOL) validateRevision: (TDRevision*)newRev context: (id<TDValidationContext>
NSSet* deletableProperties = [NSSet setWithObjects: @"_replication_state", nil];
NSSet* mutableProperties = [NSSet setWithObjects: @"filter", @"query_params",
@"heartbeat", @"feed", nil];
+ NSSet* partialMutableProperties = [NSSet setWithObjects:@"target", @"source", nil];
return [context enumerateChanges: ^BOOL(NSString *key, id oldValue, id newValue) {
if (![context currentRevision])
return ![key hasPrefix: @"_"];
+
+ // allow change of 'headers' and 'auth' in target and source
+ if ([partialMutableProperties containsObject:key]) {
+ NSDictionary *old = $castIf(NSDictionary, oldValue);
+ NSDictionary *nuu = $castIf(NSDictionary, newValue);
+ if ([oldValue isKindOfClass:[NSString class]]) {
+ old = [NSDictionary dictionaryWithObject:oldValue forKey:@"url"];
+ }
+ if ([newValue isKindOfClass:[NSString class]]) {
+ nuu = [NSDictionary dictionaryWithObject:newValue forKey:@"url"];
+ }
+ NSMutableSet* changedKeys = [NSMutableSet set];
+ for (NSString *subKey in old.allKeys) {
+ if (!$equal([old objectForKey: subKey], [nuu objectForKey: subKey])) {
+ [changedKeys addObject:subKey];
+ }
+ }
+ for (NSString *subKey in nuu.allKeys) {
+ if (![old objectForKey:subKey]) {
+ [changedKeys addObject:subKey];
+ }
+ }
+ NSSet* mutableSubProperties = [NSSet setWithObjects:@"headers", @"auth", nil];
+ [changedKeys minusSet:mutableSubProperties];
+ return [changedKeys count] == 0;
+ }
+
NSSet* allowed = newValue ? mutableProperties : deletableProperties;
return [allowed containsObject: key];
}];
View
5 Source/TDReplicator_Tests.m
@@ -93,8 +93,9 @@ static void deleteRemoteDB(void) {
[db open];
__block int filterCalls = 0;
- [db defineFilter: @"filter" asBlock: ^BOOL(TDRevision *revision) {
- Log(@"Test filter called on %@, properties = %@", revision, revision.properties);
+ [db defineFilter: @"filter" asBlock: ^BOOL(TDRevision *revision, NSDictionary* params) {
+ Log(@"Test filter called with params = %@", params);
+ Log(@"Rev = %@, properties = %@", revision, revision.properties);
CAssert(revision.properties);
++filterCalls;
return YES;
View
140 Source/TDRouter+Handlers.m
@@ -443,17 +443,15 @@ - (void) sendContinuousChange: (TDRevision*)rev {
- (void) dbChanged: (NSNotification*)n {
TDRevision* rev = [n.userInfo objectForKey: @"rev"];
- if (_changesFilter && !_changesFilter(rev))
+ if (_changesFilter && !_changesFilter(rev, _changesFilterParams))
return;
if (_longpoll) {
Log(@"TDRouter: Sending longpoll response");
- [self sendResponse];
+ [self sendResponseHeaders];
NSDictionary* body = [self responseBodyForChanges: $array(rev) since: 0];
_response.body = [TDBody bodyWithProperties: body];
- if (_onDataAvailable)
- _onDataAvailable(_response.body.asJSON, YES);
- [self finished];
+ [self sendResponseBodyAndFinish: YES];
} else {
Log(@"TDRouter: Sending continous change chunk");
[self sendContinuousChange: rev];
@@ -487,11 +485,13 @@ - (TDStatus) do_GET_changes: (TDDatabase*)db {
_changesFilter = [[_db filterNamed: filterName] retain];
if (!_changesFilter)
return kTDStatusNotFound;
+ _changesFilterParams = [self.jsonQueries copy];
}
TDRevisionList* changes = [db changesSinceSequence: since
options: &options
- filter: _changesFilter];
+ filter: _changesFilter
+ params: _changesFilterParams];
if (!changes)
return kTDStatusDBError;
@@ -499,7 +499,7 @@ - (TDStatus) do_GET_changes: (TDDatabase*)db {
if (continuous || (_longpoll && changes.count==0)) {
// Response is going to stay open (continuous, or hanging GET):
if (continuous) {
- [self sendResponse];
+ [self sendResponseHeaders];
for (TDRevision* rev in changes)
[self sendContinuousChange: rev];
}
@@ -508,7 +508,6 @@ - (TDStatus) do_GET_changes: (TDDatabase*)db {
name: TDDatabaseChangeNotification
object: db];
// Don't close connection; more data to come
- _waiting = YES;
return 0;
} else {
// Return a response immediately and close the connection:
@@ -632,19 +631,40 @@ - (TDStatus) do_GET: (TDDatabase*)db docID: (NSString*)docID attachment: (NSStri
TDAttachmentEncoding encoding = kTDAttachmentEncodingNone;
NSString* acceptEncoding = [_request valueForHTTPHeaderField: @"Accept-Encoding"];
BOOL acceptEncoded = (acceptEncoding && [acceptEncoding rangeOfString: @"gzip"].length > 0);
-
- NSData* contents = [_db getAttachmentForSequence: rev.sequence
- named: attachment
- type: &type
- encoding: (acceptEncoded ? &encoding : NULL)
- status: &status];
- if (!contents)
- return status;
+
+ if ($equal(_request.HTTPMethod, @"HEAD")) {
+ NSString* filePath = [_db getAttachmentPathForSequence: rev.sequence
+ named: attachment
+ type: &type
+ encoding: &encoding
+ status: &status];
+ if (!filePath)
+ return status;
+ if (_local) {
+ // Let in-app clients know the location of the attachment file:
+ [_response setValue: [[NSURL fileURLWithPath: filePath] absoluteString]
+ ofHeader: @"Location"];
+ }
+ UInt64 size = [[[NSFileManager defaultManager] attributesOfItemAtPath: filePath
+ error: nil]
+ fileSize];
+ if (size)
+ [_response setValue: $sprintf(@"%llu", size) ofHeader: @"Content-Length"];
+
+ } else {
+ NSData* contents = [_db getAttachmentForSequence: rev.sequence
+ named: attachment
+ type: &type
+ encoding: (acceptEncoded ? &encoding : NULL)
+ status: &status];
+ if (!contents)
+ return status;
+ _response.body = [TDBody bodyWithJSON: contents]; //FIX: This is a lie, it's not JSON
+ }
if (type)
[_response setValue: type ofHeader: @"Content-Type"];
if (encoding == kTDAttachmentEncodingGZIP)
[_response setValue: @"gzip" ofHeader: @"Content-Encoding"];
- _response.body = [TDBody bodyWithJSON: contents]; //FIX: This is a lie, it's not JSON
return kTDStatusOK;
}
@@ -723,13 +743,45 @@ - (TDStatus) update: (TDDatabase*)db
}
-- (TDBody*) documentBodyFromRequest: (TDStatus*)outStatus {
+- (TDStatus) readDocumentBodyThen: (TDStatus(^)(TDBody*))block {
+ TDStatus status;
NSString* contentType = [_request valueForHTTPHeaderField: @"Content-Type"];
- NSDictionary* properties = [TDMultipartDocumentReader readData: _request.HTTPBody
- ofType: contentType
- toDatabase: _db
- status: outStatus];
- return properties ? [TDBody bodyWithProperties: properties] : nil;
+ NSInputStream* bodyStream = _request.HTTPBodyStream;
+ if (bodyStream) {
+ block = [[block copy] autorelease];
+ status = [TDMultipartDocumentReader readStream: bodyStream
+ ofType: contentType
+ toDatabase: _db
+ then: ^(TDMultipartDocumentReader* reader) {
+ // Called when the reader is done reading/parsing the stream:
+ TDStatus status = reader.status;
+ if (!TDStatusIsError(status)) {
+ NSDictionary* properties = reader.document;
+ if (properties)
+ status = block([TDBody bodyWithProperties: properties]);
+ else
+ status = kTDStatusBadRequest;
+ }
+ _response.internalStatus = status;
+ [self finished];
+ }];
+
+ if (TDStatusIsError(status))
+ return status;
+ // Don't close connection; more data to come
+ return 0;
+
+ } else {
+ NSDictionary* properties = [TDMultipartDocumentReader readData: _request.HTTPBody
+ ofType: contentType
+ toDatabase: _db
+ status: &status];
+ if (TDStatusIsError(status))
+ return status;
+ else if (!properties)
+ return kTDStatusBadRequest;
+ return block([TDBody bodyWithProperties: properties]);
+ }
}
@@ -737,32 +789,28 @@ - (TDStatus) do_POST: (TDDatabase*)db {
TDStatus status = [self openDB];
if (TDStatusIsError(status))
return status;
- TDBody* body = [self documentBodyFromRequest: &status];
- if (!body)
- return status;
- return [self update: db docID: nil body: body deleting: NO];
+ return [self readDocumentBodyThen: ^(TDBody *body) {
+ return [self update: db docID: nil body: body deleting: NO];
+ }];
}
- (TDStatus) do_PUT: (TDDatabase*)db docID: (NSString*)docID {
- TDStatus status;
- TDBody* body = [self documentBodyFromRequest: &status];
- if (!body)
- return status;
-
- if (![self query: @"new_edits"] || [self boolQuery: @"new_edits"]) {
- // Regular PUT:
- return [self update: db docID: docID body: body deleting: NO];
- } else {
- // PUT with new_edits=false -- forcible insertion of existing revision:
- TDRevision* rev = [[[TDRevision alloc] initWithBody: body] autorelease];
- if (!rev)
- return kTDStatusBadJSON;
- if (!$equal(rev.docID, docID) || !rev.revID)
- return kTDStatusBadID;
- NSArray* history = [TDDatabase parseCouchDBRevisionHistory: body.properties];
- return [_db forceInsert: rev revisionHistory: history source: nil];
- }
+ return [self readDocumentBodyThen: ^TDStatus(TDBody *body) {
+ if (![self query: @"new_edits"] || [self boolQuery: @"new_edits"]) {
+ // Regular PUT:
+ return [self update: db docID: docID body: body deleting: NO];
+ } else {
+ // PUT with new_edits=false -- forcible insertion of existing revision:
+ TDRevision* rev = [[[TDRevision alloc] initWithBody: body] autorelease];
+ if (!rev)
+ return kTDStatusBadJSON;
+ if (!$equal(rev.docID, docID) || !rev.revID)
+ return kTDStatusBadID;
+ NSArray* history = [TDDatabase parseCouchDBRevisionHistory: body.properties];
+ return [_db forceInsert: rev revisionHistory: history source: nil];
+ }
+ }];
}
@@ -917,7 +965,7 @@ - (TDStatus) do_POST_temp_view: (TDDatabase*)db {
if ([self cacheWithEtag: $sprintf(@"%lld", _db.lastSequence)]) // conditional GET
return kTDStatusNotModified;
- TDView* view = [self compileView: @"@@TEMP@@" fromProperties: props];
+ TDView* view = [self compileView: @"@@TEMPVIEW@@" fromProperties: props];
if (!view)
return kTDStatusDBError;
@try {
View
11 Source/TDRouter.h
@@ -26,8 +26,10 @@ typedef void (^OnFinishedBlock)();
NSDictionary* _queries;
TDResponse* _response;
TDDatabase* _db;
+ BOOL _local;
BOOL _waiting;
BOOL _responseSent;
+ BOOL _processRanges;
OnAccessCheckBlock _onAccessCheck;
OnResponseReadyBlock _onResponseReady;
OnDataAvailableBlock _onDataAvailable;
@@ -35,10 +37,13 @@ typedef void (^OnFinishedBlock)();
BOOL _running;
BOOL _longpoll;
TDFilterBlock _changesFilter;
+ NSDictionary* _changesFilterParams;
BOOL _changesIncludeDocs;
}
-- (id) initWithServer: (TDServer*)server request: (NSURLRequest*)request;
+- (id) initWithServer: (TDServer*)server request: (NSURLRequest*)request isLocal: (BOOL)isLocal;
+
+@property BOOL processRanges;
@property (copy) OnAccessCheckBlock onAccessCheck;
@property (copy) OnResponseReadyBlock onResponseReady;
@@ -61,6 +66,7 @@ typedef void (^OnFinishedBlock)();
- (BOOL) boolQuery: (NSString*)param;
- (int) intQuery: (NSString*)param defaultValue: (int)defaultValue;
- (id) jsonQuery: (NSString*)param error: (NSError**)outError;
+- (NSMutableDictionary*) jsonQueries;
- (BOOL) cacheWithEtag: (NSString*)etag;
- (TDContentOptions) contentOptions;
- (BOOL) getQueryOptions: (struct TDQueryOptions*)options;
@@ -68,7 +74,8 @@ typedef void (^OnFinishedBlock)();
@property (readonly) NSDictionary* bodyAsDictionary;
@property (readonly) NSString* ifMatch;
- (TDStatus) openDB;
-- (void) sendResponse;
+- (void) sendResponseHeaders;
+- (void) sendResponseBodyAndFinish: (BOOL)finished;
- (void) finished;
@end
View
198 Source/TDRouter.m
@@ -22,6 +22,7 @@
#import "TDReplicatorManager.h"
#import "TDInternal.h"
#import "ExceptionUtils.h"
+#import "MYRegexUtils.h"
#ifdef GNUSTEP
#import <GNUstepBase/NSURL+GNUstepBase.h>
@@ -52,6 +53,8 @@ - (id) initWithDatabaseManager: (TDDatabaseManager*)dbManager request: (NSURLReq
_dbManager = [dbManager retain];
_request = [request retain];
_response = [[TDResponse alloc] init];
+ _local = YES;
+ _processRanges = YES;
if (0) { // assignments just to appease static analyzer so it knows these ivars are used
_longpoll = NO;
_changesIncludeDocs = NO;
@@ -60,12 +63,17 @@ - (id) initWithDatabaseManager: (TDDatabaseManager*)dbManager request: (NSURLReq
return self;
}
-- (id) initWithServer: (TDServer*)server request: (NSURLRequest*)request {
+- (id) initWithServer: (TDServer*)server
+ request: (NSURLRequest*)request
+ isLocal: (BOOL)isLocal
+{
NSParameterAssert(server);
NSParameterAssert(request);
self = [self initWithDatabaseManager: nil request: request];
if (self) {
_server = [server retain];
+ _local = isLocal;
+ _processRanges = YES;
}
return self;
}
@@ -80,6 +88,7 @@ - (void)dealloc {
[_path release];
[_db release];
[_changesFilter release];
+ [_changesFilterParams release];
[_onAccessCheck release];
[_onResponseReady release];
[_onDataAvailable release];
@@ -90,7 +99,7 @@ - (void)dealloc {
@synthesize onAccessCheck=_onAccessCheck, onResponseReady=_onResponseReady,
onDataAvailable=_onDataAvailable, onFinished=_onFinished,
- request=_request, response=_response;
+ request=_request, response=_response, processRanges=_processRanges;
- (NSDictionary*) queries {
@@ -142,6 +151,18 @@ - (id) jsonQuery: (NSString*)param error: (NSError**)outError {
return result;
}
+- (NSMutableDictionary*) jsonQueries {
+ NSMutableDictionary* queries = $mdict();
+ [self.queries enumerateKeysAndObjectsUsingBlock: ^(NSString* param, NSString* value, BOOL *stop) {
+ id parsed = [TDJSON JSONObjectWithData: [value dataUsingEncoding: NSUTF8StringEncoding]
+ options: TDJSONReadingAllowFragments
+ error: nil];
+ if (parsed)
+ [queries setObject: parsed forKey: param];
+ }];
+ return queries;
+}
+
- (BOOL) cacheWithEtag: (NSString*)etag {
NSString* eTag = $sprintf(@"\"%@\"", etag);
@@ -257,15 +278,6 @@ - (TDStatus) openDB {
}
-- (void) sendResponse {
- if (!_responseSent) {
- _responseSent = YES;
- if (_onResponseReady)
- _onResponseReady(_response);
- }
-}
-
-
- (TDStatus) route {
// Refer to: http://wiki.apache.org/couchdb/Complete_HTTP_API_Reference
@@ -379,6 +391,19 @@ - (TDStatus) route {
- (void) run {
+ if (WillLogTo(TDRouter)) {
+ NSMutableString* output = [NSMutableString stringWithFormat: @"%@ %@",
+ _request.HTTPMethod, _request.URL];
+ if (_request.HTTPBodyStream)
+ [output appendString: @" + body stream"];
+ else if (_request.HTTPBody.length > 0)
+ [output appendFormat: @" + %llu-byte body", (uint64_t)_request.HTTPBody.length];
+ NSDictionary* headers = _request.allHTTPHeaderFields;
+ for (NSString* key in headers)
+ [output appendFormat: @"\n\t%@: %@", key, [headers objectForKey: key]];
+ LogTo(TDRouter, @"%@", output);
+ }
+
Assert(_dbManager);
// Call the appropriate handler method:
TDStatus status;
@@ -390,44 +415,149 @@ - (void) run {
[_response reset];
}
+ // If response is ready (nonzero status), tell my client about it:
+ if (status > 0) {
+ _response.internalStatus = status;
+ [self processRequestRanges];
+ [self sendResponseHeaders];
+ [self sendResponseBodyAndFinish: !_waiting];
+ } else {
+ _waiting = YES;
+ }
+
+ // If I will keep running asynchronously (i.e. a _changes feed handler), listen for the
+ // database closing so I can stop then:
+ if (_waiting)
+ [[NSNotificationCenter defaultCenter] addObserver: self selector: @selector(dbClosing:)
+ name: TDDatabaseWillCloseNotification
+ object: _db];
+}
+
+
+- (void) processRequestRanges {
+ if (!_processRanges || _response.status != 200 || !($equal(_request.HTTPMethod, @"GET") ||
+ $equal(_request.HTTPMethod, @"HEAD"))) {
+ return;
+ }
+
+ [_response setValue: @"bytes" ofHeader: @"Accept-Ranges"];
+
+ NSData* body = _response.body.asJSON; // misnomer; may not be JSON
+ NSUInteger bodyLength = body.length;
+ if (bodyLength == 0)
+ return;
+
+ // Range requests: http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.35
+ NSString* rangeHeader = [_request valueForHTTPHeaderField: @"Range"];
+ if (!rangeHeader)
+ return;
+
+ // Parse the header value into 'from' and 'to' range strings:
+ static NSRegularExpression* regex;
+ if (!regex)
+ regex = [$regex(@"^bytes=(\\d+)?-(\\d+)?$") retain];
+ NSTextCheckingResult *match = [regex firstMatchInString: rangeHeader options: 0
+ range: NSMakeRange(0, rangeHeader.length)];
+ if (!match) {
+ Warn(@"Invalid request Range header value: '%@'", rangeHeader);
+ return;
+ }
+ NSString *fromStr=nil, *toStr = nil;
+ NSRange r = [match rangeAtIndex: 1];
+ if (r.length)
+ fromStr = [rangeHeader substringWithRange: r];
+ r = [match rangeAtIndex: 2];
+ if (r.length)
+ toStr = [rangeHeader substringWithRange: r];
+
+ // Now convert those into the integer offsets (remember that 'to' is inclusive):
+ NSUInteger from, to;
+ if (fromStr.length > 0) {
+ from = (NSUInteger)fromStr.integerValue;
+ if (toStr.length > 0)
+ to = MIN((NSUInteger)toStr.integerValue, bodyLength - 1);
+ else
+ to = bodyLength - 1;
+ if (to < from)
+ return; // invalid range
+ } else if (toStr.length > 0) {
+ to = bodyLength - 1;
+ from = bodyLength - MIN((NSUInteger)toStr.integerValue, bodyLength);
+ } else {
+ return; // "-" is an invalid range
+ }
+
+ if (from >= bodyLength || to < from) {
+ _response.status = 416; // Requested Range Not Satisfiable
+ NSString* contentRangeStr = $sprintf(@"bytes */%llu", (uint64_t)bodyLength);
+ [_response setValue: contentRangeStr ofHeader: @"Content-Range"];
+ _response.body = nil;
+ return;
+ }
+
+ body = [body subdataWithRange: NSMakeRange(from, to - from + 1)];
+ _response.body = [TDBody bodyWithJSON: body]; // not actually JSON
+
+ // Content-Range: http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.16
+ NSString* contentRangeStr = $sprintf(@"bytes %llu-%llu/%llu",
+ (uint64_t)from, (uint64_t)to, (uint64_t)bodyLength);
+ [_response setValue: contentRangeStr ofHeader: @"Content-Range"];
+ _response.status = 206; // Partial Content
+ LogTo(TDRouter, @"Content-Range: %@", contentRangeStr);
+}
+
+
+- (void) sendResponseHeaders {
+ if (_responseSent)
+ return;
+ _responseSent = YES;
+
+ [_response.headers setObject: $sprintf(@"TouchDB %g", TouchDBVersionNumber)
+ forKey: @"Server"];
+
// Check for a mismatch between the Accept request header and the response type:
NSString* accept = [_request valueForHTTPHeaderField: @"Accept"];
- if (accept && !$equal(accept, @"*/*")) {
+ if (accept && [accept rangeOfString: @"*/*"].length == 0) {
NSString* responseType = _response.baseContentType;
if (responseType && [accept rangeOfString: responseType].length == 0) {
LogTo(TDRouter, @"Error kTDStatusNotAcceptable: Can't satisfy request Accept: %@", accept);
- status = kTDStatusNotAcceptable;
+ _response.internalStatus = kTDStatusNotAcceptable;
[_response reset];
}
}
- [_response.headers setObject: $sprintf(@"TouchDB %g", TouchDBVersionNumber)
- forKey: @"Server"];
-
if (_response.body.isValidJSON)
[_response setValue: @"application/json" ofHeader: @"Content-Type"];
- // If response is ready (nonzero status), tell my client about it:
- if (status > 0) {
- _response.internalStatus = status;
- [self sendResponse];
- if (_onDataAvailable && _response.body) {
- _onDataAvailable(_response.body.asJSON, !_waiting);
- }
- if (!_waiting)
- [self finished];
+ if (_response.status == 200 && ($equal(_request.HTTPMethod, @"GET") ||
+ $equal(_request.HTTPMethod, @"HEAD"))) {
+ if (![_response.headers objectForKey: @"Cache-Control"])
+ [_response setValue: @"must-revalidate" ofHeader: @"Cache-Control"];
}
-
- // If I will keep running asynchronously (i.e. a _changes feed handler), listen for the
- // database closing so I can stop then:
- if (_running)
- [[NSNotificationCenter defaultCenter] addObserver: self selector: @selector(dbClosing:)
- name: TDDatabaseWillCloseNotification
- object: _db];
+
+ if (_onResponseReady)
+ _onResponseReady(_response);
+}
+
+
+- (void) sendResponseBodyAndFinish: (BOOL)finished {
+ if (_onDataAvailable && _response.body && !$equal(_request.HTTPMethod, @"HEAD")) {
+ _onDataAvailable(_response.body.asJSON, finished);
+ }
+ if (finished)
+ [self finished];
}
- (void) finished {
+ if (WillLogTo(TDRouter)) {
+ NSMutableString* output = [NSMutableString stringWithFormat: @"Response -- status=%d, body=%llu bytes",
+ _response.status, (uint64_t)_response.body.asJSON.length];
+ NSDictionary* headers = _response.headers;
+ for (NSString* key in headers)
+ [output appendFormat: @"\n\t%@: %@", key, [headers objectForKey: key]];
+ LogTo(TDRouter, @"%@", output);
+ }
OnFinishedBlock onFinished = [_onFinished retain];
[self stopNow];
if (onFinished)
@@ -473,9 +603,9 @@ - (TDStatus) do_UNKNOWN {
- (void) dbClosing: (NSNotification*)n {
LogTo(TDRouter, @"Database closing! Returning error 500");
- if (_responseSent) {
+ if (!_responseSent) {
_response.internalStatus = 500;
- [self sendResponse];
+ [self sendResponseHeaders];
}
[self finished];
}
View
52 Source/TDRouter_Tests.m
@@ -400,14 +400,10 @@ static void CheckCacheable(TDDatabaseManager* server, NSString* path) {
}
-TestCase(TDRouter_GetAttachment) {
- TDDatabaseManager* server = createDBManager();
+static NSDictionary* createDocWithAttachments(TDDatabaseManager* server,
+ NSData* attach1, NSData* attach2) {
Send(server, @"PUT", @"/db", kTDStatusCreated, nil);
-
- // Create a document with an attachment:
- NSData* attach1 = [@"This is the body of attach1" dataUsingEncoding: NSUTF8StringEncoding];
NSString* base64 = [TDBase64 encode: attach1];
- NSData* attach2 = [@"This is the body of path/to/attachment" dataUsingEncoding: NSUTF8StringEncoding];
NSString* base642 = [TDBase64 encode: attach2];
NSDictionary* attachmentDict = $dict({@"attach", $dict({@"content_type", @"text/plain"},
{@"data", base64})},
@@ -417,9 +413,18 @@ static void CheckCacheable(TDDatabaseManager* server, NSString* path) {
NSDictionary* props = $dict({@"message", @"hello"},
{@"_attachments", attachmentDict});
- NSDictionary* result = SendBody(server, @"PUT", @"/db/doc1", props, kTDStatusCreated, nil);
+ return SendBody(server, @"PUT", @"/db/doc1", props, kTDStatusCreated, nil);
+}
+
+
+TestCase(TDRouter_GetAttachment) {
+ TDDatabaseManager* server = createDBManager();
+
+ NSData* attach1 = [@"This is the body of attach1" dataUsingEncoding: NSUTF8StringEncoding];
+ NSData* attach2 = [@"This is the body of path/to/attachment" dataUsingEncoding: NSUTF8StringEncoding];
+ NSDictionary* result = createDocWithAttachments(server, attach1, attach2);
NSString* revID = [result objectForKey: @"rev"];
-
+
// Now get the attachment via its URL:
TDResponse* response = SendRequest(server, @"GET", @"/db/doc1/attach", nil, nil);
CAssertEq(response.status, kTDStatusOK);
@@ -459,6 +464,7 @@ static void CheckCacheable(TDDatabaseManager* server, NSString* path) {
{@"revpos", $object(1)})}));
// Update the document but not the attachments:
+ NSDictionary *attachmentDict, *props;
attachmentDict = $dict({@"attach", $dict({@"content_type", @"text/plain"},
{@"stub", $true})},
{@"path/to/attachment",
@@ -488,6 +494,36 @@ static void CheckCacheable(TDDatabaseManager* server, NSString* path) {
}
+TestCase(TDRouter_GetRange) {
+ TDDatabaseManager* server = createDBManager();
+
+ NSData* attach1 = [@"This is the body of attach1" dataUsingEncoding: NSUTF8StringEncoding];
+ NSData* attach2 = [@"This is the body of path/to/attachment" dataUsingEncoding: NSUTF8StringEncoding];
+ createDocWithAttachments(server, attach1, attach2);
+
+ TDResponse* response = SendRequest(server, @"GET", @"/db/doc1/attach",
+ $dict({@"Range", @"bytes=5-15"}),
+ nil);
+ CAssertEq(response.status, 206);
+ CAssertEqual([response.headers objectForKey: @"Content-Range"], @"bytes 5-15/27");
+ CAssertEqual(response.body.asJSON, [@"is the body" dataUsingEncoding: NSUTF8StringEncoding]);
+
+ response = SendRequest(server, @"GET", @"/db/doc1/attach",
+ $dict({@"Range", @"bytes=12-"}),
+ nil);
+ CAssertEq(response.status, 206);
+ CAssertEqual([response.headers objectForKey: @"Content-Range"], @"bytes 12-26/27");
+ CAssertEqual(response.body.asJSON, [@"body of attach1" dataUsingEncoding: NSUTF8StringEncoding]);
+
+ response = SendRequest(server, @"GET", @"/db/doc1/attach",
+ $dict({@"Range", @"bytes=-7"}),
+ nil);
+ CAssertEq(response.status, 206);
+ CAssertEqual([response.headers objectForKey: @"Content-Range"], @"bytes 20-26/27");
+ CAssertEqual(response.body.asJSON, [@"attach1" dataUsingEncoding: NSUTF8StringEncoding]);
+}
+
+
TestCase(TDRouter_PutMultipart) {
RequireTestCase(TDRouter_Docs);
RequireTestCase(TDMultipartDownloader);
View
7 Source/TDServer.h
@@ -32,3 +32,10 @@
- (void) close;
@end
+
+
+/** Starts a TDServer and registers it with TDURLProtocol so you can call it using the CouchDB-compatible REST API.
+ @param serverDirectory The top-level directory where you want the server to store databases. Will be created if it does not already exist.
+ @param outError An error will be stored here if the function returns nil.
+ @return The root URL of the REST API, or nil if the server failed to start. */
+NSURL* TDStartServer(NSString* serverDirectory, NSError** outError);
View
11 Source/TDServer.m
@@ -143,6 +143,17 @@ - (void) tellDatabaseManager: (void (^)(TDDatabaseManager*))block {
+NSURL* TDStartServer(NSString* serverDirectory, NSError** outError) {
+ CAssert(![TDURLProtocol server], @"A TDServer is already running");
+ TDServer* tdServer = [[[TDServer alloc] initWithDirectory: serverDirectory
+ error: outError] autorelease];
+ if (!tdServer)
+ return nil;
+ return [TDURLProtocol registerServer: tdServer forHostname: nil];
+}
+
+
+
TestCase(TDServer) {
RequireTestCase(TDDatabaseManager);
View
4 Source/TDURLProtocol.h
@@ -18,6 +18,10 @@
/** The root URL served by this protocol, "touchdb:///". */
+ (NSURL*) rootURL;
+/** An alternate root URL with HTTP scheme; use this for CouchApps in UIWebViews.
+ (This URL will have the hostname of the touchdb: URL with ".touchdb." appended.) */
++ (NSURL*) HTTPURLForServerURL: (NSURL*)serverURL;
+
/** Registers a TDServer instance with a URL hostname.
'touchdb:' URLs with that hostname will be routed to that server.
If the server is nil, that hostname is unregistered, and URLs with that hostname will cause a host-not-found error.
View
38 Source/TDURLProtocol.m
@@ -35,6 +35,9 @@ + (void) initialize {
}
+#pragma mark - REGISTERING SERVERS:
+
+
+ (void) setServer: (TDServer*)server {
@synchronized(self) {
[self registerServer: server forHostname: nil];
@@ -108,13 +111,39 @@ + (TDServer*) serverForHostname: (NSString*)hostname {
}
++ (TDServer*) serverForURL: (NSURL*)url {
+ NSString* scheme = url.scheme.lowercaseString;
+ if ([scheme isEqualToString: kScheme])
+ return [self serverForHostname: url.host];
+ if ([scheme isEqualToString: @"http"] || [scheme isEqualToString: @"https"]) {
+ NSString* host = url.host;
+ if ([host hasSuffix: @".touchdb."]) {
+ host = [host substringToIndex: host.length - 9];
+ return [self serverForHostname: host];