mirror of
https://github.com/swisspol/GCDWebServer.git
synced 2026-01-31 00:00:04 +08:00
Compare commits
358 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c6d118f4ec | ||
|
|
0da9ee6afe | ||
|
|
6c93927e12 | ||
|
|
f4cf591e50 | ||
|
|
5c737549fc | ||
|
|
1c36bf07c8 | ||
|
|
df8d66f6c8 | ||
|
|
45432e6563 | ||
|
|
7720b1363d | ||
|
|
a7a6cfdbc7 | ||
|
|
70f687e34b | ||
|
|
11967061a1 | ||
|
|
2d8dc8775d | ||
|
|
9cb7caacbd | ||
|
|
ac10c0c5b0 | ||
|
|
acdb5c9262 | ||
|
|
f54cc20bd6 | ||
|
|
02738433bf | ||
|
|
c9563db0a6 | ||
|
|
cd1eea5612 | ||
|
|
f7c1c4eff5 | ||
|
|
fdc0feddf0 | ||
|
|
25dbee032d | ||
|
|
c3c7aaad00 | ||
|
|
2c53064f5d | ||
|
|
8d0a3599ee | ||
|
|
653dfb727b | ||
|
|
7e4dd53c98 | ||
|
|
4739d208c0 | ||
|
|
f57c307e7d | ||
|
|
466b1f8444 | ||
|
|
fd565421dc | ||
|
|
8811d2233e | ||
|
|
e561389d33 | ||
|
|
32cf20a1d8 | ||
|
|
3c6c2a2b5d | ||
|
|
2bf2dc72c5 | ||
|
|
0001648879 | ||
|
|
cbbf5483e8 | ||
|
|
edd1f2850b | ||
|
|
5eb5e14b70 | ||
|
|
3e46e12648 | ||
|
|
ee4395e67d | ||
|
|
34884f273a | ||
|
|
87745c0fde | ||
|
|
ec800b43d5 | ||
|
|
79d9fb389c | ||
|
|
33d14f22e0 | ||
|
|
b060305d6d | ||
|
|
561f56e7fb | ||
|
|
e9fdd19830 | ||
|
|
03fae468d1 | ||
|
|
0a7d185417 | ||
|
|
1e29a0195b | ||
|
|
4e29da53a2 | ||
|
|
acc54ceac3 | ||
|
|
c46a2ddb22 | ||
|
|
71e972084c | ||
|
|
e8c872b286 | ||
|
|
fc928d0e2b | ||
|
|
e65f0eeaf1 | ||
|
|
79ae63a4c0 | ||
|
|
21cc6bfb35 | ||
|
|
faf28fe0f9 | ||
|
|
bac5b680df | ||
|
|
7df465336e | ||
|
|
11254331d1 | ||
|
|
9f345c6858 | ||
|
|
a554893844 | ||
|
|
06569fe2cc | ||
|
|
e812d3f43c | ||
|
|
d30c88729d | ||
|
|
1a7dc913f1 | ||
|
|
47ed2cc593 | ||
|
|
033b0d3e56 | ||
|
|
6ed51907da | ||
|
|
8ed23992d8 | ||
|
|
b4927bdd75 | ||
|
|
44ffec1cf6 | ||
|
|
257ed94b65 | ||
|
|
4ea0a12317 | ||
|
|
ac788ca906 | ||
|
|
9f9509a05e | ||
|
|
48777cc151 | ||
|
|
c35d514b08 | ||
|
|
a013f9cebb | ||
|
|
1e28aef262 | ||
|
|
09851dd71b | ||
|
|
23f77b5765 | ||
|
|
7de8b0c643 | ||
|
|
2685819b61 | ||
|
|
fbcf3fa96c | ||
|
|
0f8e4f57e0 | ||
|
|
750214b5d5 | ||
|
|
ce5310f2ce | ||
|
|
83f1c062b5 | ||
|
|
0c51a9b071 | ||
|
|
3b5bc40e5e | ||
|
|
3178baa86e | ||
|
|
9a97e44d97 | ||
|
|
6c2b11e7b6 | ||
|
|
56b8e1befe | ||
|
|
4231465c58 | ||
|
|
4958eb8c72 | ||
|
|
43555c6662 | ||
|
|
7c08c54156 | ||
|
|
5834770e08 | ||
|
|
52406a12c2 | ||
|
|
e83814b0fa | ||
|
|
fba593a602 | ||
|
|
eec17d5c7b | ||
|
|
32bc72113a | ||
|
|
200d21c3fa | ||
|
|
3042b853bc | ||
|
|
50eb0437c1 | ||
|
|
00cc560b8e | ||
|
|
fa41f26b30 | ||
|
|
845969ec0d | ||
|
|
629df7895d | ||
|
|
55104e5b1e | ||
|
|
fcc95fdc11 | ||
|
|
10a94e36fd | ||
|
|
010ef9b8bc | ||
|
|
5ca7c27a07 | ||
|
|
c6632633f8 | ||
|
|
5b6eebbb9e | ||
|
|
a4c61bd071 | ||
|
|
2543279a6d | ||
|
|
95231b1a66 | ||
|
|
5f2877b85f | ||
|
|
47a51c3d42 | ||
|
|
3873dd1ad3 | ||
|
|
2ff10258e7 | ||
|
|
4360c4f7db | ||
|
|
8a6a139687 | ||
|
|
4eba86f348 | ||
|
|
ea973735c1 | ||
|
|
5707076e8d | ||
|
|
e1fb807a93 | ||
|
|
71575729e9 | ||
|
|
94e30f6442 | ||
|
|
c98941121a | ||
|
|
4ee9c30911 | ||
|
|
48cf20bb55 | ||
|
|
ac9b8a5f47 | ||
|
|
cad428ca6f | ||
|
|
bb5c1a5195 | ||
|
|
bef95231d2 | ||
|
|
0192c364b6 | ||
|
|
062a0dcee4 | ||
|
|
21d9fc2f62 | ||
|
|
614ff58be5 | ||
|
|
7b0477b1e0 | ||
|
|
a674614713 | ||
|
|
b549f1197d | ||
|
|
9d38bb4f94 | ||
|
|
44c6a8adcf | ||
|
|
aaf8679308 | ||
|
|
81d74b46b8 | ||
|
|
f7de5cac09 | ||
|
|
a1c68352a4 | ||
|
|
e70a3338a5 | ||
|
|
3c33e9f056 | ||
|
|
d160e5ff91 | ||
|
|
2d2343ab34 | ||
|
|
f6783daadd | ||
|
|
99cae36644 | ||
|
|
b292710102 | ||
|
|
b8b4a35178 | ||
|
|
ecc572a934 | ||
|
|
3a02341b0c | ||
|
|
e792fe8eb6 | ||
|
|
4c8ec1d685 | ||
|
|
f7bb5babf8 | ||
|
|
ae88198f20 | ||
|
|
d71c0d493f | ||
|
|
d611ae0cbe | ||
|
|
93287edfd5 | ||
|
|
dc287906d6 | ||
|
|
ab9459a67a | ||
|
|
aa8fc97b9b | ||
|
|
863febed62 | ||
|
|
2ff117dbf3 | ||
|
|
4838d0def9 | ||
|
|
c394ae8bf5 | ||
|
|
bdfe6728ae | ||
|
|
b1ab7479b3 | ||
|
|
03a0ac32ee | ||
|
|
bd2c292cb6 | ||
|
|
e8b67264ab | ||
|
|
3d5fd0b828 | ||
|
|
9524d31b1b | ||
|
|
a3606d6027 | ||
|
|
00b2c38109 | ||
|
|
0a9d3105fc | ||
|
|
0f0a9840e4 | ||
|
|
047fdddb0e | ||
|
|
79d6075a84 | ||
|
|
594497d234 | ||
|
|
1f7c0366f0 | ||
|
|
fe472cdd54 | ||
|
|
9c33c83351 | ||
|
|
9719406303 | ||
|
|
0b8f7ff6ad | ||
|
|
71c08cff73 | ||
|
|
1a6786488a | ||
|
|
472c7855a7 | ||
|
|
2fdeb9581c | ||
|
|
c4310fcdf4 | ||
|
|
33645d3c6b | ||
|
|
3618dcac7e | ||
|
|
432e3826c9 | ||
|
|
4e31508195 | ||
|
|
628f6673b0 | ||
|
|
1944cd8a6e | ||
|
|
d2001e38ca | ||
|
|
18889793b7 | ||
|
|
14d538b0fb | ||
|
|
3b7198b4cc | ||
|
|
abb891334a | ||
|
|
059f5c8d01 | ||
|
|
9d9546bb6d | ||
|
|
2ff05b1aa0 | ||
|
|
bf2c9a170d | ||
|
|
15caa9cd20 | ||
|
|
32ba49ae34 | ||
|
|
8b87924776 | ||
|
|
5bda05c1f9 | ||
|
|
a8481af765 | ||
|
|
b5ad507a57 | ||
|
|
8c8e4847a5 | ||
|
|
514c09dc39 | ||
|
|
c4bf7b11e2 | ||
|
|
a933b2126e | ||
|
|
001df4ea39 | ||
|
|
75d018a375 | ||
|
|
4449e42601 | ||
|
|
c45053bc11 | ||
|
|
5070e4fc33 | ||
|
|
7c1e70a538 | ||
|
|
7102c7922e | ||
|
|
2de9418307 | ||
|
|
e59cf4b6df | ||
|
|
9e8f0e00f3 | ||
|
|
d7650a71e0 | ||
|
|
420ddc3eac | ||
|
|
143e38c968 | ||
|
|
8e9fe4c4c1 | ||
|
|
95bccff2f7 | ||
|
|
780a608d6c | ||
|
|
18d93bbf47 | ||
|
|
b35ebd7d58 | ||
|
|
a11b047233 | ||
|
|
4eac9d4f8e | ||
|
|
d1e2a1a12f | ||
|
|
54d5abd3a8 | ||
|
|
a9fee8d7e2 | ||
|
|
6b15bdaa4e | ||
|
|
3771cf4e92 | ||
|
|
a5d83abdd0 | ||
|
|
f1e9f1a37c | ||
|
|
00b5ec87ba | ||
|
|
cf94e70a42 | ||
|
|
d47409c776 | ||
|
|
a9db13475b | ||
|
|
17fad0f1b9 | ||
|
|
5493d9e803 | ||
|
|
12b1edb958 | ||
|
|
7544a6dc4e | ||
|
|
f9621c8aac | ||
|
|
7a93b27478 | ||
|
|
9d48f9ec12 | ||
|
|
7c6e85cf9a | ||
|
|
24fbd161d8 | ||
|
|
0ae0d4175a | ||
|
|
6d550a02b7 | ||
|
|
a7f46b762f | ||
|
|
d1c7f9a323 | ||
|
|
93bfe65211 | ||
|
|
8ab53f74d5 | ||
|
|
04a69787bf | ||
|
|
dfd019de7d | ||
|
|
4db631fa27 | ||
|
|
ba03d756c6 | ||
|
|
04f59a9214 | ||
|
|
40ea252ad6 | ||
|
|
c193860468 | ||
|
|
94ad8c745e | ||
|
|
56c096996f | ||
|
|
8cbaf0f867 | ||
|
|
295901c0b3 | ||
|
|
2dda0c98ce | ||
|
|
70a38c8b01 | ||
|
|
1b12a7bd14 | ||
|
|
75e6332500 | ||
|
|
420ed719e8 | ||
|
|
3b75f9dd20 | ||
|
|
f01307b2a7 | ||
|
|
1f5e650423 | ||
|
|
d404112a88 | ||
|
|
dd3f539f74 | ||
|
|
0c51d09b69 | ||
|
|
0c53c52dd4 | ||
|
|
a687b52563 | ||
|
|
c8c34aa61f | ||
|
|
ed709d1476 | ||
|
|
3dc7cb0ec4 | ||
|
|
142f007e58 | ||
|
|
c8d2b225ba | ||
|
|
f7d6da55cd | ||
|
|
5a0c274807 | ||
|
|
591da12aa3 | ||
|
|
72475429e4 | ||
|
|
01da5969e4 | ||
|
|
46890a0642 | ||
|
|
143ca5b99f | ||
|
|
519866bc03 | ||
|
|
a93cac5ea6 | ||
|
|
43b578677f | ||
|
|
10cbe27e50 | ||
|
|
b1169ce7d1 | ||
|
|
766072eb89 | ||
|
|
0807cf5fe6 | ||
|
|
c6701cd474 | ||
|
|
b6866bee8e | ||
|
|
075774b6c0 | ||
|
|
7743d18006 | ||
|
|
633d40f155 | ||
|
|
5d82a80a34 | ||
|
|
3e5fe3f956 | ||
|
|
5a26a09d8e | ||
|
|
51e8dbe475 | ||
|
|
4a3d4537a3 | ||
|
|
a5208bd60f | ||
|
|
2597d6da00 | ||
|
|
1ca5a56952 | ||
|
|
80cff01154 | ||
|
|
1e17d5c455 | ||
|
|
ce1eb3c29a | ||
|
|
f11647ee7f | ||
|
|
60f281fd30 | ||
|
|
181984ba6d | ||
|
|
4685fae29c | ||
|
|
8619799e62 | ||
|
|
b1061378fb | ||
|
|
9bc2bfa506 | ||
|
|
078c5e7bc3 | ||
|
|
3f304b3c14 | ||
|
|
80348079a6 | ||
|
|
099b2a03e6 | ||
|
|
f0c63f4776 | ||
|
|
4e3aa3bc5c | ||
|
|
c5d82b85ed | ||
|
|
b1181d2e40 | ||
|
|
d931e04d47 | ||
|
|
016c4da6d2 | ||
|
|
38dd39a789 | ||
|
|
748f6a8bc9 |
16
.clang-format
Normal file
16
.clang-format
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
---
|
||||||
|
BasedOnStyle: Google
|
||||||
|
Standard: Cpp11
|
||||||
|
ColumnLimit: 0
|
||||||
|
AlignTrailingComments: false
|
||||||
|
NamespaceIndentation: All
|
||||||
|
DerivePointerAlignment: false
|
||||||
|
AlwaysBreakBeforeMultilineStrings: false
|
||||||
|
AccessModifierOffset: -2
|
||||||
|
ObjCSpaceBeforeProtocolList: true
|
||||||
|
SortIncludes: false
|
||||||
|
---
|
||||||
|
Language: Cpp
|
||||||
|
---
|
||||||
|
Language: ObjC
|
||||||
|
...
|
||||||
4
.gitignore
vendored
4
.gitignore
vendored
@@ -1,5 +1,5 @@
|
|||||||
.DS_Store
|
.DS_Store
|
||||||
xcuserdata
|
xcuserdata
|
||||||
project.xcworkspace
|
project.xcworkspace
|
||||||
|
/build
|
||||||
Tests/Payload
|
/Carthage/Build
|
||||||
|
|||||||
@@ -1,2 +1,3 @@
|
|||||||
language: objective-c
|
language: objective-c
|
||||||
script: ./Run-Tests.sh
|
script: ./Run-Tests.sh
|
||||||
|
osx_image: xcode11.3
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
Copyright (c) 2012-2014, Pierre-Olivier Latour
|
Copyright (c) 2012-2019, Pierre-Olivier Latour
|
||||||
All rights reserved.
|
All rights reserved.
|
||||||
|
|
||||||
Redistribution and use in source and binary forms, with or without
|
Redistribution and use in source and binary forms, with or without
|
||||||
@@ -25,11 +25,28 @@
|
|||||||
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
#import "GCDWebServerStreamingResponse.h"
|
// GCDWebServer Core
|
||||||
|
#import "GCDWebServer.h"
|
||||||
|
#import "GCDWebServerConnection.h"
|
||||||
|
#import "GCDWebServerFunctions.h"
|
||||||
|
#import "GCDWebServerHTTPStatusCodes.h"
|
||||||
|
#import "GCDWebServerResponse.h"
|
||||||
|
#import "GCDWebServerRequest.h"
|
||||||
|
|
||||||
typedef NSData* (^GCDWebServerStreamBlock)(NSError** error);
|
// GCDWebServer Requests
|
||||||
|
#import "GCDWebServerDataRequest.h"
|
||||||
|
#import "GCDWebServerFileRequest.h"
|
||||||
|
#import "GCDWebServerMultiPartFormRequest.h"
|
||||||
|
#import "GCDWebServerURLEncodedFormRequest.h"
|
||||||
|
|
||||||
@interface GCDWebServerStreamingResponse : GCDWebServerResponse
|
// GCDWebServer Responses
|
||||||
+ (instancetype)responseWithContentType:(NSString*)type streamBlock:(GCDWebServerStreamBlock)block;
|
#import "GCDWebServerDataResponse.h"
|
||||||
- (instancetype)initWithContentType:(NSString*)type streamBlock:(GCDWebServerStreamBlock)block; // Block must return empty NSData when done or nil on error and set the "error" argument accordingly
|
#import "GCDWebServerErrorResponse.h"
|
||||||
@end
|
#import "GCDWebServerFileResponse.h"
|
||||||
|
#import "GCDWebServerStreamedResponse.h"
|
||||||
|
|
||||||
|
// GCDWebUploader
|
||||||
|
#import "GCDWebUploader.h"
|
||||||
|
|
||||||
|
// GCDWebDAVServer
|
||||||
|
#import "GCDWebDAVServer.h"
|
||||||
22
Frameworks/Info.plist
Normal file
22
Frameworks/Info.plist
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>CFBundleDevelopmentRegion</key>
|
||||||
|
<string>en</string>
|
||||||
|
<key>CFBundleExecutable</key>
|
||||||
|
<string>$(EXECUTABLE_NAME)</string>
|
||||||
|
<key>CFBundleIdentifier</key>
|
||||||
|
<string>${PRODUCT_BUNDLE_IDENTIFIER}</string>
|
||||||
|
<key>CFBundleInfoDictionaryVersion</key>
|
||||||
|
<string>6.0</string>
|
||||||
|
<key>CFBundleName</key>
|
||||||
|
<string>$(PRODUCT_NAME)</string>
|
||||||
|
<key>CFBundlePackageType</key>
|
||||||
|
<string>FMWK</string>
|
||||||
|
<key>CFBundleVersion</key>
|
||||||
|
<string>${BUNDLE_VERSION_STRING}</string>
|
||||||
|
<key>CFBundleShortVersionString</key>
|
||||||
|
<string>${BUNDLE_VERSION_STRING}</string>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||
43
Frameworks/Tests.m
Normal file
43
Frameworks/Tests.m
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
#import <GCDWebServers/GCDWebServers.h>
|
||||||
|
#import <XCTest/XCTest.h>
|
||||||
|
|
||||||
|
#pragma clang diagnostic ignored "-Weverything" // Prevent "messaging to unqualified id" warnings
|
||||||
|
|
||||||
|
@interface Tests : XCTestCase
|
||||||
|
@end
|
||||||
|
|
||||||
|
@implementation Tests
|
||||||
|
|
||||||
|
- (void)testWebServer {
|
||||||
|
GCDWebServer* server = [[GCDWebServer alloc] init];
|
||||||
|
XCTAssertNotNil(server);
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)testDAVServer {
|
||||||
|
GCDWebDAVServer* server = [[GCDWebDAVServer alloc] init];
|
||||||
|
XCTAssertNotNil(server);
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)testWebUploader {
|
||||||
|
GCDWebUploader* server = [[GCDWebUploader alloc] init];
|
||||||
|
XCTAssertNotNil(server);
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)testPaths {
|
||||||
|
XCTAssertEqualObjects(GCDWebServerNormalizePath(@""), @"");
|
||||||
|
XCTAssertEqualObjects(GCDWebServerNormalizePath(@"/foo/"), @"/foo");
|
||||||
|
XCTAssertEqualObjects(GCDWebServerNormalizePath(@"foo/bar"), @"foo/bar");
|
||||||
|
XCTAssertEqualObjects(GCDWebServerNormalizePath(@"foo//bar"), @"foo/bar");
|
||||||
|
XCTAssertEqualObjects(GCDWebServerNormalizePath(@"foo/bar//"), @"foo/bar");
|
||||||
|
XCTAssertEqualObjects(GCDWebServerNormalizePath(@"foo/./bar"), @"foo/bar");
|
||||||
|
XCTAssertEqualObjects(GCDWebServerNormalizePath(@"foo/bar/."), @"foo/bar");
|
||||||
|
XCTAssertEqualObjects(GCDWebServerNormalizePath(@"foo/../bar"), @"bar");
|
||||||
|
XCTAssertEqualObjects(GCDWebServerNormalizePath(@"/foo/../bar"), @"/bar");
|
||||||
|
XCTAssertEqualObjects(GCDWebServerNormalizePath(@"/foo/.."), @"/");
|
||||||
|
XCTAssertEqualObjects(GCDWebServerNormalizePath(@"/.."), @"/");
|
||||||
|
XCTAssertEqualObjects(GCDWebServerNormalizePath(@"."), @"");
|
||||||
|
XCTAssertEqualObjects(GCDWebServerNormalizePath(@".."), @"");
|
||||||
|
XCTAssertEqualObjects(GCDWebServerNormalizePath(@"../.."), @"");
|
||||||
|
}
|
||||||
|
|
||||||
|
@end
|
||||||
3
Frameworks/module.modulemap
Normal file
3
Frameworks/module.modulemap
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
framework module GCDWebServers {
|
||||||
|
umbrella header "GCDWebServers.h"
|
||||||
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
Copyright (c) 2012-2014, Pierre-Olivier Latour
|
Copyright (c) 2012-2019, Pierre-Olivier Latour
|
||||||
All rights reserved.
|
All rights reserved.
|
||||||
|
|
||||||
Redistribution and use in source and binary forms, with or without
|
Redistribution and use in source and binary forms, with or without
|
||||||
@@ -27,32 +27,134 @@
|
|||||||
|
|
||||||
#import "GCDWebServer.h"
|
#import "GCDWebServer.h"
|
||||||
|
|
||||||
|
NS_ASSUME_NONNULL_BEGIN
|
||||||
|
|
||||||
@class GCDWebDAVServer;
|
@class GCDWebDAVServer;
|
||||||
|
|
||||||
// These methods are always called on main thread
|
/**
|
||||||
|
* Delegate methods for GCDWebDAVServer.
|
||||||
|
*
|
||||||
|
* @warning These methods are always called on the main thread in a serialized way.
|
||||||
|
*/
|
||||||
@protocol GCDWebDAVServerDelegate <GCDWebServerDelegate>
|
@protocol GCDWebDAVServerDelegate <GCDWebServerDelegate>
|
||||||
@optional
|
@optional
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This method is called whenever a file has been downloaded.
|
||||||
|
*/
|
||||||
- (void)davServer:(GCDWebDAVServer*)server didDownloadFileAtPath:(NSString*)path;
|
- (void)davServer:(GCDWebDAVServer*)server didDownloadFileAtPath:(NSString*)path;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This method is called whenever a file has been uploaded.
|
||||||
|
*/
|
||||||
- (void)davServer:(GCDWebDAVServer*)server didUploadFileAtPath:(NSString*)path;
|
- (void)davServer:(GCDWebDAVServer*)server didUploadFileAtPath:(NSString*)path;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This method is called whenever a file or directory has been moved.
|
||||||
|
*/
|
||||||
- (void)davServer:(GCDWebDAVServer*)server didMoveItemFromPath:(NSString*)fromPath toPath:(NSString*)toPath;
|
- (void)davServer:(GCDWebDAVServer*)server didMoveItemFromPath:(NSString*)fromPath toPath:(NSString*)toPath;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This method is called whenever a file or directory has been copied.
|
||||||
|
*/
|
||||||
- (void)davServer:(GCDWebDAVServer*)server didCopyItemFromPath:(NSString*)fromPath toPath:(NSString*)toPath;
|
- (void)davServer:(GCDWebDAVServer*)server didCopyItemFromPath:(NSString*)fromPath toPath:(NSString*)toPath;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This method is called whenever a file or directory has been deleted.
|
||||||
|
*/
|
||||||
- (void)davServer:(GCDWebDAVServer*)server didDeleteItemAtPath:(NSString*)path;
|
- (void)davServer:(GCDWebDAVServer*)server didDeleteItemAtPath:(NSString*)path;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This method is called whenever a directory has been created.
|
||||||
|
*/
|
||||||
- (void)davServer:(GCDWebDAVServer*)server didCreateDirectoryAtPath:(NSString*)path;
|
- (void)davServer:(GCDWebDAVServer*)server didCreateDirectoryAtPath:(NSString*)path;
|
||||||
|
|
||||||
@end
|
@end
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The GCDWebDAVServer subclass of GCDWebServer implements a class 1 compliant
|
||||||
|
* WebDAV server. It is also partially class 2 compliant but only when the
|
||||||
|
* client is the OS X WebDAV implementation (so it can work with the OS X Finder).
|
||||||
|
*
|
||||||
|
* See the README.md file for more information about the features of GCDWebDAVServer.
|
||||||
|
*/
|
||||||
@interface GCDWebDAVServer : GCDWebServer
|
@interface GCDWebDAVServer : GCDWebServer
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the upload directory as specified when the server was initialized.
|
||||||
|
*/
|
||||||
@property(nonatomic, readonly) NSString* uploadDirectory;
|
@property(nonatomic, readonly) NSString* uploadDirectory;
|
||||||
@property(nonatomic, assign) id<GCDWebDAVServerDelegate> delegate;
|
|
||||||
@property(nonatomic, copy) NSArray* allowedFileExtensions; // Default is nil i.e. all file extensions are allowed
|
/**
|
||||||
@property(nonatomic) BOOL showHiddenFiles; // Default is NO
|
* Sets the delegate for the server.
|
||||||
|
*/
|
||||||
|
@property(nonatomic, weak, nullable) id<GCDWebDAVServerDelegate> delegate;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets which files are allowed to be operated on depending on their extension.
|
||||||
|
*
|
||||||
|
* The default value is nil i.e. all file extensions are allowed.
|
||||||
|
*/
|
||||||
|
@property(nonatomic, copy) NSArray<NSString*>* allowedFileExtensions;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets if files and directories whose name start with a period are allowed to
|
||||||
|
* be operated on.
|
||||||
|
*
|
||||||
|
* The default value is NO.
|
||||||
|
*/
|
||||||
|
@property(nonatomic) BOOL allowHiddenItems;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This method is the designated initializer for the class.
|
||||||
|
*/
|
||||||
- (instancetype)initWithUploadDirectory:(NSString*)path;
|
- (instancetype)initWithUploadDirectory:(NSString*)path;
|
||||||
|
|
||||||
@end
|
@end
|
||||||
|
|
||||||
// These methods can be called from any thread
|
/**
|
||||||
|
* Hooks to customize the behavior of GCDWebDAVServer.
|
||||||
|
*
|
||||||
|
* @warning These methods can be called on any GCD thread.
|
||||||
|
*/
|
||||||
@interface GCDWebDAVServer (Subclassing)
|
@interface GCDWebDAVServer (Subclassing)
|
||||||
- (BOOL)shouldUploadFileAtPath:(NSString*)path withTemporaryFile:(NSString*)tempPath; // Default implementation returns YES
|
|
||||||
- (BOOL)shouldMoveItemFromPath:(NSString*)fromPath toPath:(NSString*)toPath; // Default implementation returns YES
|
/**
|
||||||
- (BOOL)shouldCopyItemFromPath:(NSString*)fromPath toPath:(NSString*)toPath; // Default implementation returns YES
|
* This method is called to check if a file upload is allowed to complete.
|
||||||
- (BOOL)shouldDeleteItemAtPath:(NSString*)path; // Default implementation returns YES
|
* The uploaded file is available for inspection at "tempPath".
|
||||||
- (BOOL)shouldCreateDirectoryAtPath:(NSString*)path; // Default implementation returns YES
|
*
|
||||||
|
* The default implementation returns YES.
|
||||||
|
*/
|
||||||
|
- (BOOL)shouldUploadFileAtPath:(NSString*)path withTemporaryFile:(NSString*)tempPath;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This method is called to check if a file or directory is allowed to be moved.
|
||||||
|
*
|
||||||
|
* The default implementation returns YES.
|
||||||
|
*/
|
||||||
|
- (BOOL)shouldMoveItemFromPath:(NSString*)fromPath toPath:(NSString*)toPath;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This method is called to check if a file or directory is allowed to be copied.
|
||||||
|
*
|
||||||
|
* The default implementation returns YES.
|
||||||
|
*/
|
||||||
|
- (BOOL)shouldCopyItemFromPath:(NSString*)fromPath toPath:(NSString*)toPath;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This method is called to check if a file or directory is allowed to be deleted.
|
||||||
|
*
|
||||||
|
* The default implementation returns YES.
|
||||||
|
*/
|
||||||
|
- (BOOL)shouldDeleteItemAtPath:(NSString*)path;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This method is called to check if a directory is allowed to be created.
|
||||||
|
*
|
||||||
|
* The default implementation returns YES.
|
||||||
|
*/
|
||||||
|
- (BOOL)shouldCreateDirectoryAtPath:(NSString*)path;
|
||||||
|
|
||||||
@end
|
@end
|
||||||
|
|
||||||
|
NS_ASSUME_NONNULL_END
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
Copyright (c) 2012-2014, Pierre-Olivier Latour
|
Copyright (c) 2012-2019, Pierre-Olivier Latour
|
||||||
All rights reserved.
|
All rights reserved.
|
||||||
|
|
||||||
Redistribution and use in source and binary forms, with or without
|
Redistribution and use in source and binary forms, with or without
|
||||||
@@ -25,6 +25,10 @@
|
|||||||
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
#if !__has_feature(objc_arc)
|
||||||
|
#error GCDWebDAVServer requires ARC
|
||||||
|
#endif
|
||||||
|
|
||||||
// WebDAV specifications: http://webdav.org/specs/rfc4918.html
|
// WebDAV specifications: http://webdav.org/specs/rfc4918.html
|
||||||
|
|
||||||
// Requires "HEADER_SEARCH_PATHS = $(SDKROOT)/usr/include/libxml2" in Xcode build settings
|
// Requires "HEADER_SEARCH_PATHS = $(SDKROOT)/usr/include/libxml2" in Xcode build settings
|
||||||
@@ -51,18 +55,110 @@ typedef NS_ENUM(NSInteger, DAVProperties) {
|
|||||||
kDAVAllProperties = kDAVProperty_ResourceType | kDAVProperty_CreationDate | kDAVProperty_LastModified | kDAVProperty_ContentLength
|
kDAVAllProperties = kDAVProperty_ResourceType | kDAVProperty_CreationDate | kDAVProperty_LastModified | kDAVProperty_ContentLength
|
||||||
};
|
};
|
||||||
|
|
||||||
@interface GCDWebDAVServer () {
|
NS_ASSUME_NONNULL_BEGIN
|
||||||
@private
|
|
||||||
NSString* _uploadDirectory;
|
@interface GCDWebDAVServer (Methods)
|
||||||
NSArray* _allowedExtensions;
|
- (nullable GCDWebServerResponse*)performOPTIONS:(GCDWebServerRequest*)request;
|
||||||
BOOL _showHidden;
|
- (nullable GCDWebServerResponse*)performGET:(GCDWebServerRequest*)request;
|
||||||
|
- (nullable GCDWebServerResponse*)performPUT:(GCDWebServerFileRequest*)request;
|
||||||
|
- (nullable GCDWebServerResponse*)performDELETE:(GCDWebServerRequest*)request;
|
||||||
|
- (nullable GCDWebServerResponse*)performMKCOL:(GCDWebServerDataRequest*)request;
|
||||||
|
- (nullable GCDWebServerResponse*)performCOPY:(GCDWebServerRequest*)request isMove:(BOOL)isMove;
|
||||||
|
- (nullable GCDWebServerResponse*)performPROPFIND:(GCDWebServerDataRequest*)request;
|
||||||
|
- (nullable GCDWebServerResponse*)performLOCK:(GCDWebServerDataRequest*)request;
|
||||||
|
- (nullable GCDWebServerResponse*)performUNLOCK:(GCDWebServerRequest*)request;
|
||||||
|
@end
|
||||||
|
|
||||||
|
NS_ASSUME_NONNULL_END
|
||||||
|
|
||||||
|
@implementation GCDWebDAVServer
|
||||||
|
|
||||||
|
@dynamic delegate;
|
||||||
|
|
||||||
|
- (instancetype)initWithUploadDirectory:(NSString*)path {
|
||||||
|
if ((self = [super init])) {
|
||||||
|
_uploadDirectory = [path copy];
|
||||||
|
GCDWebDAVServer* __unsafe_unretained server = self;
|
||||||
|
|
||||||
|
// 9.1 PROPFIND method
|
||||||
|
[self addDefaultHandlerForMethod:@"PROPFIND"
|
||||||
|
requestClass:[GCDWebServerDataRequest class]
|
||||||
|
processBlock:^GCDWebServerResponse*(GCDWebServerRequest* request) {
|
||||||
|
return [server performPROPFIND:(GCDWebServerDataRequest*)request];
|
||||||
|
}];
|
||||||
|
|
||||||
|
// 9.3 MKCOL Method
|
||||||
|
[self addDefaultHandlerForMethod:@"MKCOL"
|
||||||
|
requestClass:[GCDWebServerDataRequest class]
|
||||||
|
processBlock:^GCDWebServerResponse*(GCDWebServerRequest* request) {
|
||||||
|
return [server performMKCOL:(GCDWebServerDataRequest*)request];
|
||||||
|
}];
|
||||||
|
|
||||||
|
// 9.4 GET & HEAD methods
|
||||||
|
[self addDefaultHandlerForMethod:@"GET"
|
||||||
|
requestClass:[GCDWebServerRequest class]
|
||||||
|
processBlock:^GCDWebServerResponse*(GCDWebServerRequest* request) {
|
||||||
|
return [server performGET:request];
|
||||||
|
}];
|
||||||
|
|
||||||
|
// 9.6 DELETE method
|
||||||
|
[self addDefaultHandlerForMethod:@"DELETE"
|
||||||
|
requestClass:[GCDWebServerRequest class]
|
||||||
|
processBlock:^GCDWebServerResponse*(GCDWebServerRequest* request) {
|
||||||
|
return [server performDELETE:request];
|
||||||
|
}];
|
||||||
|
|
||||||
|
// 9.7 PUT method
|
||||||
|
[self addDefaultHandlerForMethod:@"PUT"
|
||||||
|
requestClass:[GCDWebServerFileRequest class]
|
||||||
|
processBlock:^GCDWebServerResponse*(GCDWebServerRequest* request) {
|
||||||
|
return [server performPUT:(GCDWebServerFileRequest*)request];
|
||||||
|
}];
|
||||||
|
|
||||||
|
// 9.8 COPY method
|
||||||
|
[self addDefaultHandlerForMethod:@"COPY"
|
||||||
|
requestClass:[GCDWebServerRequest class]
|
||||||
|
processBlock:^GCDWebServerResponse*(GCDWebServerRequest* request) {
|
||||||
|
return [server performCOPY:request isMove:NO];
|
||||||
|
}];
|
||||||
|
|
||||||
|
// 9.9 MOVE method
|
||||||
|
[self addDefaultHandlerForMethod:@"MOVE"
|
||||||
|
requestClass:[GCDWebServerRequest class]
|
||||||
|
processBlock:^GCDWebServerResponse*(GCDWebServerRequest* request) {
|
||||||
|
return [server performCOPY:request isMove:YES];
|
||||||
|
}];
|
||||||
|
|
||||||
|
// 9.10 LOCK method
|
||||||
|
[self addDefaultHandlerForMethod:@"LOCK"
|
||||||
|
requestClass:[GCDWebServerDataRequest class]
|
||||||
|
processBlock:^GCDWebServerResponse*(GCDWebServerRequest* request) {
|
||||||
|
return [server performLOCK:(GCDWebServerDataRequest*)request];
|
||||||
|
}];
|
||||||
|
|
||||||
|
// 9.11 UNLOCK method
|
||||||
|
[self addDefaultHandlerForMethod:@"UNLOCK"
|
||||||
|
requestClass:[GCDWebServerRequest class]
|
||||||
|
processBlock:^GCDWebServerResponse*(GCDWebServerRequest* request) {
|
||||||
|
return [server performUNLOCK:request];
|
||||||
|
}];
|
||||||
|
|
||||||
|
// 10.1 OPTIONS method / DAV Header
|
||||||
|
[self addDefaultHandlerForMethod:@"OPTIONS"
|
||||||
|
requestClass:[GCDWebServerRequest class]
|
||||||
|
processBlock:^GCDWebServerResponse*(GCDWebServerRequest* request) {
|
||||||
|
return [server performOPTIONS:request];
|
||||||
|
}];
|
||||||
|
}
|
||||||
|
return self;
|
||||||
}
|
}
|
||||||
|
|
||||||
@end
|
@end
|
||||||
|
|
||||||
@implementation GCDWebDAVServer (Methods)
|
@implementation GCDWebDAVServer (Methods)
|
||||||
|
|
||||||
- (BOOL)_checkFileExtension:(NSString*)fileName {
|
- (BOOL)_checkFileExtension:(NSString*)fileName {
|
||||||
if (_allowedExtensions && ![_allowedExtensions containsObject:[[fileName pathExtension] lowercaseString]]) {
|
if (_allowedFileExtensions && ![_allowedFileExtensions containsObject:[[fileName pathExtension] lowercaseString]]) {
|
||||||
return NO;
|
return NO;
|
||||||
}
|
}
|
||||||
return YES;
|
return YES;
|
||||||
@@ -85,27 +181,32 @@ static inline BOOL _IsMacFinder(GCDWebServerRequest* request) {
|
|||||||
|
|
||||||
- (GCDWebServerResponse*)performGET:(GCDWebServerRequest*)request {
|
- (GCDWebServerResponse*)performGET:(GCDWebServerRequest*)request {
|
||||||
NSString* relativePath = request.path;
|
NSString* relativePath = request.path;
|
||||||
NSString* absolutePath = [_uploadDirectory stringByAppendingPathComponent:relativePath];
|
NSString* absolutePath = [_uploadDirectory stringByAppendingPathComponent:GCDWebServerNormalizePath(relativePath)];
|
||||||
BOOL isDirectory = NO;
|
BOOL isDirectory = NO;
|
||||||
if (![absolutePath hasPrefix:_uploadDirectory] || ![[NSFileManager defaultManager] fileExistsAtPath:absolutePath isDirectory:&isDirectory]) {
|
if (![[NSFileManager defaultManager] fileExistsAtPath:absolutePath isDirectory:&isDirectory]) {
|
||||||
return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_NotFound message:@"\"%@\" does not exist", relativePath];
|
return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_NotFound message:@"\"%@\" does not exist", relativePath];
|
||||||
}
|
}
|
||||||
|
|
||||||
NSString* itemName = [absolutePath lastPathComponent];
|
NSString* itemName = [absolutePath lastPathComponent];
|
||||||
if (([itemName hasPrefix:@"."] && !_showHidden) || (!isDirectory && ![self _checkFileExtension:itemName])) {
|
if (([itemName hasPrefix:@"."] && !_allowHiddenItems) || (!isDirectory && ![self _checkFileExtension:itemName])) {
|
||||||
return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_Forbidden message:@"Downlading item name \"%@\" is not allowed", itemName];
|
return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_Forbidden message:@"Downlading item name \"%@\" is not allowed", itemName];
|
||||||
}
|
}
|
||||||
|
|
||||||
// Because HEAD requests are mapped to GET ones, we need to handle directories but it's OK to return nothing per http://webdav.org/specs/rfc4918.html#rfc.section.9.4
|
// Because HEAD requests are mapped to GET ones, we need to handle directories but it's OK to return nothing per http://webdav.org/specs/rfc4918.html#rfc.section.9.4
|
||||||
if (isDirectory) {
|
if (isDirectory) {
|
||||||
return [GCDWebServerResponse response];
|
return [GCDWebServerResponse response];
|
||||||
}
|
}
|
||||||
|
|
||||||
if ([self.delegate respondsToSelector:@selector(davServer:didDownloadFileAtPath:)]) {
|
if ([self.delegate respondsToSelector:@selector(davServer:didDownloadFileAtPath:)]) {
|
||||||
dispatch_async(dispatch_get_main_queue(), ^{
|
dispatch_async(dispatch_get_main_queue(), ^{
|
||||||
[self.delegate davServer:self didDownloadFileAtPath:absolutePath];
|
[self.delegate davServer:self didDownloadFileAtPath:absolutePath];
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ([request hasByteRange]) {
|
||||||
|
return [GCDWebServerFileResponse responseWithFile:absolutePath byteRange:request.byteRange];
|
||||||
|
}
|
||||||
|
|
||||||
return [GCDWebServerFileResponse responseWithFile:absolutePath];
|
return [GCDWebServerFileResponse responseWithFile:absolutePath];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -113,37 +214,34 @@ static inline BOOL _IsMacFinder(GCDWebServerRequest* request) {
|
|||||||
if ([request hasByteRange]) {
|
if ([request hasByteRange]) {
|
||||||
return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_BadRequest message:@"Range uploads not supported"];
|
return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_BadRequest message:@"Range uploads not supported"];
|
||||||
}
|
}
|
||||||
|
|
||||||
NSString* relativePath = request.path;
|
NSString* relativePath = request.path;
|
||||||
NSString* absolutePath = [_uploadDirectory stringByAppendingPathComponent:relativePath];
|
NSString* absolutePath = [_uploadDirectory stringByAppendingPathComponent:GCDWebServerNormalizePath(relativePath)];
|
||||||
if (![absolutePath hasPrefix:_uploadDirectory]) {
|
|
||||||
return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_NotFound message:@"\"%@\" does not exist", relativePath];
|
|
||||||
}
|
|
||||||
BOOL isDirectory;
|
BOOL isDirectory;
|
||||||
if (![[NSFileManager defaultManager] fileExistsAtPath:[absolutePath stringByDeletingLastPathComponent] isDirectory:&isDirectory] || !isDirectory) {
|
if (![[NSFileManager defaultManager] fileExistsAtPath:[absolutePath stringByDeletingLastPathComponent] isDirectory:&isDirectory] || !isDirectory) {
|
||||||
return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_Conflict message:@"Missing intermediate collection(s) for \"%@\"", relativePath];
|
return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_Conflict message:@"Missing intermediate collection(s) for \"%@\"", relativePath];
|
||||||
}
|
}
|
||||||
|
|
||||||
BOOL existing = [[NSFileManager defaultManager] fileExistsAtPath:absolutePath isDirectory:&isDirectory];
|
BOOL existing = [[NSFileManager defaultManager] fileExistsAtPath:absolutePath isDirectory:&isDirectory];
|
||||||
if (existing && isDirectory) {
|
if (existing && isDirectory) {
|
||||||
return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_MethodNotAllowed message:@"PUT not allowed on existing collection \"%@\"", relativePath];
|
return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_MethodNotAllowed message:@"PUT not allowed on existing collection \"%@\"", relativePath];
|
||||||
}
|
}
|
||||||
|
|
||||||
NSString* fileName = [absolutePath lastPathComponent];
|
NSString* fileName = [absolutePath lastPathComponent];
|
||||||
if (([fileName hasPrefix:@"."] && !_showHidden) || ![self _checkFileExtension:fileName]) {
|
if (([fileName hasPrefix:@"."] && !_allowHiddenItems) || ![self _checkFileExtension:fileName]) {
|
||||||
return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_Forbidden message:@"Uploading file name \"%@\" is not allowed", fileName];
|
return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_Forbidden message:@"Uploading file name \"%@\" is not allowed", fileName];
|
||||||
}
|
}
|
||||||
|
|
||||||
if (![self shouldUploadFileAtPath:absolutePath withTemporaryFile:request.temporaryPath]) {
|
if (![self shouldUploadFileAtPath:absolutePath withTemporaryFile:request.temporaryPath]) {
|
||||||
return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_Forbidden message:@"Uploading file to \"%@\" is not permitted", relativePath];
|
return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_Forbidden message:@"Uploading file to \"%@\" is not permitted", relativePath];
|
||||||
}
|
}
|
||||||
|
|
||||||
[[NSFileManager defaultManager] removeItemAtPath:absolutePath error:NULL];
|
[[NSFileManager defaultManager] removeItemAtPath:absolutePath error:NULL];
|
||||||
NSError* error = nil;
|
NSError* error = nil;
|
||||||
if (![[NSFileManager defaultManager] moveItemAtPath:request.temporaryPath toPath:absolutePath error:&error]) {
|
if (![[NSFileManager defaultManager] moveItemAtPath:request.temporaryPath toPath:absolutePath error:&error]) {
|
||||||
return [GCDWebServerErrorResponse responseWithServerError:kGCDWebServerHTTPStatusCode_InternalServerError underlyingError:error message:@"Failed moving uploaded file to \"%@\"", relativePath];
|
return [GCDWebServerErrorResponse responseWithServerError:kGCDWebServerHTTPStatusCode_InternalServerError underlyingError:error message:@"Failed moving uploaded file to \"%@\"", relativePath];
|
||||||
}
|
}
|
||||||
|
|
||||||
if ([self.delegate respondsToSelector:@selector(davServer:didUploadFileAtPath:)]) {
|
if ([self.delegate respondsToSelector:@selector(davServer:didUploadFileAtPath:)]) {
|
||||||
dispatch_async(dispatch_get_main_queue(), ^{
|
dispatch_async(dispatch_get_main_queue(), ^{
|
||||||
[self.delegate davServer:self didUploadFileAtPath:absolutePath];
|
[self.delegate davServer:self didUploadFileAtPath:absolutePath];
|
||||||
@@ -157,28 +255,28 @@ static inline BOOL _IsMacFinder(GCDWebServerRequest* request) {
|
|||||||
if (depthHeader && ![depthHeader isEqualToString:@"infinity"]) {
|
if (depthHeader && ![depthHeader isEqualToString:@"infinity"]) {
|
||||||
return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_BadRequest message:@"Unsupported 'Depth' header: %@", depthHeader];
|
return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_BadRequest message:@"Unsupported 'Depth' header: %@", depthHeader];
|
||||||
}
|
}
|
||||||
|
|
||||||
NSString* relativePath = request.path;
|
NSString* relativePath = request.path;
|
||||||
NSString* absolutePath = [_uploadDirectory stringByAppendingPathComponent:relativePath];
|
NSString* absolutePath = [_uploadDirectory stringByAppendingPathComponent:GCDWebServerNormalizePath(relativePath)];
|
||||||
BOOL isDirectory = NO;
|
BOOL isDirectory = NO;
|
||||||
if (![absolutePath hasPrefix:_uploadDirectory] || ![[NSFileManager defaultManager] fileExistsAtPath:absolutePath isDirectory:&isDirectory]) {
|
if (![[NSFileManager defaultManager] fileExistsAtPath:absolutePath isDirectory:&isDirectory]) {
|
||||||
return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_NotFound message:@"\"%@\" does not exist", relativePath];
|
return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_NotFound message:@"\"%@\" does not exist", relativePath];
|
||||||
}
|
}
|
||||||
|
|
||||||
NSString* itemName = [absolutePath lastPathComponent];
|
NSString* itemName = [absolutePath lastPathComponent];
|
||||||
if (([itemName hasPrefix:@"."] && !_showHidden) || (!isDirectory && ![self _checkFileExtension:itemName])) {
|
if (([itemName hasPrefix:@"."] && !_allowHiddenItems) || (!isDirectory && ![self _checkFileExtension:itemName])) {
|
||||||
return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_Forbidden message:@"Deleting item name \"%@\" is not allowed", itemName];
|
return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_Forbidden message:@"Deleting item name \"%@\" is not allowed", itemName];
|
||||||
}
|
}
|
||||||
|
|
||||||
if (![self shouldDeleteItemAtPath:absolutePath]) {
|
if (![self shouldDeleteItemAtPath:absolutePath]) {
|
||||||
return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_Forbidden message:@"Deleting \"%@\" is not permitted", relativePath];
|
return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_Forbidden message:@"Deleting \"%@\" is not permitted", relativePath];
|
||||||
}
|
}
|
||||||
|
|
||||||
NSError* error = nil;
|
NSError* error = nil;
|
||||||
if (![[NSFileManager defaultManager] removeItemAtPath:absolutePath error:&error]) {
|
if (![[NSFileManager defaultManager] removeItemAtPath:absolutePath error:&error]) {
|
||||||
return [GCDWebServerErrorResponse responseWithServerError:kGCDWebServerHTTPStatusCode_InternalServerError underlyingError:error message:@"Failed deleting \"%@\"", relativePath];
|
return [GCDWebServerErrorResponse responseWithServerError:kGCDWebServerHTTPStatusCode_InternalServerError underlyingError:error message:@"Failed deleting \"%@\"", relativePath];
|
||||||
}
|
}
|
||||||
|
|
||||||
if ([self.delegate respondsToSelector:@selector(davServer:didDeleteItemAtPath:)]) {
|
if ([self.delegate respondsToSelector:@selector(davServer:didDeleteItemAtPath:)]) {
|
||||||
dispatch_async(dispatch_get_main_queue(), ^{
|
dispatch_async(dispatch_get_main_queue(), ^{
|
||||||
[self.delegate davServer:self didDeleteItemAtPath:absolutePath];
|
[self.delegate davServer:self didDeleteItemAtPath:absolutePath];
|
||||||
@@ -191,26 +289,23 @@ static inline BOOL _IsMacFinder(GCDWebServerRequest* request) {
|
|||||||
if ([request hasBody] && (request.contentLength > 0)) {
|
if ([request hasBody] && (request.contentLength > 0)) {
|
||||||
return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_UnsupportedMediaType message:@"Unexpected request body for MKCOL method"];
|
return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_UnsupportedMediaType message:@"Unexpected request body for MKCOL method"];
|
||||||
}
|
}
|
||||||
|
|
||||||
NSString* relativePath = request.path;
|
NSString* relativePath = request.path;
|
||||||
NSString* absolutePath = [_uploadDirectory stringByAppendingPathComponent:relativePath];
|
NSString* absolutePath = [_uploadDirectory stringByAppendingPathComponent:GCDWebServerNormalizePath(relativePath)];
|
||||||
if (![absolutePath hasPrefix:_uploadDirectory]) {
|
|
||||||
return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_NotFound message:@"\"%@\" does not exist", relativePath];
|
|
||||||
}
|
|
||||||
BOOL isDirectory;
|
BOOL isDirectory;
|
||||||
if (![[NSFileManager defaultManager] fileExistsAtPath:[absolutePath stringByDeletingLastPathComponent] isDirectory:&isDirectory] || !isDirectory) {
|
if (![[NSFileManager defaultManager] fileExistsAtPath:[absolutePath stringByDeletingLastPathComponent] isDirectory:&isDirectory] || !isDirectory) {
|
||||||
return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_Conflict message:@"Missing intermediate collection(s) for \"%@\"", relativePath];
|
return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_Conflict message:@"Missing intermediate collection(s) for \"%@\"", relativePath];
|
||||||
}
|
}
|
||||||
|
|
||||||
NSString* directoryName = [absolutePath lastPathComponent];
|
NSString* directoryName = [absolutePath lastPathComponent];
|
||||||
if (!_showHidden && [directoryName hasPrefix:@"."]) {
|
if (!_allowHiddenItems && [directoryName hasPrefix:@"."]) {
|
||||||
return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_Forbidden message:@"Creating directory name \"%@\" is not allowed", directoryName];
|
return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_Forbidden message:@"Creating directory name \"%@\" is not allowed", directoryName];
|
||||||
}
|
}
|
||||||
|
|
||||||
if (![self shouldCreateDirectoryAtPath:absolutePath]) {
|
if (![self shouldCreateDirectoryAtPath:absolutePath]) {
|
||||||
return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_Forbidden message:@"Creating directory \"%@\" is not permitted", relativePath];
|
return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_Forbidden message:@"Creating directory \"%@\" is not permitted", relativePath];
|
||||||
}
|
}
|
||||||
|
|
||||||
NSError* error = nil;
|
NSError* error = nil;
|
||||||
if (![[NSFileManager defaultManager] createDirectoryAtPath:absolutePath withIntermediateDirectories:NO attributes:nil error:&error]) {
|
if (![[NSFileManager defaultManager] createDirectoryAtPath:absolutePath withIntermediateDirectories:NO attributes:nil error:&error]) {
|
||||||
return [GCDWebServerErrorResponse responseWithServerError:kGCDWebServerHTTPStatusCode_InternalServerError underlyingError:error message:@"Failed creating directory \"%@\"", relativePath];
|
return [GCDWebServerErrorResponse responseWithServerError:kGCDWebServerHTTPStatusCode_InternalServerError underlyingError:error message:@"Failed creating directory \"%@\"", relativePath];
|
||||||
@@ -219,12 +314,12 @@ static inline BOOL _IsMacFinder(GCDWebServerRequest* request) {
|
|||||||
NSString* creationDateHeader = [request.headers objectForKey:@"X-GCDWebServer-CreationDate"];
|
NSString* creationDateHeader = [request.headers objectForKey:@"X-GCDWebServer-CreationDate"];
|
||||||
if (creationDateHeader) {
|
if (creationDateHeader) {
|
||||||
NSDate* date = GCDWebServerParseISO8601(creationDateHeader);
|
NSDate* date = GCDWebServerParseISO8601(creationDateHeader);
|
||||||
if (!date || ![[NSFileManager defaultManager] setAttributes:@{NSFileCreationDate: date} ofItemAtPath:absolutePath error:&error]) {
|
if (!date || ![[NSFileManager defaultManager] setAttributes:@{NSFileCreationDate : date} ofItemAtPath:absolutePath error:&error]) {
|
||||||
return [GCDWebServerErrorResponse responseWithServerError:kGCDWebServerHTTPStatusCode_InternalServerError underlyingError:error message:@"Failed setting creation date for directory \"%@\"", relativePath];
|
return [GCDWebServerErrorResponse responseWithServerError:kGCDWebServerHTTPStatusCode_InternalServerError underlyingError:error message:@"Failed setting creation date for directory \"%@\"", relativePath];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
if ([self.delegate respondsToSelector:@selector(davServer:didCreateDirectoryAtPath:)]) {
|
if ([self.delegate respondsToSelector:@selector(davServer:didCreateDirectoryAtPath:)]) {
|
||||||
dispatch_async(dispatch_get_main_queue(), ^{
|
dispatch_async(dispatch_get_main_queue(), ^{
|
||||||
[self.delegate davServer:self didCreateDirectoryAtPath:absolutePath];
|
[self.delegate davServer:self didCreateDirectoryAtPath:absolutePath];
|
||||||
@@ -240,40 +335,45 @@ static inline BOOL _IsMacFinder(GCDWebServerRequest* request) {
|
|||||||
return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_BadRequest message:@"Unsupported 'Depth' header: %@", depthHeader];
|
return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_BadRequest message:@"Unsupported 'Depth' header: %@", depthHeader];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
NSString* srcRelativePath = request.path;
|
NSString* srcRelativePath = request.path;
|
||||||
NSString* srcAbsolutePath = [_uploadDirectory stringByAppendingPathComponent:srcRelativePath];
|
NSString* srcAbsolutePath = [_uploadDirectory stringByAppendingPathComponent:GCDWebServerNormalizePath(srcRelativePath)];
|
||||||
if (![srcAbsolutePath hasPrefix:_uploadDirectory]) {
|
|
||||||
return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_NotFound message:@"\"%@\" does not exist", srcRelativePath];
|
|
||||||
}
|
|
||||||
|
|
||||||
NSString* dstRelativePath = [request.headers objectForKey:@"Destination"];
|
NSString* dstRelativePath = [request.headers objectForKey:@"Destination"];
|
||||||
NSRange range = [dstRelativePath rangeOfString:[request.headers objectForKey:@"Host"]];
|
NSRange range = [dstRelativePath rangeOfString:(NSString*)[request.headers objectForKey:@"Host"]];
|
||||||
if ((dstRelativePath == nil) || (range.location == NSNotFound)) {
|
if ((dstRelativePath == nil) || (range.location == NSNotFound)) {
|
||||||
return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_BadRequest message:@"Malformed 'Destination' header: %@", dstRelativePath];
|
return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_BadRequest message:@"Malformed 'Destination' header: %@", dstRelativePath];
|
||||||
}
|
}
|
||||||
|
#pragma clang diagnostic push
|
||||||
|
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
|
||||||
dstRelativePath = [[dstRelativePath substringFromIndex:(range.location + range.length)] stringByReplacingPercentEscapesUsingEncoding:NSUTF8StringEncoding];
|
dstRelativePath = [[dstRelativePath substringFromIndex:(range.location + range.length)] stringByReplacingPercentEscapesUsingEncoding:NSUTF8StringEncoding];
|
||||||
NSString* dstAbsolutePath = [_uploadDirectory stringByAppendingPathComponent:dstRelativePath];
|
#pragma clang diagnostic pop
|
||||||
if (![dstAbsolutePath hasPrefix:_uploadDirectory]) {
|
NSString* dstAbsolutePath = [_uploadDirectory stringByAppendingPathComponent:GCDWebServerNormalizePath(dstRelativePath)];
|
||||||
|
if (!dstAbsolutePath) {
|
||||||
return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_NotFound message:@"\"%@\" does not exist", srcRelativePath];
|
return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_NotFound message:@"\"%@\" does not exist", srcRelativePath];
|
||||||
}
|
}
|
||||||
|
|
||||||
BOOL isDirectory;
|
BOOL isDirectory;
|
||||||
if (![[NSFileManager defaultManager] fileExistsAtPath:[dstAbsolutePath stringByDeletingLastPathComponent] isDirectory:&isDirectory] || !isDirectory) {
|
if (![[NSFileManager defaultManager] fileExistsAtPath:[dstAbsolutePath stringByDeletingLastPathComponent] isDirectory:&isDirectory] || !isDirectory) {
|
||||||
return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_Conflict message:@"Invalid destination \"%@\"", dstRelativePath];
|
return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_Conflict message:@"Invalid destination \"%@\"", dstRelativePath];
|
||||||
}
|
}
|
||||||
|
|
||||||
NSString* itemName = [dstAbsolutePath lastPathComponent];
|
NSString* srcName = [srcAbsolutePath lastPathComponent];
|
||||||
if ((!_showHidden && [itemName hasPrefix:@"."]) || (!isDirectory && ![self _checkFileExtension:itemName])) {
|
if ((!_allowHiddenItems && [srcName hasPrefix:@"."]) || (!isDirectory && ![self _checkFileExtension:srcName])) {
|
||||||
return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_Forbidden message:@"%@ to item name \"%@\" is not allowed", isMove ? @"Moving" : @"Copying", itemName];
|
return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_Forbidden message:@"%@ from item name \"%@\" is not allowed", isMove ? @"Moving" : @"Copying", srcName];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
NSString* dstName = [dstAbsolutePath lastPathComponent];
|
||||||
|
if ((!_allowHiddenItems && [dstName hasPrefix:@"."]) || (!isDirectory && ![self _checkFileExtension:dstName])) {
|
||||||
|
return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_Forbidden message:@"%@ to item name \"%@\" is not allowed", isMove ? @"Moving" : @"Copying", dstName];
|
||||||
|
}
|
||||||
|
|
||||||
NSString* overwriteHeader = [request.headers objectForKey:@"Overwrite"];
|
NSString* overwriteHeader = [request.headers objectForKey:@"Overwrite"];
|
||||||
BOOL existing = [[NSFileManager defaultManager] fileExistsAtPath:dstAbsolutePath];
|
BOOL existing = [[NSFileManager defaultManager] fileExistsAtPath:dstAbsolutePath];
|
||||||
if (existing && ((isMove && ![overwriteHeader isEqualToString:@"T"]) || (!isMove && [overwriteHeader isEqualToString:@"F"]))) {
|
if (existing && ((isMove && ![overwriteHeader isEqualToString:@"T"]) || (!isMove && [overwriteHeader isEqualToString:@"F"]))) {
|
||||||
return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_PreconditionFailed message:@"Destination \"%@\" already exists", dstRelativePath];
|
return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_PreconditionFailed message:@"Destination \"%@\" already exists", dstRelativePath];
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isMove) {
|
if (isMove) {
|
||||||
if (![self shouldMoveItemFromPath:srcAbsolutePath toPath:dstAbsolutePath]) {
|
if (![self shouldMoveItemFromPath:srcAbsolutePath toPath:dstAbsolutePath]) {
|
||||||
return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_Forbidden message:@"Moving \"%@\" to \"%@\" is not permitted", srcRelativePath, dstRelativePath];
|
return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_Forbidden message:@"Moving \"%@\" to \"%@\" is not permitted", srcRelativePath, dstRelativePath];
|
||||||
@@ -283,7 +383,7 @@ static inline BOOL _IsMacFinder(GCDWebServerRequest* request) {
|
|||||||
return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_Forbidden message:@"Copying \"%@\" to \"%@\" is not permitted", srcRelativePath, dstRelativePath];
|
return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_Forbidden message:@"Copying \"%@\" to \"%@\" is not permitted", srcRelativePath, dstRelativePath];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
NSError* error = nil;
|
NSError* error = nil;
|
||||||
if (isMove) {
|
if (isMove) {
|
||||||
[[NSFileManager defaultManager] removeItemAtPath:dstAbsolutePath error:NULL];
|
[[NSFileManager defaultManager] removeItemAtPath:dstAbsolutePath error:NULL];
|
||||||
@@ -295,7 +395,7 @@ static inline BOOL _IsMacFinder(GCDWebServerRequest* request) {
|
|||||||
return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_Forbidden underlyingError:error message:@"Failed copying \"%@\" to \"%@\"", srcRelativePath, dstRelativePath];
|
return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_Forbidden underlyingError:error message:@"Failed copying \"%@\" to \"%@\"", srcRelativePath, dstRelativePath];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isMove) {
|
if (isMove) {
|
||||||
if ([self.delegate respondsToSelector:@selector(davServer:didMoveItemFromPath:toPath:)]) {
|
if ([self.delegate respondsToSelector:@selector(davServer:didMoveItemFromPath:toPath:)]) {
|
||||||
dispatch_async(dispatch_get_main_queue(), ^{
|
dispatch_async(dispatch_get_main_queue(), ^{
|
||||||
@@ -309,7 +409,7 @@ static inline BOOL _IsMacFinder(GCDWebServerRequest* request) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return [GCDWebServerResponse responseWithStatusCode:(existing ? kGCDWebServerHTTPStatusCode_NoContent : kGCDWebServerHTTPStatusCode_Created)];
|
return [GCDWebServerResponse responseWithStatusCode:(existing ? kGCDWebServerHTTPStatusCode_NoContent : kGCDWebServerHTTPStatusCode_Created)];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -324,7 +424,10 @@ static inline xmlNodePtr _XMLChildWithName(xmlNodePtr child, const xmlChar* name
|
|||||||
}
|
}
|
||||||
|
|
||||||
- (void)_addPropertyResponseForItem:(NSString*)itemPath resource:(NSString*)resourcePath properties:(DAVProperties)properties xmlString:(NSMutableString*)xmlString {
|
- (void)_addPropertyResponseForItem:(NSString*)itemPath resource:(NSString*)resourcePath properties:(DAVProperties)properties xmlString:(NSMutableString*)xmlString {
|
||||||
|
#pragma clang diagnostic push
|
||||||
|
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
|
||||||
CFStringRef escapedPath = CFURLCreateStringByAddingPercentEscapes(kCFAllocatorDefault, (__bridge CFStringRef)resourcePath, NULL, CFSTR("<&>?+"), kCFStringEncodingUTF8);
|
CFStringRef escapedPath = CFURLCreateStringByAddingPercentEscapes(kCFAllocatorDefault, (__bridge CFStringRef)resourcePath, NULL, CFSTR("<&>?+"), kCFStringEncodingUTF8);
|
||||||
|
#pragma clang diagnostic pop
|
||||||
if (escapedPath) {
|
if (escapedPath) {
|
||||||
NSDictionary* attributes = [[NSFileManager defaultManager] attributesOfItemAtPath:itemPath error:NULL];
|
NSDictionary* attributes = [[NSFileManager defaultManager] attributesOfItemAtPath:itemPath error:NULL];
|
||||||
NSString* type = [attributes objectForKey:NSFileType];
|
NSString* type = [attributes objectForKey:NSFileType];
|
||||||
@@ -335,7 +438,7 @@ static inline xmlNodePtr _XMLChildWithName(xmlNodePtr child, const xmlChar* name
|
|||||||
[xmlString appendFormat:@"<D:href>%@</D:href>", escapedPath];
|
[xmlString appendFormat:@"<D:href>%@</D:href>", escapedPath];
|
||||||
[xmlString appendString:@"<D:propstat>"];
|
[xmlString appendString:@"<D:propstat>"];
|
||||||
[xmlString appendString:@"<D:prop>"];
|
[xmlString appendString:@"<D:prop>"];
|
||||||
|
|
||||||
if (properties & kDAVProperty_ResourceType) {
|
if (properties & kDAVProperty_ResourceType) {
|
||||||
if (isDirectory) {
|
if (isDirectory) {
|
||||||
[xmlString appendString:@"<D:resourcetype><D:collection/></D:resourcetype>"];
|
[xmlString appendString:@"<D:resourcetype><D:collection/></D:resourcetype>"];
|
||||||
@@ -343,19 +446,19 @@ static inline xmlNodePtr _XMLChildWithName(xmlNodePtr child, const xmlChar* name
|
|||||||
[xmlString appendString:@"<D:resourcetype/>"];
|
[xmlString appendString:@"<D:resourcetype/>"];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if ((properties & kDAVProperty_CreationDate) && [attributes objectForKey:NSFileCreationDate]) {
|
if ((properties & kDAVProperty_CreationDate) && [attributes objectForKey:NSFileCreationDate]) {
|
||||||
[xmlString appendFormat:@"<D:creationdate>%@</D:creationdate>", GCDWebServerFormatISO8601([attributes fileCreationDate])];
|
[xmlString appendFormat:@"<D:creationdate>%@</D:creationdate>", GCDWebServerFormatISO8601((NSDate*)[attributes fileCreationDate])];
|
||||||
}
|
}
|
||||||
|
|
||||||
if ((properties & kDAVProperty_LastModified) && isFile && [attributes objectForKey:NSFileModificationDate]) { // Last modification date is not useful for directories as it changes implicitely and 'Last-Modified' header is not provided for directories anyway
|
if ((properties & kDAVProperty_LastModified) && isFile && [attributes objectForKey:NSFileModificationDate]) { // Last modification date is not useful for directories as it changes implicitely and 'Last-Modified' header is not provided for directories anyway
|
||||||
[xmlString appendFormat:@"<D:getlastmodified>%@</D:getlastmodified>", GCDWebServerFormatRFC822([attributes fileModificationDate])];
|
[xmlString appendFormat:@"<D:getlastmodified>%@</D:getlastmodified>", GCDWebServerFormatRFC822((NSDate*)[attributes fileModificationDate])];
|
||||||
}
|
}
|
||||||
|
|
||||||
if ((properties & kDAVProperty_ContentLength) && !isDirectory && [attributes objectForKey:NSFileSize]) {
|
if ((properties & kDAVProperty_ContentLength) && !isDirectory && [attributes objectForKey:NSFileSize]) {
|
||||||
[xmlString appendFormat:@"<D:getcontentlength>%llu</D:getcontentlength>", [attributes fileSize]];
|
[xmlString appendFormat:@"<D:getcontentlength>%llu</D:getcontentlength>", [attributes fileSize]];
|
||||||
}
|
}
|
||||||
|
|
||||||
[xmlString appendString:@"</D:prop>"];
|
[xmlString appendString:@"</D:prop>"];
|
||||||
[xmlString appendString:@"<D:status>HTTP/1.1 200 OK</D:status>"];
|
[xmlString appendString:@"<D:status>HTTP/1.1 200 OK</D:status>"];
|
||||||
[xmlString appendString:@"</D:propstat>"];
|
[xmlString appendString:@"</D:propstat>"];
|
||||||
@@ -377,7 +480,7 @@ static inline xmlNodePtr _XMLChildWithName(xmlNodePtr child, const xmlChar* name
|
|||||||
} else {
|
} else {
|
||||||
return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_BadRequest message:@"Unsupported 'Depth' header: %@", depthHeader]; // TODO: Return 403 / propfind-finite-depth for "infinity" depth
|
return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_BadRequest message:@"Unsupported 'Depth' header: %@", depthHeader]; // TODO: Return 403 / propfind-finite-depth for "infinity" depth
|
||||||
}
|
}
|
||||||
|
|
||||||
DAVProperties properties = 0;
|
DAVProperties properties = 0;
|
||||||
if (request.data.length) {
|
if (request.data.length) {
|
||||||
BOOL success = YES;
|
BOOL success = YES;
|
||||||
@@ -413,36 +516,33 @@ static inline xmlNodePtr _XMLChildWithName(xmlNodePtr child, const xmlChar* name
|
|||||||
}
|
}
|
||||||
if (!success) {
|
if (!success) {
|
||||||
NSString* string = [[NSString alloc] initWithData:request.data encoding:NSUTF8StringEncoding];
|
NSString* string = [[NSString alloc] initWithData:request.data encoding:NSUTF8StringEncoding];
|
||||||
#if !__has_feature(objc_arc)
|
|
||||||
[string autorelease];
|
|
||||||
#endif
|
|
||||||
return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_BadRequest message:@"Invalid DAV properties:\n%@", string];
|
return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_BadRequest message:@"Invalid DAV properties:\n%@", string];
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
properties = kDAVAllProperties;
|
properties = kDAVAllProperties;
|
||||||
}
|
}
|
||||||
|
|
||||||
NSString* relativePath = request.path;
|
NSString* relativePath = request.path;
|
||||||
NSString* absolutePath = [_uploadDirectory stringByAppendingPathComponent:relativePath];
|
NSString* absolutePath = [_uploadDirectory stringByAppendingPathComponent:GCDWebServerNormalizePath(relativePath)];
|
||||||
BOOL isDirectory = NO;
|
BOOL isDirectory = NO;
|
||||||
if (![absolutePath hasPrefix:_uploadDirectory] || ![[NSFileManager defaultManager] fileExistsAtPath:absolutePath isDirectory:&isDirectory]) {
|
if (![[NSFileManager defaultManager] fileExistsAtPath:absolutePath isDirectory:&isDirectory]) {
|
||||||
return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_NotFound message:@"\"%@\" does not exist", relativePath];
|
return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_NotFound message:@"\"%@\" does not exist", relativePath];
|
||||||
}
|
}
|
||||||
|
|
||||||
NSString* itemName = [absolutePath lastPathComponent];
|
NSString* itemName = [absolutePath lastPathComponent];
|
||||||
if (([itemName hasPrefix:@"."] && !_showHidden) || (!isDirectory && ![self _checkFileExtension:itemName])) {
|
if (([itemName hasPrefix:@"."] && !_allowHiddenItems) || (!isDirectory && ![self _checkFileExtension:itemName])) {
|
||||||
return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_Forbidden message:@"Retrieving properties for item name \"%@\" is not allowed", itemName];
|
return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_Forbidden message:@"Retrieving properties for item name \"%@\" is not allowed", itemName];
|
||||||
}
|
}
|
||||||
|
|
||||||
NSArray* items = nil;
|
NSArray* items = nil;
|
||||||
if (isDirectory) {
|
if (isDirectory) {
|
||||||
NSError* error = nil;
|
NSError* error = nil;
|
||||||
items = [[NSFileManager defaultManager] contentsOfDirectoryAtPath:absolutePath error:&error];
|
items = [[[NSFileManager defaultManager] contentsOfDirectoryAtPath:absolutePath error:&error] sortedArrayUsingSelector:@selector(localizedStandardCompare:)];
|
||||||
if (items == nil) {
|
if (items == nil) {
|
||||||
return [GCDWebServerErrorResponse responseWithServerError:kGCDWebServerHTTPStatusCode_InternalServerError underlyingError:error message:@"Failed listing directory \"%@\"", relativePath];
|
return [GCDWebServerErrorResponse responseWithServerError:kGCDWebServerHTTPStatusCode_InternalServerError underlyingError:error message:@"Failed listing directory \"%@\"", relativePath];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
NSMutableString* xmlString = [NSMutableString stringWithString:@"<?xml version=\"1.0\" encoding=\"utf-8\" ?>"];
|
NSMutableString* xmlString = [NSMutableString stringWithString:@"<?xml version=\"1.0\" encoding=\"utf-8\" ?>"];
|
||||||
[xmlString appendString:@"<D:multistatus xmlns:D=\"DAV:\">\n"];
|
[xmlString appendString:@"<D:multistatus xmlns:D=\"DAV:\">\n"];
|
||||||
if (![relativePath hasPrefix:@"/"]) {
|
if (![relativePath hasPrefix:@"/"]) {
|
||||||
@@ -454,14 +554,14 @@ static inline xmlNodePtr _XMLChildWithName(xmlNodePtr child, const xmlChar* name
|
|||||||
relativePath = [relativePath stringByAppendingString:@"/"];
|
relativePath = [relativePath stringByAppendingString:@"/"];
|
||||||
}
|
}
|
||||||
for (NSString* item in items) {
|
for (NSString* item in items) {
|
||||||
if (_showHidden || ![item hasPrefix:@"."]) {
|
if (_allowHiddenItems || ![item hasPrefix:@"."]) {
|
||||||
[self _addPropertyResponseForItem:[absolutePath stringByAppendingPathComponent:item] resource:[relativePath stringByAppendingString:item] properties:properties xmlString:xmlString];
|
[self _addPropertyResponseForItem:[absolutePath stringByAppendingPathComponent:item] resource:[relativePath stringByAppendingString:item] properties:properties xmlString:xmlString];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
[xmlString appendString:@"</D:multistatus>"];
|
[xmlString appendString:@"</D:multistatus>"];
|
||||||
|
|
||||||
GCDWebServerDataResponse* response = [GCDWebServerDataResponse responseWithData:[xmlString dataUsingEncoding:NSUTF8StringEncoding]
|
GCDWebServerDataResponse* response = [GCDWebServerDataResponse responseWithData:(NSData*)[xmlString dataUsingEncoding:NSUTF8StringEncoding]
|
||||||
contentType:@"application/xml; charset=\"utf-8\""];
|
contentType:@"application/xml; charset=\"utf-8\""];
|
||||||
response.statusCode = kGCDWebServerHTTPStatusCode_MultiStatus;
|
response.statusCode = kGCDWebServerHTTPStatusCode_MultiStatus;
|
||||||
return response;
|
return response;
|
||||||
@@ -471,14 +571,14 @@ static inline xmlNodePtr _XMLChildWithName(xmlNodePtr child, const xmlChar* name
|
|||||||
if (!_IsMacFinder(request)) {
|
if (!_IsMacFinder(request)) {
|
||||||
return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_MethodNotAllowed message:@"LOCK method only allowed for Mac Finder"];
|
return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_MethodNotAllowed message:@"LOCK method only allowed for Mac Finder"];
|
||||||
}
|
}
|
||||||
|
|
||||||
NSString* relativePath = request.path;
|
NSString* relativePath = request.path;
|
||||||
NSString* absolutePath = [_uploadDirectory stringByAppendingPathComponent:relativePath];
|
NSString* absolutePath = [_uploadDirectory stringByAppendingPathComponent:GCDWebServerNormalizePath(relativePath)];
|
||||||
BOOL isDirectory = NO;
|
BOOL isDirectory = NO;
|
||||||
if (![absolutePath hasPrefix:_uploadDirectory] || ![[NSFileManager defaultManager] fileExistsAtPath:absolutePath isDirectory:&isDirectory]) {
|
if (![[NSFileManager defaultManager] fileExistsAtPath:absolutePath isDirectory:&isDirectory]) {
|
||||||
return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_NotFound message:@"\"%@\" does not exist", relativePath];
|
return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_NotFound message:@"\"%@\" does not exist", relativePath];
|
||||||
}
|
}
|
||||||
|
|
||||||
NSString* depthHeader = [request.headers objectForKey:@"Depth"];
|
NSString* depthHeader = [request.headers objectForKey:@"Depth"];
|
||||||
NSString* timeoutHeader = [request.headers objectForKey:@"Timeout"];
|
NSString* timeoutHeader = [request.headers objectForKey:@"Timeout"];
|
||||||
NSString* scope = nil;
|
NSString* scope = nil;
|
||||||
@@ -514,21 +614,18 @@ static inline xmlNodePtr _XMLChildWithName(xmlNodePtr child, const xmlChar* name
|
|||||||
}
|
}
|
||||||
if (!success) {
|
if (!success) {
|
||||||
NSString* string = [[NSString alloc] initWithData:request.data encoding:NSUTF8StringEncoding];
|
NSString* string = [[NSString alloc] initWithData:request.data encoding:NSUTF8StringEncoding];
|
||||||
#if !__has_feature(objc_arc)
|
|
||||||
[string autorelease];
|
|
||||||
#endif
|
|
||||||
return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_BadRequest message:@"Invalid DAV properties:\n%@", string];
|
return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_BadRequest message:@"Invalid DAV properties:\n%@", string];
|
||||||
}
|
}
|
||||||
|
|
||||||
if (![scope isEqualToString:@"exclusive"] || ![type isEqualToString:@"write"] || ![depthHeader isEqualToString:@"0"]) {
|
if (![scope isEqualToString:@"exclusive"] || ![type isEqualToString:@"write"] || ![depthHeader isEqualToString:@"0"]) {
|
||||||
return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_Forbidden message:@"Locking request \"%@/%@/%@\" for \"%@\" is not allowed", scope, type, depthHeader, relativePath];
|
return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_Forbidden message:@"Locking request \"%@/%@/%@\" for \"%@\" is not allowed", scope, type, depthHeader, relativePath];
|
||||||
}
|
}
|
||||||
|
|
||||||
NSString* itemName = [absolutePath lastPathComponent];
|
NSString* itemName = [absolutePath lastPathComponent];
|
||||||
if ((!_showHidden && [itemName hasPrefix:@"."]) || (!isDirectory && ![self _checkFileExtension:itemName])) {
|
if ((!_allowHiddenItems && [itemName hasPrefix:@"."]) || (!isDirectory && ![self _checkFileExtension:itemName])) {
|
||||||
return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_Forbidden message:@"Locking item name \"%@\" is not allowed", itemName];
|
return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_Forbidden message:@"Locking item name \"%@\" is not allowed", itemName];
|
||||||
}
|
}
|
||||||
|
|
||||||
#ifdef __GCDWEBSERVER_ENABLE_TESTING__
|
#ifdef __GCDWEBSERVER_ENABLE_TESTING__
|
||||||
NSString* lockTokenHeader = [request.headers objectForKey:@"X-GCDWebServer-LockToken"];
|
NSString* lockTokenHeader = [request.headers objectForKey:@"X-GCDWebServer-LockToken"];
|
||||||
if (lockTokenHeader) {
|
if (lockTokenHeader) {
|
||||||
@@ -542,7 +639,7 @@ static inline xmlNodePtr _XMLChildWithName(xmlNodePtr child, const xmlChar* name
|
|||||||
CFRelease(string);
|
CFRelease(string);
|
||||||
CFRelease(uuid);
|
CFRelease(uuid);
|
||||||
}
|
}
|
||||||
|
|
||||||
NSMutableString* xmlString = [NSMutableString stringWithString:@"<?xml version=\"1.0\" encoding=\"utf-8\" ?>"];
|
NSMutableString* xmlString = [NSMutableString stringWithString:@"<?xml version=\"1.0\" encoding=\"utf-8\" ?>"];
|
||||||
[xmlString appendString:@"<D:prop xmlns:D=\"DAV:\">\n"];
|
[xmlString appendString:@"<D:prop xmlns:D=\"DAV:\">\n"];
|
||||||
[xmlString appendString:@"<D:lockdiscovery>\n<D:activelock>\n"];
|
[xmlString appendString:@"<D:lockdiscovery>\n<D:activelock>\n"];
|
||||||
@@ -556,13 +653,13 @@ static inline xmlNodePtr _XMLChildWithName(xmlNodePtr child, const xmlChar* name
|
|||||||
[xmlString appendFormat:@"<D:timeout>%@</D:timeout>\n", timeoutHeader];
|
[xmlString appendFormat:@"<D:timeout>%@</D:timeout>\n", timeoutHeader];
|
||||||
}
|
}
|
||||||
[xmlString appendFormat:@"<D:locktoken><D:href>%@</D:href></D:locktoken>\n", token];
|
[xmlString appendFormat:@"<D:locktoken><D:href>%@</D:href></D:locktoken>\n", token];
|
||||||
NSString* lockroot = [@"http://" stringByAppendingString:[[request.headers objectForKey:@"Host"] stringByAppendingString:[@"/" stringByAppendingString:relativePath]]];
|
NSString* lockroot = [@"http://" stringByAppendingString:[(NSString*)[request.headers objectForKey:@"Host"] stringByAppendingString:[@"/" stringByAppendingString:relativePath]]];
|
||||||
[xmlString appendFormat:@"<D:lockroot><D:href>%@</D:href></D:lockroot>\n", lockroot];
|
[xmlString appendFormat:@"<D:lockroot><D:href>%@</D:href></D:lockroot>\n", lockroot];
|
||||||
[xmlString appendString:@"</D:activelock>\n</D:lockdiscovery>\n"];
|
[xmlString appendString:@"</D:activelock>\n</D:lockdiscovery>\n"];
|
||||||
[xmlString appendString:@"</D:prop>"];
|
[xmlString appendString:@"</D:prop>"];
|
||||||
|
|
||||||
[self logVerbose:@"WebDAV pretending to lock \"%@\"", relativePath];
|
[self logVerbose:@"WebDAV pretending to lock \"%@\"", relativePath];
|
||||||
GCDWebServerDataResponse* response = [GCDWebServerDataResponse responseWithData:[xmlString dataUsingEncoding:NSUTF8StringEncoding]
|
GCDWebServerDataResponse* response = [GCDWebServerDataResponse responseWithData:(NSData*)[xmlString dataUsingEncoding:NSUTF8StringEncoding]
|
||||||
contentType:@"application/xml; charset=\"utf-8\""];
|
contentType:@"application/xml; charset=\"utf-8\""];
|
||||||
return response;
|
return response;
|
||||||
}
|
}
|
||||||
@@ -571,110 +668,30 @@ static inline xmlNodePtr _XMLChildWithName(xmlNodePtr child, const xmlChar* name
|
|||||||
if (!_IsMacFinder(request)) {
|
if (!_IsMacFinder(request)) {
|
||||||
return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_MethodNotAllowed message:@"UNLOCK method only allowed for Mac Finder"];
|
return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_MethodNotAllowed message:@"UNLOCK method only allowed for Mac Finder"];
|
||||||
}
|
}
|
||||||
|
|
||||||
NSString* relativePath = request.path;
|
NSString* relativePath = request.path;
|
||||||
NSString* absolutePath = [_uploadDirectory stringByAppendingPathComponent:relativePath];
|
NSString* absolutePath = [_uploadDirectory stringByAppendingPathComponent:GCDWebServerNormalizePath(relativePath)];
|
||||||
BOOL isDirectory = NO;
|
BOOL isDirectory = NO;
|
||||||
if (![absolutePath hasPrefix:_uploadDirectory] || ![[NSFileManager defaultManager] fileExistsAtPath:absolutePath isDirectory:&isDirectory]) {
|
if (![[NSFileManager defaultManager] fileExistsAtPath:absolutePath isDirectory:&isDirectory]) {
|
||||||
return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_NotFound message:@"\"%@\" does not exist", relativePath];
|
return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_NotFound message:@"\"%@\" does not exist", relativePath];
|
||||||
}
|
}
|
||||||
|
|
||||||
NSString* tokenHeader = [request.headers objectForKey:@"Lock-Token"];
|
NSString* tokenHeader = [request.headers objectForKey:@"Lock-Token"];
|
||||||
if (!tokenHeader.length) {
|
if (!tokenHeader.length) {
|
||||||
return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_BadRequest message:@"Missing 'Lock-Token' header"];
|
return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_BadRequest message:@"Missing 'Lock-Token' header"];
|
||||||
}
|
}
|
||||||
|
|
||||||
NSString* itemName = [absolutePath lastPathComponent];
|
NSString* itemName = [absolutePath lastPathComponent];
|
||||||
if ((!_showHidden && [itemName hasPrefix:@"."]) || (!isDirectory && ![self _checkFileExtension:itemName])) {
|
if ((!_allowHiddenItems && [itemName hasPrefix:@"."]) || (!isDirectory && ![self _checkFileExtension:itemName])) {
|
||||||
return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_Forbidden message:@"Unlocking item name \"%@\" is not allowed", itemName];
|
return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_Forbidden message:@"Unlocking item name \"%@\" is not allowed", itemName];
|
||||||
}
|
}
|
||||||
|
|
||||||
[self logVerbose:@"WebDAV pretending to unlock \"%@\"", relativePath];
|
[self logVerbose:@"WebDAV pretending to unlock \"%@\"", relativePath];
|
||||||
return [GCDWebServerResponse responseWithStatusCode:kGCDWebServerHTTPStatusCode_NoContent];
|
return [GCDWebServerResponse responseWithStatusCode:kGCDWebServerHTTPStatusCode_NoContent];
|
||||||
}
|
}
|
||||||
|
|
||||||
@end
|
@end
|
||||||
|
|
||||||
@implementation GCDWebDAVServer
|
|
||||||
|
|
||||||
@synthesize uploadDirectory=_uploadDirectory, allowedFileExtensions=_allowedExtensions, showHiddenFiles=_showHidden;
|
|
||||||
|
|
||||||
- (instancetype)initWithUploadDirectory:(NSString*)path {
|
|
||||||
if ((self = [super init])) {
|
|
||||||
_uploadDirectory = [[path stringByStandardizingPath] copy];
|
|
||||||
#if __has_feature(objc_arc)
|
|
||||||
GCDWebDAVServer* __unsafe_unretained server = self;
|
|
||||||
#else
|
|
||||||
__block GCDWebDAVServer* server = self;
|
|
||||||
#endif
|
|
||||||
|
|
||||||
// 9.1 PROPFIND method
|
|
||||||
[self addDefaultHandlerForMethod:@"PROPFIND" requestClass:[GCDWebServerDataRequest class] processBlock:^GCDWebServerResponse *(GCDWebServerRequest* request) {
|
|
||||||
return [server performPROPFIND:(GCDWebServerDataRequest*)request];
|
|
||||||
}];
|
|
||||||
|
|
||||||
// 9.3 MKCOL Method
|
|
||||||
[self addDefaultHandlerForMethod:@"MKCOL" requestClass:[GCDWebServerDataRequest class] processBlock:^GCDWebServerResponse *(GCDWebServerRequest* request) {
|
|
||||||
return [server performMKCOL:(GCDWebServerDataRequest*)request];
|
|
||||||
}];
|
|
||||||
|
|
||||||
// 9.4 GET & HEAD methods
|
|
||||||
[self addDefaultHandlerForMethod:@"GET" requestClass:[GCDWebServerRequest class] processBlock:^GCDWebServerResponse *(GCDWebServerRequest* request) {
|
|
||||||
return [server performGET:request];
|
|
||||||
}];
|
|
||||||
|
|
||||||
// 9.6 DELETE method
|
|
||||||
[self addDefaultHandlerForMethod:@"DELETE" requestClass:[GCDWebServerRequest class] processBlock:^GCDWebServerResponse *(GCDWebServerRequest* request) {
|
|
||||||
return [server performDELETE:request];
|
|
||||||
}];
|
|
||||||
|
|
||||||
// 9.7 PUT method
|
|
||||||
[self addDefaultHandlerForMethod:@"PUT" requestClass:[GCDWebServerFileRequest class] processBlock:^GCDWebServerResponse *(GCDWebServerRequest* request) {
|
|
||||||
return [server performPUT:(GCDWebServerFileRequest*)request];
|
|
||||||
}];
|
|
||||||
|
|
||||||
// 9.8 COPY method
|
|
||||||
[self addDefaultHandlerForMethod:@"COPY" requestClass:[GCDWebServerRequest class] processBlock:^GCDWebServerResponse *(GCDWebServerRequest* request) {
|
|
||||||
return [server performCOPY:request isMove:NO];
|
|
||||||
}];
|
|
||||||
|
|
||||||
// 9.9 MOVE method
|
|
||||||
[self addDefaultHandlerForMethod:@"MOVE" requestClass:[GCDWebServerRequest class] processBlock:^GCDWebServerResponse *(GCDWebServerRequest* request) {
|
|
||||||
return [server performCOPY:request isMove:YES];
|
|
||||||
}];
|
|
||||||
|
|
||||||
// 9.10 LOCK method
|
|
||||||
[self addDefaultHandlerForMethod:@"LOCK" requestClass:[GCDWebServerDataRequest class] processBlock:^GCDWebServerResponse *(GCDWebServerRequest* request) {
|
|
||||||
return [server performLOCK:(GCDWebServerDataRequest*)request];
|
|
||||||
}];
|
|
||||||
|
|
||||||
// 9.11 UNLOCK method
|
|
||||||
[self addDefaultHandlerForMethod:@"UNLOCK" requestClass:[GCDWebServerRequest class] processBlock:^GCDWebServerResponse *(GCDWebServerRequest* request) {
|
|
||||||
return [server performUNLOCK:request];
|
|
||||||
}];
|
|
||||||
|
|
||||||
// 10.1 OPTIONS method / DAV Header
|
|
||||||
[self addDefaultHandlerForMethod:@"OPTIONS" requestClass:[GCDWebServerRequest class] processBlock:^GCDWebServerResponse *(GCDWebServerRequest* request) {
|
|
||||||
return [server performOPTIONS:request];
|
|
||||||
}];
|
|
||||||
|
|
||||||
}
|
|
||||||
return self;
|
|
||||||
}
|
|
||||||
|
|
||||||
#if !__has_feature(objc_arc)
|
|
||||||
|
|
||||||
- (void)dealloc {
|
|
||||||
[_uploadDirectory release];
|
|
||||||
[_allowedExtensions release];
|
|
||||||
|
|
||||||
[super dealloc];
|
|
||||||
}
|
|
||||||
|
|
||||||
#endif
|
|
||||||
|
|
||||||
@end
|
|
||||||
|
|
||||||
@implementation GCDWebDAVServer (Subclassing)
|
@implementation GCDWebDAVServer (Subclassing)
|
||||||
|
|
||||||
- (BOOL)shouldUploadFileAtPath:(NSString*)path withTemporaryFile:(NSString*)tempPath {
|
- (BOOL)shouldUploadFileAtPath:(NSString*)path withTemporaryFile:(NSString*)tempPath {
|
||||||
|
|||||||
@@ -1,20 +1,21 @@
|
|||||||
# http://guides.cocoapods.org/syntax/podspec.html
|
# http://guides.cocoapods.org/syntax/podspec.html
|
||||||
# Verify Podspec with:
|
# http://guides.cocoapods.org/making/getting-setup-with-trunk.html
|
||||||
# sudo gem update cocoapods
|
# $ sudo gem update cocoapods
|
||||||
# pod spec lint GCDWebServer.podspec --verbose
|
# (optional) $ pod trunk register {email} {name} --description={computer}
|
||||||
# Add to source line:
|
# $ pod trunk --verbose push
|
||||||
# :tag => s.version.to_s
|
# DELETE THIS SECTION BEFORE PROCEEDING!
|
||||||
|
|
||||||
Pod::Spec.new do |s|
|
Pod::Spec.new do |s|
|
||||||
s.name = 'GCDWebServer'
|
s.name = 'GCDWebServer'
|
||||||
s.version = '2.2'
|
s.version = '3.5.4'
|
||||||
s.author = { 'Pierre-Olivier Latour' => 'info@pol-online.net' }
|
s.author = { 'Pierre-Olivier Latour' => 'info@pol-online.net' }
|
||||||
s.license = { :type => 'BSD', :file => 'LICENSE' }
|
s.license = { :type => 'BSD', :file => 'LICENSE' }
|
||||||
s.homepage = 'https://github.com/swisspol/GCDWebServer'
|
s.homepage = 'https://github.com/swisspol/GCDWebServer'
|
||||||
s.summary = 'Lightweight GCD based HTTP server for OS X & iOS (includes web based uploader & WebDAV server)'
|
s.summary = 'Lightweight GCD based HTTP server for OS X & iOS (includes web based uploader & WebDAV server)'
|
||||||
|
|
||||||
s.source = { :git => 'https://github.com/swisspol/GCDWebServer.git' }
|
s.source = { :git => 'https://github.com/swisspol/GCDWebServer.git', :tag => s.version.to_s }
|
||||||
s.ios.deployment_target = '5.0'
|
s.ios.deployment_target = '8.0'
|
||||||
|
s.tvos.deployment_target = '9.0'
|
||||||
s.osx.deployment_target = '10.7'
|
s.osx.deployment_target = '10.7'
|
||||||
s.requires_arc = true
|
s.requires_arc = true
|
||||||
|
|
||||||
@@ -22,10 +23,14 @@ Pod::Spec.new do |s|
|
|||||||
|
|
||||||
s.subspec 'Core' do |cs|
|
s.subspec 'Core' do |cs|
|
||||||
cs.source_files = 'GCDWebServer/**/*.{h,m}'
|
cs.source_files = 'GCDWebServer/**/*.{h,m}'
|
||||||
|
cs.private_header_files = "GCDWebServer/Core/GCDWebServerPrivate.h"
|
||||||
cs.requires_arc = true
|
cs.requires_arc = true
|
||||||
cs.ios.library = 'z'
|
cs.ios.library = 'z'
|
||||||
|
cs.ios.frameworks = 'CoreServices', 'CFNetwork'
|
||||||
|
cs.tvos.library = 'z'
|
||||||
|
cs.tvos.frameworks = 'CoreServices', 'CFNetwork'
|
||||||
cs.osx.library = 'z'
|
cs.osx.library = 'z'
|
||||||
cs.compiler_flags = '-DNDEBUG' # TODO: Only set this for Release configuration
|
cs.osx.framework = 'SystemConfiguration'
|
||||||
end
|
end
|
||||||
|
|
||||||
s.subspec 'WebDAV' do |cs|
|
s.subspec 'WebDAV' do |cs|
|
||||||
@@ -33,6 +38,7 @@ Pod::Spec.new do |s|
|
|||||||
cs.source_files = 'GCDWebDAVServer/*.{h,m}'
|
cs.source_files = 'GCDWebDAVServer/*.{h,m}'
|
||||||
cs.requires_arc = true
|
cs.requires_arc = true
|
||||||
cs.ios.library = 'xml2'
|
cs.ios.library = 'xml2'
|
||||||
|
cs.tvos.library = 'xml2'
|
||||||
cs.osx.library = 'xml2'
|
cs.osx.library = 'xml2'
|
||||||
cs.compiler_flags = '-I$(SDKROOT)/usr/include/libxml2'
|
cs.compiler_flags = '-I$(SDKROOT)/usr/include/libxml2'
|
||||||
end
|
end
|
||||||
@@ -42,6 +48,5 @@ Pod::Spec.new do |s|
|
|||||||
cs.source_files = 'GCDWebUploader/*.{h,m}'
|
cs.source_files = 'GCDWebUploader/*.{h,m}'
|
||||||
cs.requires_arc = true
|
cs.requires_arc = true
|
||||||
cs.resource = "GCDWebUploader/GCDWebUploader.bundle"
|
cs.resource = "GCDWebUploader/GCDWebUploader.bundle"
|
||||||
end
|
end
|
||||||
|
|
||||||
end
|
end
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,87 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<Scheme
|
||||||
|
LastUpgradeVersion = "1130"
|
||||||
|
version = "1.3">
|
||||||
|
<BuildAction
|
||||||
|
parallelizeBuildables = "YES"
|
||||||
|
buildImplicitDependencies = "YES">
|
||||||
|
<BuildActionEntries>
|
||||||
|
<BuildActionEntry
|
||||||
|
buildForTesting = "NO"
|
||||||
|
buildForRunning = "YES"
|
||||||
|
buildForProfiling = "YES"
|
||||||
|
buildForArchiving = "YES"
|
||||||
|
buildForAnalyzing = "YES">
|
||||||
|
<BuildableReference
|
||||||
|
BuildableIdentifier = "primary"
|
||||||
|
BlueprintIdentifier = "CEE28CD01AE004D800F4023C"
|
||||||
|
BuildableName = "GCDWebServers.framework"
|
||||||
|
BlueprintName = "GCDWebServers (Mac)"
|
||||||
|
ReferencedContainer = "container:GCDWebServer.xcodeproj">
|
||||||
|
</BuildableReference>
|
||||||
|
</BuildActionEntry>
|
||||||
|
</BuildActionEntries>
|
||||||
|
</BuildAction>
|
||||||
|
<TestAction
|
||||||
|
buildConfiguration = "Debug"
|
||||||
|
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||||
|
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||||
|
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||||
|
enableAddressSanitizer = "YES">
|
||||||
|
<Testables>
|
||||||
|
<TestableReference
|
||||||
|
skipped = "NO">
|
||||||
|
<BuildableReference
|
||||||
|
BuildableIdentifier = "primary"
|
||||||
|
BlueprintIdentifier = "E24039241BA09207000B7089"
|
||||||
|
BuildableName = "Tests.xctest"
|
||||||
|
BlueprintName = "Tests (Mac)"
|
||||||
|
ReferencedContainer = "container:GCDWebServer.xcodeproj">
|
||||||
|
</BuildableReference>
|
||||||
|
</TestableReference>
|
||||||
|
</Testables>
|
||||||
|
</TestAction>
|
||||||
|
<LaunchAction
|
||||||
|
buildConfiguration = "Debug"
|
||||||
|
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||||
|
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||||
|
launchStyle = "0"
|
||||||
|
useCustomWorkingDirectory = "NO"
|
||||||
|
ignoresPersistentStateOnLaunch = "NO"
|
||||||
|
debugDocumentVersioning = "YES"
|
||||||
|
debugServiceExtension = "internal"
|
||||||
|
allowLocationSimulation = "YES">
|
||||||
|
<MacroExpansion>
|
||||||
|
<BuildableReference
|
||||||
|
BuildableIdentifier = "primary"
|
||||||
|
BlueprintIdentifier = "CEE28CD01AE004D800F4023C"
|
||||||
|
BuildableName = "GCDWebServers.framework"
|
||||||
|
BlueprintName = "GCDWebServers (Mac)"
|
||||||
|
ReferencedContainer = "container:GCDWebServer.xcodeproj">
|
||||||
|
</BuildableReference>
|
||||||
|
</MacroExpansion>
|
||||||
|
</LaunchAction>
|
||||||
|
<ProfileAction
|
||||||
|
buildConfiguration = "Release"
|
||||||
|
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||||
|
savedToolIdentifier = ""
|
||||||
|
useCustomWorkingDirectory = "NO"
|
||||||
|
debugDocumentVersioning = "YES">
|
||||||
|
<MacroExpansion>
|
||||||
|
<BuildableReference
|
||||||
|
BuildableIdentifier = "primary"
|
||||||
|
BlueprintIdentifier = "CEE28CD01AE004D800F4023C"
|
||||||
|
BuildableName = "GCDWebServers.framework"
|
||||||
|
BlueprintName = "GCDWebServers (Mac)"
|
||||||
|
ReferencedContainer = "container:GCDWebServer.xcodeproj">
|
||||||
|
</BuildableReference>
|
||||||
|
</MacroExpansion>
|
||||||
|
</ProfileAction>
|
||||||
|
<AnalyzeAction
|
||||||
|
buildConfiguration = "Debug">
|
||||||
|
</AnalyzeAction>
|
||||||
|
<ArchiveAction
|
||||||
|
buildConfiguration = "Release"
|
||||||
|
revealArchiveInOrganizer = "YES">
|
||||||
|
</ArchiveAction>
|
||||||
|
</Scheme>
|
||||||
@@ -0,0 +1,85 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<Scheme
|
||||||
|
LastUpgradeVersion = "1130"
|
||||||
|
version = "1.3">
|
||||||
|
<BuildAction
|
||||||
|
parallelizeBuildables = "YES"
|
||||||
|
buildImplicitDependencies = "YES">
|
||||||
|
<BuildActionEntries>
|
||||||
|
<BuildActionEntry
|
||||||
|
buildForTesting = "NO"
|
||||||
|
buildForRunning = "YES"
|
||||||
|
buildForProfiling = "YES"
|
||||||
|
buildForArchiving = "YES"
|
||||||
|
buildForAnalyzing = "YES">
|
||||||
|
<BuildableReference
|
||||||
|
BuildableIdentifier = "primary"
|
||||||
|
BlueprintIdentifier = "CEE28CEE1AE0051F00F4023C"
|
||||||
|
BuildableName = "GCDWebServers.framework"
|
||||||
|
BlueprintName = "GCDWebServers (iOS)"
|
||||||
|
ReferencedContainer = "container:GCDWebServer.xcodeproj">
|
||||||
|
</BuildableReference>
|
||||||
|
</BuildActionEntry>
|
||||||
|
</BuildActionEntries>
|
||||||
|
</BuildAction>
|
||||||
|
<TestAction
|
||||||
|
buildConfiguration = "Debug"
|
||||||
|
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||||
|
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||||
|
shouldUseLaunchSchemeArgsEnv = "YES">
|
||||||
|
<MacroExpansion>
|
||||||
|
<BuildableReference
|
||||||
|
BuildableIdentifier = "primary"
|
||||||
|
BlueprintIdentifier = "CEE28CEE1AE0051F00F4023C"
|
||||||
|
BuildableName = "GCDWebServers.framework"
|
||||||
|
BlueprintName = "GCDWebServers (iOS)"
|
||||||
|
ReferencedContainer = "container:GCDWebServer.xcodeproj">
|
||||||
|
</BuildableReference>
|
||||||
|
</MacroExpansion>
|
||||||
|
<Testables>
|
||||||
|
</Testables>
|
||||||
|
</TestAction>
|
||||||
|
<LaunchAction
|
||||||
|
buildConfiguration = "Debug"
|
||||||
|
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||||
|
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||||
|
launchStyle = "0"
|
||||||
|
useCustomWorkingDirectory = "NO"
|
||||||
|
ignoresPersistentStateOnLaunch = "NO"
|
||||||
|
debugDocumentVersioning = "YES"
|
||||||
|
debugServiceExtension = "internal"
|
||||||
|
allowLocationSimulation = "YES">
|
||||||
|
<MacroExpansion>
|
||||||
|
<BuildableReference
|
||||||
|
BuildableIdentifier = "primary"
|
||||||
|
BlueprintIdentifier = "CEE28CEE1AE0051F00F4023C"
|
||||||
|
BuildableName = "GCDWebServers.framework"
|
||||||
|
BlueprintName = "GCDWebServers (iOS)"
|
||||||
|
ReferencedContainer = "container:GCDWebServer.xcodeproj">
|
||||||
|
</BuildableReference>
|
||||||
|
</MacroExpansion>
|
||||||
|
</LaunchAction>
|
||||||
|
<ProfileAction
|
||||||
|
buildConfiguration = "Release"
|
||||||
|
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||||
|
savedToolIdentifier = ""
|
||||||
|
useCustomWorkingDirectory = "NO"
|
||||||
|
debugDocumentVersioning = "YES">
|
||||||
|
<MacroExpansion>
|
||||||
|
<BuildableReference
|
||||||
|
BuildableIdentifier = "primary"
|
||||||
|
BlueprintIdentifier = "CEE28CEE1AE0051F00F4023C"
|
||||||
|
BuildableName = "GCDWebServers.framework"
|
||||||
|
BlueprintName = "GCDWebServers (iOS)"
|
||||||
|
ReferencedContainer = "container:GCDWebServer.xcodeproj">
|
||||||
|
</BuildableReference>
|
||||||
|
</MacroExpansion>
|
||||||
|
</ProfileAction>
|
||||||
|
<AnalyzeAction
|
||||||
|
buildConfiguration = "Debug">
|
||||||
|
</AnalyzeAction>
|
||||||
|
<ArchiveAction
|
||||||
|
buildConfiguration = "Release"
|
||||||
|
revealArchiveInOrganizer = "YES">
|
||||||
|
</ArchiveAction>
|
||||||
|
</Scheme>
|
||||||
@@ -0,0 +1,85 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<Scheme
|
||||||
|
LastUpgradeVersion = "1130"
|
||||||
|
version = "1.3">
|
||||||
|
<BuildAction
|
||||||
|
parallelizeBuildables = "YES"
|
||||||
|
buildImplicitDependencies = "YES">
|
||||||
|
<BuildActionEntries>
|
||||||
|
<BuildActionEntry
|
||||||
|
buildForTesting = "NO"
|
||||||
|
buildForRunning = "YES"
|
||||||
|
buildForProfiling = "YES"
|
||||||
|
buildForArchiving = "YES"
|
||||||
|
buildForAnalyzing = "YES">
|
||||||
|
<BuildableReference
|
||||||
|
BuildableIdentifier = "primary"
|
||||||
|
BlueprintIdentifier = "E2DDD18A1BE69404002CE867"
|
||||||
|
BuildableName = "GCDWebServers.framework"
|
||||||
|
BlueprintName = "GCDWebServers (tvOS)"
|
||||||
|
ReferencedContainer = "container:GCDWebServer.xcodeproj">
|
||||||
|
</BuildableReference>
|
||||||
|
</BuildActionEntry>
|
||||||
|
</BuildActionEntries>
|
||||||
|
</BuildAction>
|
||||||
|
<TestAction
|
||||||
|
buildConfiguration = "Debug"
|
||||||
|
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||||
|
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||||
|
shouldUseLaunchSchemeArgsEnv = "YES">
|
||||||
|
<MacroExpansion>
|
||||||
|
<BuildableReference
|
||||||
|
BuildableIdentifier = "primary"
|
||||||
|
BlueprintIdentifier = "E2DDD18A1BE69404002CE867"
|
||||||
|
BuildableName = "GCDWebServers.framework"
|
||||||
|
BlueprintName = "GCDWebServers (tvOS)"
|
||||||
|
ReferencedContainer = "container:GCDWebServer.xcodeproj">
|
||||||
|
</BuildableReference>
|
||||||
|
</MacroExpansion>
|
||||||
|
<Testables>
|
||||||
|
</Testables>
|
||||||
|
</TestAction>
|
||||||
|
<LaunchAction
|
||||||
|
buildConfiguration = "Debug"
|
||||||
|
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||||
|
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||||
|
launchStyle = "0"
|
||||||
|
useCustomWorkingDirectory = "NO"
|
||||||
|
ignoresPersistentStateOnLaunch = "NO"
|
||||||
|
debugDocumentVersioning = "YES"
|
||||||
|
debugServiceExtension = "internal"
|
||||||
|
allowLocationSimulation = "YES">
|
||||||
|
<MacroExpansion>
|
||||||
|
<BuildableReference
|
||||||
|
BuildableIdentifier = "primary"
|
||||||
|
BlueprintIdentifier = "E2DDD18A1BE69404002CE867"
|
||||||
|
BuildableName = "GCDWebServers.framework"
|
||||||
|
BlueprintName = "GCDWebServers (tvOS)"
|
||||||
|
ReferencedContainer = "container:GCDWebServer.xcodeproj">
|
||||||
|
</BuildableReference>
|
||||||
|
</MacroExpansion>
|
||||||
|
</LaunchAction>
|
||||||
|
<ProfileAction
|
||||||
|
buildConfiguration = "Release"
|
||||||
|
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||||
|
savedToolIdentifier = ""
|
||||||
|
useCustomWorkingDirectory = "NO"
|
||||||
|
debugDocumentVersioning = "YES">
|
||||||
|
<MacroExpansion>
|
||||||
|
<BuildableReference
|
||||||
|
BuildableIdentifier = "primary"
|
||||||
|
BlueprintIdentifier = "E2DDD18A1BE69404002CE867"
|
||||||
|
BuildableName = "GCDWebServers.framework"
|
||||||
|
BlueprintName = "GCDWebServers (tvOS)"
|
||||||
|
ReferencedContainer = "container:GCDWebServer.xcodeproj">
|
||||||
|
</BuildableReference>
|
||||||
|
</MacroExpansion>
|
||||||
|
</ProfileAction>
|
||||||
|
<AnalyzeAction
|
||||||
|
buildConfiguration = "Debug">
|
||||||
|
</AnalyzeAction>
|
||||||
|
<ArchiveAction
|
||||||
|
buildConfiguration = "Release"
|
||||||
|
revealArchiveInOrganizer = "YES">
|
||||||
|
</ArchiveAction>
|
||||||
|
</Scheme>
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
Copyright (c) 2012-2014, Pierre-Olivier Latour
|
Copyright (c) 2012-2019, Pierre-Olivier Latour
|
||||||
All rights reserved.
|
All rights reserved.
|
||||||
|
|
||||||
Redistribution and use in source and binary forms, with or without
|
Redistribution and use in source and binary forms, with or without
|
||||||
@@ -30,90 +30,615 @@
|
|||||||
#import "GCDWebServerRequest.h"
|
#import "GCDWebServerRequest.h"
|
||||||
#import "GCDWebServerResponse.h"
|
#import "GCDWebServerResponse.h"
|
||||||
|
|
||||||
typedef NS_ENUM(int, GCDWebServerLogLevel) {
|
NS_ASSUME_NONNULL_BEGIN
|
||||||
kGCDWebServerLogLevel_Debug = 0, // Only available if "NDEBUG" is not defined when building
|
|
||||||
kGCDWebServerLogLevel_Verbose,
|
|
||||||
kGCDWebServerLogLevel_Info,
|
|
||||||
kGCDWebServerLogLevel_Warning,
|
|
||||||
kGCDWebServerLogLevel_Error,
|
|
||||||
kGCDWebServerLogLevel_Exception,
|
|
||||||
};
|
|
||||||
|
|
||||||
typedef GCDWebServerRequest* (^GCDWebServerMatchBlock)(NSString* requestMethod, NSURL* requestURL, NSDictionary* requestHeaders, NSString* urlPath, NSDictionary* urlQuery);
|
/**
|
||||||
typedef GCDWebServerResponse* (^GCDWebServerProcessBlock)(GCDWebServerRequest* request);
|
* The GCDWebServerMatchBlock is called for every handler added to the
|
||||||
|
* GCDWebServer whenever a new HTTP request has started (i.e. HTTP headers have
|
||||||
|
* been received). The block is passed the basic info for the request (HTTP method,
|
||||||
|
* URL, headers...) and must decide if it wants to handle it or not.
|
||||||
|
*
|
||||||
|
* If the handler can handle the request, the block must return a new
|
||||||
|
* GCDWebServerRequest instance created with the same basic info.
|
||||||
|
* Otherwise, it simply returns nil.
|
||||||
|
*/
|
||||||
|
typedef GCDWebServerRequest* _Nullable (^GCDWebServerMatchBlock)(NSString* requestMethod, NSURL* requestURL, NSDictionary<NSString*, NSString*>* requestHeaders, NSString* urlPath, NSDictionary<NSString*, NSString*>* urlQuery);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The GCDWebServerProcessBlock is called after the HTTP request has been fully
|
||||||
|
* received (i.e. the entire HTTP body has been read). The block is passed the
|
||||||
|
* GCDWebServerRequest created at the previous step by the GCDWebServerMatchBlock.
|
||||||
|
*
|
||||||
|
* The block must return a GCDWebServerResponse or nil on error, which will
|
||||||
|
* result in a 500 HTTP status code returned to the client. It's however
|
||||||
|
* recommended to return a GCDWebServerErrorResponse on error so more useful
|
||||||
|
* information can be returned to the client.
|
||||||
|
*/
|
||||||
|
typedef GCDWebServerResponse* _Nullable (^GCDWebServerProcessBlock)(__kindof GCDWebServerRequest* request);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The GCDWebServerAsynchronousProcessBlock works like the GCDWebServerProcessBlock
|
||||||
|
* except the GCDWebServerResponse can be returned to the server at a later time
|
||||||
|
* allowing for asynchronous generation of the response.
|
||||||
|
*
|
||||||
|
* The block must eventually call "completionBlock" passing a GCDWebServerResponse
|
||||||
|
* or nil on error, which will result in a 500 HTTP status code returned to the client.
|
||||||
|
* It's however recommended to return a GCDWebServerErrorResponse on error so more
|
||||||
|
* useful information can be returned to the client.
|
||||||
|
*/
|
||||||
|
typedef void (^GCDWebServerCompletionBlock)(GCDWebServerResponse* _Nullable response);
|
||||||
|
typedef void (^GCDWebServerAsyncProcessBlock)(__kindof GCDWebServerRequest* request, GCDWebServerCompletionBlock completionBlock);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The GCDWebServerBuiltInLoggerBlock is used to override the built-in logger at runtime.
|
||||||
|
* The block will be passed the log level and the log message, see setLogLevel for
|
||||||
|
* documentation of the log levels for the built-in logger.
|
||||||
|
*/
|
||||||
|
typedef void (^GCDWebServerBuiltInLoggerBlock)(int level, NSString* _Nonnull message);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The port used by the GCDWebServer (NSNumber / NSUInteger).
|
||||||
|
*
|
||||||
|
* The default value is 0 i.e. let the OS pick a random port.
|
||||||
|
*/
|
||||||
|
extern NSString* const GCDWebServerOption_Port;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The Bonjour name used by the GCDWebServer (NSString). If set to an empty string,
|
||||||
|
* the name will automatically take the value of the GCDWebServerOption_ServerName
|
||||||
|
* option. If this option is set to nil, Bonjour will be disabled.
|
||||||
|
*
|
||||||
|
* The default value is nil.
|
||||||
|
*/
|
||||||
|
extern NSString* const GCDWebServerOption_BonjourName;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The Bonjour TXT Data used by the GCDWebServer (NSDictionary<NSString, NSString>).
|
||||||
|
*
|
||||||
|
* The default value is nil.
|
||||||
|
*/
|
||||||
|
extern NSString* const GCDWebServerOption_BonjourTXTData;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The Bonjour service type used by the GCDWebServer (NSString).
|
||||||
|
*
|
||||||
|
* The default value is "_http._tcp", the service type for HTTP web servers.
|
||||||
|
*/
|
||||||
|
extern NSString* const GCDWebServerOption_BonjourType;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Request a port mapping in the NAT gateway (NSNumber / BOOL).
|
||||||
|
*
|
||||||
|
* This uses the DNSService API under the hood which supports IPv4 mappings only.
|
||||||
|
*
|
||||||
|
* The default value is NO.
|
||||||
|
*
|
||||||
|
* @warning The external port set up by the NAT gateway may be different than
|
||||||
|
* the one used by the GCDWebServer.
|
||||||
|
*/
|
||||||
|
extern NSString* const GCDWebServerOption_RequestNATPortMapping;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Only accept HTTP requests coming from localhost i.e. not from the outside
|
||||||
|
* network (NSNumber / BOOL).
|
||||||
|
*
|
||||||
|
* The default value is NO.
|
||||||
|
*
|
||||||
|
* @warning Bonjour and NAT port mapping should be disabled if using this option
|
||||||
|
* since the server will not be reachable from the outside network anyway.
|
||||||
|
*/
|
||||||
|
extern NSString* const GCDWebServerOption_BindToLocalhost;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The maximum number of incoming HTTP requests that can be queued waiting to
|
||||||
|
* be handled before new ones are dropped (NSNumber / NSUInteger).
|
||||||
|
*
|
||||||
|
* The default value is 16.
|
||||||
|
*/
|
||||||
|
extern NSString* const GCDWebServerOption_MaxPendingConnections;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The value for "Server" HTTP header used by the GCDWebServer (NSString).
|
||||||
|
*
|
||||||
|
* The default value is the GCDWebServer class name.
|
||||||
|
*/
|
||||||
|
extern NSString* const GCDWebServerOption_ServerName;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The authentication method used by the GCDWebServer
|
||||||
|
* (one of "GCDWebServerAuthenticationMethod_...").
|
||||||
|
*
|
||||||
|
* The default value is nil i.e. authentication is disabled.
|
||||||
|
*/
|
||||||
|
extern NSString* const GCDWebServerOption_AuthenticationMethod;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The authentication realm used by the GCDWebServer (NSString).
|
||||||
|
*
|
||||||
|
* The default value is the same as the GCDWebServerOption_ServerName option.
|
||||||
|
*/
|
||||||
|
extern NSString* const GCDWebServerOption_AuthenticationRealm;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The authentication accounts used by the GCDWebServer
|
||||||
|
* (NSDictionary of username / password pairs).
|
||||||
|
*
|
||||||
|
* The default value is nil i.e. no accounts.
|
||||||
|
*/
|
||||||
|
extern NSString* const GCDWebServerOption_AuthenticationAccounts;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The class used by the GCDWebServer when instantiating GCDWebServerConnection
|
||||||
|
* (subclass of GCDWebServerConnection).
|
||||||
|
*
|
||||||
|
* The default value is the GCDWebServerConnection class.
|
||||||
|
*/
|
||||||
|
extern NSString* const GCDWebServerOption_ConnectionClass;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Allow the GCDWebServer to pretend "HEAD" requests are actually "GET" ones
|
||||||
|
* and automatically discard the HTTP body of the response (NSNumber / BOOL).
|
||||||
|
*
|
||||||
|
* The default value is YES.
|
||||||
|
*/
|
||||||
|
extern NSString* const GCDWebServerOption_AutomaticallyMapHEADToGET;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The interval expressed in seconds used by the GCDWebServer to decide how to
|
||||||
|
* coalesce calls to -webServerDidConnect: and -webServerDidDisconnect:
|
||||||
|
* (NSNumber / double). Coalescing will be disabled if the interval is <= 0.0.
|
||||||
|
*
|
||||||
|
* The default value is 1.0 second.
|
||||||
|
*/
|
||||||
|
extern NSString* const GCDWebServerOption_ConnectedStateCoalescingInterval;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the dispatch queue priority on which server connection will be
|
||||||
|
* run (NSNumber / long).
|
||||||
|
*
|
||||||
|
*
|
||||||
|
* The default value is DISPATCH_QUEUE_PRIORITY_DEFAULT.
|
||||||
|
*/
|
||||||
|
extern NSString* const GCDWebServerOption_DispatchQueuePriority;
|
||||||
|
|
||||||
extern NSString* const GCDWebServerOption_Port; // NSNumber / NSUInteger (default is 0 i.e. use a random port)
|
|
||||||
extern NSString* const GCDWebServerOption_BonjourName; // NSString (default is empty string i.e. use computer name)
|
|
||||||
extern NSString* const GCDWebServerOption_MaxPendingConnections; // NSNumber / NSUInteger (default is 16)
|
|
||||||
extern NSString* const GCDWebServerOption_ServerName; // NSString (default is server class name)
|
|
||||||
extern NSString* const GCDWebServerOption_ConnectionClass; // Subclass of GCDWebServerConnection (default is GCDWebServerConnection class)
|
|
||||||
extern NSString* const GCDWebServerOption_AutomaticallyMapHEADToGET; // NSNumber / BOOL (default is YES)
|
|
||||||
extern NSString* const GCDWebServerOption_ConnectedStateCoalescingInterval; // NSNumber / double (default is 1.0 seconds - set to <=0.0 to disable coaslescing of -webServerDidConnect: / -webServerDidDisconnect:)
|
|
||||||
#if TARGET_OS_IPHONE
|
#if TARGET_OS_IPHONE
|
||||||
extern NSString* const GCDWebServerOption_AutomaticallySuspendInBackground; // NSNumber / BOOL (default is YES)
|
|
||||||
|
/**
|
||||||
|
* Enables the GCDWebServer to automatically suspend itself (as if -stop was
|
||||||
|
* called) when the iOS app goes into the background and the last
|
||||||
|
* GCDWebServerConnection is closed, then resume itself (as if -start was called)
|
||||||
|
* when the iOS app comes back to the foreground (NSNumber / BOOL).
|
||||||
|
*
|
||||||
|
* See the README.md file for more information about this option.
|
||||||
|
*
|
||||||
|
* The default value is YES.
|
||||||
|
*
|
||||||
|
* @warning The running property will be NO while the GCDWebServer is suspended.
|
||||||
|
*/
|
||||||
|
extern NSString* const GCDWebServerOption_AutomaticallySuspendInBackground;
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
/**
|
||||||
|
* HTTP Basic Authentication scheme (see https://tools.ietf.org/html/rfc2617).
|
||||||
|
*
|
||||||
|
* @warning Use of this authentication scheme is not recommended as the
|
||||||
|
* passwords are sent in clear.
|
||||||
|
*/
|
||||||
|
extern NSString* const GCDWebServerAuthenticationMethod_Basic;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* HTTP Digest Access Authentication scheme (see https://tools.ietf.org/html/rfc2617).
|
||||||
|
*/
|
||||||
|
extern NSString* const GCDWebServerAuthenticationMethod_DigestAccess;
|
||||||
|
|
||||||
@class GCDWebServer;
|
@class GCDWebServer;
|
||||||
|
|
||||||
// These methods are always called on main thread
|
/**
|
||||||
|
* Delegate methods for GCDWebServer.
|
||||||
|
*
|
||||||
|
* @warning These methods are always called on the main thread in a serialized way.
|
||||||
|
*/
|
||||||
@protocol GCDWebServerDelegate <NSObject>
|
@protocol GCDWebServerDelegate <NSObject>
|
||||||
@optional
|
@optional
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This method is called after the server has successfully started.
|
||||||
|
*/
|
||||||
- (void)webServerDidStart:(GCDWebServer*)server;
|
- (void)webServerDidStart:(GCDWebServer*)server;
|
||||||
- (void)webServerDidConnect:(GCDWebServer*)server; // Called when first connection is opened
|
|
||||||
- (void)webServerDidDisconnect:(GCDWebServer*)server; // Called when last connection is closed
|
/**
|
||||||
|
* This method is called after the Bonjour registration for the server has
|
||||||
|
* successfully completed.
|
||||||
|
*
|
||||||
|
* Use the "bonjourServerURL" property to retrieve the Bonjour address of the
|
||||||
|
* server.
|
||||||
|
*/
|
||||||
|
- (void)webServerDidCompleteBonjourRegistration:(GCDWebServer*)server;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This method is called after the NAT port mapping for the server has been
|
||||||
|
* updated.
|
||||||
|
*
|
||||||
|
* Use the "publicServerURL" property to retrieve the public address of the
|
||||||
|
* server.
|
||||||
|
*/
|
||||||
|
- (void)webServerDidUpdateNATPortMapping:(GCDWebServer*)server;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This method is called when the first GCDWebServerConnection is opened by the
|
||||||
|
* server to serve a series of HTTP requests.
|
||||||
|
*
|
||||||
|
* A series of HTTP requests is considered ongoing as long as new HTTP requests
|
||||||
|
* keep coming (and new GCDWebServerConnection instances keep being opened),
|
||||||
|
* until before the last HTTP request has been responded to (and the
|
||||||
|
* corresponding last GCDWebServerConnection closed).
|
||||||
|
*/
|
||||||
|
- (void)webServerDidConnect:(GCDWebServer*)server;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This method is called when the last GCDWebServerConnection is closed after
|
||||||
|
* the server has served a series of HTTP requests.
|
||||||
|
*
|
||||||
|
* The GCDWebServerOption_ConnectedStateCoalescingInterval option can be used
|
||||||
|
* to have the server wait some extra delay before considering that the series
|
||||||
|
* of HTTP requests has ended (in case there some latency between consecutive
|
||||||
|
* requests). This effectively coalesces the calls to -webServerDidConnect:
|
||||||
|
* and -webServerDidDisconnect:.
|
||||||
|
*/
|
||||||
|
- (void)webServerDidDisconnect:(GCDWebServer*)server;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This method is called after the server has stopped.
|
||||||
|
*/
|
||||||
- (void)webServerDidStop:(GCDWebServer*)server;
|
- (void)webServerDidStop:(GCDWebServer*)server;
|
||||||
|
|
||||||
@end
|
@end
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The GCDWebServer class listens for incoming HTTP requests on a given port,
|
||||||
|
* then passes each one to a "handler" capable of generating an HTTP response
|
||||||
|
* for it, which is then sent back to the client.
|
||||||
|
*
|
||||||
|
* GCDWebServer instances can be created and used from any thread but it's
|
||||||
|
* recommended to have the main thread's runloop be running so internal callbacks
|
||||||
|
* can be handled e.g. for Bonjour registration.
|
||||||
|
*
|
||||||
|
* See the README.md file for more information about the architecture of GCDWebServer.
|
||||||
|
*/
|
||||||
@interface GCDWebServer : NSObject
|
@interface GCDWebServer : NSObject
|
||||||
@property(nonatomic, assign) id<GCDWebServerDelegate> delegate;
|
|
||||||
|
/**
|
||||||
|
* Sets the delegate for the server.
|
||||||
|
*/
|
||||||
|
@property(nonatomic, weak, nullable) id<GCDWebServerDelegate> delegate;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns YES if the server is currently running.
|
||||||
|
*/
|
||||||
@property(nonatomic, readonly, getter=isRunning) BOOL running;
|
@property(nonatomic, readonly, getter=isRunning) BOOL running;
|
||||||
@property(nonatomic, readonly) NSUInteger port; // Only non-zero if running
|
|
||||||
@property(nonatomic, readonly) NSString* bonjourName; // Only non-nil if Bonjour registration is active
|
/**
|
||||||
|
* Returns the port used by the server.
|
||||||
|
*
|
||||||
|
* @warning This property is only valid if the server is running.
|
||||||
|
*/
|
||||||
|
@property(nonatomic, readonly) NSUInteger port;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the Bonjour name used by the server.
|
||||||
|
*
|
||||||
|
* @warning This property is only valid if the server is running and Bonjour
|
||||||
|
* registration has successfully completed, which can take up to a few seconds.
|
||||||
|
*/
|
||||||
|
@property(nonatomic, readonly, nullable) NSString* bonjourName;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the Bonjour service type used by the server.
|
||||||
|
*
|
||||||
|
* @warning This property is only valid if the server is running and Bonjour
|
||||||
|
* registration has successfully completed, which can take up to a few seconds.
|
||||||
|
*/
|
||||||
|
@property(nonatomic, readonly, nullable) NSString* bonjourType;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This method is the designated initializer for the class.
|
||||||
|
*/
|
||||||
- (instancetype)init;
|
- (instancetype)init;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds to the server a handler that generates responses synchronously when handling incoming HTTP requests.
|
||||||
|
*
|
||||||
|
* Handlers are called in a LIFO queue, so if multiple handlers can potentially
|
||||||
|
* respond to a given request, the latest added one wins.
|
||||||
|
*
|
||||||
|
* @warning Addling handlers while the server is running is not allowed.
|
||||||
|
*/
|
||||||
- (void)addHandlerWithMatchBlock:(GCDWebServerMatchBlock)matchBlock processBlock:(GCDWebServerProcessBlock)processBlock;
|
- (void)addHandlerWithMatchBlock:(GCDWebServerMatchBlock)matchBlock processBlock:(GCDWebServerProcessBlock)processBlock;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds to the server a handler that generates responses asynchronously when handling incoming HTTP requests.
|
||||||
|
*
|
||||||
|
* Handlers are called in a LIFO queue, so if multiple handlers can potentially
|
||||||
|
* respond to a given request, the latest added one wins.
|
||||||
|
*
|
||||||
|
* @warning Addling handlers while the server is running is not allowed.
|
||||||
|
*/
|
||||||
|
- (void)addHandlerWithMatchBlock:(GCDWebServerMatchBlock)matchBlock asyncProcessBlock:(GCDWebServerAsyncProcessBlock)processBlock;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes all handlers previously added to the server.
|
||||||
|
*
|
||||||
|
* @warning Removing handlers while the server is running is not allowed.
|
||||||
|
*/
|
||||||
- (void)removeAllHandlers;
|
- (void)removeAllHandlers;
|
||||||
|
|
||||||
- (BOOL)start; // Default is port 8080 (OS X & iOS Simulator) or 80 (iOS) and computer / device name for Bonjour
|
/**
|
||||||
- (BOOL)startWithPort:(NSUInteger)port bonjourName:(NSString*)name; // Pass nil name to disable Bonjour or empty string to use computer name
|
* Starts the server with explicit options. This method is the designated way
|
||||||
- (BOOL)startWithOptions:(NSDictionary*)options;
|
* to start the server.
|
||||||
- (void)stop; // Does not abort any currently opened connections
|
*
|
||||||
|
* Returns NO if the server failed to start and sets "error" argument if not NULL.
|
||||||
|
*/
|
||||||
|
- (BOOL)startWithOptions:(nullable NSDictionary<NSString*, id>*)options error:(NSError** _Nullable)error;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stops the server and prevents it to accepts new HTTP requests.
|
||||||
|
*
|
||||||
|
* @warning Stopping the server does not abort GCDWebServerConnection instances
|
||||||
|
* currently handling already received HTTP requests. These connections will
|
||||||
|
* continue to execute normally until completion.
|
||||||
|
*/
|
||||||
|
- (void)stop;
|
||||||
|
|
||||||
@end
|
@end
|
||||||
|
|
||||||
@interface GCDWebServer (Extensions)
|
@interface GCDWebServer (Extensions)
|
||||||
@property(nonatomic, readonly) NSURL* serverURL; // Only non-nil if server is running
|
|
||||||
@property(nonatomic, readonly) NSURL* bonjourServerURL; // Only non-nil if server is running and Bonjour registration is active
|
/**
|
||||||
|
* Returns the server's URL.
|
||||||
|
*
|
||||||
|
* @warning This property is only valid if the server is running.
|
||||||
|
*/
|
||||||
|
@property(nonatomic, readonly, nullable) NSURL* serverURL;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the server's Bonjour URL.
|
||||||
|
*
|
||||||
|
* @warning This property is only valid if the server is running and Bonjour
|
||||||
|
* registration has successfully completed, which can take up to a few seconds.
|
||||||
|
* Also be aware this property will not automatically update if the Bonjour hostname
|
||||||
|
* has been dynamically changed after the server started running (this should be rare).
|
||||||
|
*/
|
||||||
|
@property(nonatomic, readonly, nullable) NSURL* bonjourServerURL;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the server's public URL.
|
||||||
|
*
|
||||||
|
* @warning This property is only valid if the server is running and NAT port
|
||||||
|
* mapping is active.
|
||||||
|
*/
|
||||||
|
@property(nonatomic, readonly, nullable) NSURL* publicServerURL;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Starts the server on port 8080 (OS X & iOS Simulator) or port 80 (iOS)
|
||||||
|
* using the default Bonjour name.
|
||||||
|
*
|
||||||
|
* Returns NO if the server failed to start.
|
||||||
|
*/
|
||||||
|
- (BOOL)start;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Starts the server on a given port and with a specific Bonjour name.
|
||||||
|
* Pass a nil Bonjour name to disable Bonjour entirely or an empty string to
|
||||||
|
* use the default name.
|
||||||
|
*
|
||||||
|
* Returns NO if the server failed to start.
|
||||||
|
*/
|
||||||
|
- (BOOL)startWithPort:(NSUInteger)port bonjourName:(nullable NSString*)name;
|
||||||
|
|
||||||
#if !TARGET_OS_IPHONE
|
#if !TARGET_OS_IPHONE
|
||||||
- (BOOL)runWithPort:(NSUInteger)port; // Starts then automatically stops on SIGINT i.e. Ctrl-C (use on main thread only)
|
|
||||||
|
/**
|
||||||
|
* Runs the server synchronously using -startWithPort:bonjourName: until a
|
||||||
|
* SIGINT signal is received i.e. Ctrl-C. This method is intended to be used
|
||||||
|
* by command line tools.
|
||||||
|
*
|
||||||
|
* Returns NO if the server failed to start.
|
||||||
|
*
|
||||||
|
* @warning This method must be used from the main thread only.
|
||||||
|
*/
|
||||||
|
- (BOOL)runWithPort:(NSUInteger)port bonjourName:(nullable NSString*)name;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Runs the server synchronously using -startWithOptions: until a SIGTERM or
|
||||||
|
* SIGINT signal is received i.e. Ctrl-C in Terminal. This method is intended to
|
||||||
|
* be used by command line tools.
|
||||||
|
*
|
||||||
|
* Returns NO if the server failed to start and sets "error" argument if not NULL.
|
||||||
|
*
|
||||||
|
* @warning This method must be used from the main thread only.
|
||||||
|
*/
|
||||||
|
- (BOOL)runWithOptions:(nullable NSDictionary<NSString*, id>*)options error:(NSError** _Nullable)error;
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
@end
|
@end
|
||||||
|
|
||||||
@interface GCDWebServer (Handlers)
|
@interface GCDWebServer (Handlers)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds a default handler to the server to handle all incoming HTTP requests
|
||||||
|
* with a given HTTP method and generate responses synchronously.
|
||||||
|
*/
|
||||||
- (void)addDefaultHandlerForMethod:(NSString*)method requestClass:(Class)aClass processBlock:(GCDWebServerProcessBlock)block;
|
- (void)addDefaultHandlerForMethod:(NSString*)method requestClass:(Class)aClass processBlock:(GCDWebServerProcessBlock)block;
|
||||||
- (void)addHandlerForMethod:(NSString*)method path:(NSString*)path requestClass:(Class)aClass processBlock:(GCDWebServerProcessBlock)block; // Path is case-insensitive
|
|
||||||
- (void)addHandlerForMethod:(NSString*)method pathRegex:(NSString*)regex requestClass:(Class)aClass processBlock:(GCDWebServerProcessBlock)block; // Regular expression is case-insensitive
|
/**
|
||||||
|
* Adds a default handler to the server to handle all incoming HTTP requests
|
||||||
|
* with a given HTTP method and generate responses asynchronously.
|
||||||
|
*/
|
||||||
|
- (void)addDefaultHandlerForMethod:(NSString*)method requestClass:(Class)aClass asyncProcessBlock:(GCDWebServerAsyncProcessBlock)block;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds a handler to the server to handle incoming HTTP requests with a given
|
||||||
|
* HTTP method and a specific case-insensitive path and generate responses
|
||||||
|
* synchronously.
|
||||||
|
*/
|
||||||
|
- (void)addHandlerForMethod:(NSString*)method path:(NSString*)path requestClass:(Class)aClass processBlock:(GCDWebServerProcessBlock)block;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds a handler to the server to handle incoming HTTP requests with a given
|
||||||
|
* HTTP method and a specific case-insensitive path and generate responses
|
||||||
|
* asynchronously.
|
||||||
|
*/
|
||||||
|
- (void)addHandlerForMethod:(NSString*)method path:(NSString*)path requestClass:(Class)aClass asyncProcessBlock:(GCDWebServerAsyncProcessBlock)block;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds a handler to the server to handle incoming HTTP requests with a given
|
||||||
|
* HTTP method and a path matching a case-insensitive regular expression and
|
||||||
|
* generate responses synchronously.
|
||||||
|
*/
|
||||||
|
- (void)addHandlerForMethod:(NSString*)method pathRegex:(NSString*)regex requestClass:(Class)aClass processBlock:(GCDWebServerProcessBlock)block;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds a handler to the server to handle incoming HTTP requests with a given
|
||||||
|
* HTTP method and a path matching a case-insensitive regular expression and
|
||||||
|
* generate responses asynchronously.
|
||||||
|
*/
|
||||||
|
- (void)addHandlerForMethod:(NSString*)method pathRegex:(NSString*)regex requestClass:(Class)aClass asyncProcessBlock:(GCDWebServerAsyncProcessBlock)block;
|
||||||
|
|
||||||
@end
|
@end
|
||||||
|
|
||||||
@interface GCDWebServer (GETHandlers)
|
@interface GCDWebServer (GETHandlers)
|
||||||
- (void)addGETHandlerForPath:(NSString*)path staticData:(NSData*)staticData contentType:(NSString*)contentType cacheAge:(NSUInteger)cacheAge; // Path is case-insensitive
|
|
||||||
- (void)addGETHandlerForPath:(NSString*)path filePath:(NSString*)filePath isAttachment:(BOOL)isAttachment cacheAge:(NSUInteger)cacheAge allowRangeRequests:(BOOL)allowRangeRequests; // Path is case-insensitive
|
/**
|
||||||
- (void)addGETHandlerForBasePath:(NSString*)basePath directoryPath:(NSString*)directoryPath indexFilename:(NSString*)indexFilename cacheAge:(NSUInteger)cacheAge allowRangeRequests:(BOOL)allowRangeRequests; // Base path is recursive and case-sensitive
|
* Adds a handler to the server to respond to incoming "GET" HTTP requests
|
||||||
|
* with a specific case-insensitive path with in-memory data.
|
||||||
|
*/
|
||||||
|
- (void)addGETHandlerForPath:(NSString*)path staticData:(NSData*)staticData contentType:(nullable NSString*)contentType cacheAge:(NSUInteger)cacheAge;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds a handler to the server to respond to incoming "GET" HTTP requests
|
||||||
|
* with a specific case-insensitive path with a file.
|
||||||
|
*/
|
||||||
|
- (void)addGETHandlerForPath:(NSString*)path filePath:(NSString*)filePath isAttachment:(BOOL)isAttachment cacheAge:(NSUInteger)cacheAge allowRangeRequests:(BOOL)allowRangeRequests;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds a handler to the server to respond to incoming "GET" HTTP requests
|
||||||
|
* with a case-insensitive path inside a base path with the corresponding file
|
||||||
|
* inside a local directory. If no local file matches the request path, a 401
|
||||||
|
* HTTP status code is returned to the client.
|
||||||
|
*
|
||||||
|
* The "indexFilename" argument allows to specify an "index" file name to use
|
||||||
|
* when the request path corresponds to a directory.
|
||||||
|
*/
|
||||||
|
- (void)addGETHandlerForBasePath:(NSString*)basePath directoryPath:(NSString*)directoryPath indexFilename:(nullable NSString*)indexFilename cacheAge:(NSUInteger)cacheAge allowRangeRequests:(BOOL)allowRangeRequests;
|
||||||
|
|
||||||
@end
|
@end
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GCDWebServer provides its own built-in logging facility which is used by
|
||||||
|
* default. It simply sends log messages to stderr assuming it is connected
|
||||||
|
* to a terminal type device.
|
||||||
|
*
|
||||||
|
* GCDWebServer is also compatible with a limited set of third-party logging
|
||||||
|
* facilities. If one of them is available at compile time, GCDWebServer will
|
||||||
|
* automatically use it in place of the built-in one.
|
||||||
|
*
|
||||||
|
* Currently supported third-party logging facilities are:
|
||||||
|
* - XLFacility (by the same author as GCDWebServer): https://github.com/swisspol/XLFacility
|
||||||
|
*
|
||||||
|
* For the built-in logging facility, the default logging level is INFO
|
||||||
|
* (or DEBUG if the preprocessor constant "DEBUG" evaluates to non-zero at
|
||||||
|
* compile time).
|
||||||
|
*
|
||||||
|
* It's possible to have GCDWebServer use a custom logging facility by defining
|
||||||
|
* the "__GCDWEBSERVER_LOGGING_HEADER__" preprocessor constant in Xcode build
|
||||||
|
* settings to the name of a custom header file (escaped like \"MyLogging.h\").
|
||||||
|
* This header file must define the following set of macros:
|
||||||
|
*
|
||||||
|
* GWS_LOG_DEBUG(...)
|
||||||
|
* GWS_LOG_VERBOSE(...)
|
||||||
|
* GWS_LOG_INFO(...)
|
||||||
|
* GWS_LOG_WARNING(...)
|
||||||
|
* GWS_LOG_ERROR(...)
|
||||||
|
*
|
||||||
|
* IMPORTANT: These macros must behave like NSLog(). Furthermore the GWS_LOG_DEBUG()
|
||||||
|
* macro should not do anything unless the preprocessor constant "DEBUG" evaluates
|
||||||
|
* to non-zero.
|
||||||
|
*
|
||||||
|
* The logging methods below send log messages to the same logging facility
|
||||||
|
* used by GCDWebServer. They can be used for consistency wherever you interact
|
||||||
|
* with GCDWebServer in your code (e.g. in the implementation of handlers).
|
||||||
|
*/
|
||||||
@interface GCDWebServer (Logging)
|
@interface GCDWebServer (Logging)
|
||||||
#ifndef __GCDWEBSERVER_LOGGING_HEADER__
|
|
||||||
+ (void)setLogLevel:(GCDWebServerLogLevel)level; // Default level is DEBUG or INFO if "NDEBUG" is defined when building (it can also be set at runtime with the "logLevel" environment variable)
|
/**
|
||||||
#endif
|
* Sets the log level of the logging facility below which log messages are discarded.
|
||||||
- (void)logVerbose:(NSString*)format, ... NS_FORMAT_FUNCTION(1,2);
|
*
|
||||||
- (void)logInfo:(NSString*)format, ... NS_FORMAT_FUNCTION(1,2);
|
* @warning The interpretation of the "level" argument depends on the logging
|
||||||
- (void)logWarning:(NSString*)format, ... NS_FORMAT_FUNCTION(1,2);
|
* facility used at compile time.
|
||||||
- (void)logError:(NSString*)format, ... NS_FORMAT_FUNCTION(1,2);
|
*
|
||||||
|
* If using the built-in logging facility, the log levels are as follow:
|
||||||
|
* DEBUG = 0
|
||||||
|
* VERBOSE = 1
|
||||||
|
* INFO = 2
|
||||||
|
* WARNING = 3
|
||||||
|
* ERROR = 4
|
||||||
|
*/
|
||||||
|
+ (void)setLogLevel:(int)level;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set a logger to be used instead of the built-in logger which logs to stderr.
|
||||||
|
*
|
||||||
|
* IMPORTANT: In order for this override to work, you should not be specifying
|
||||||
|
* a custom logger at compile time with "__GCDWEBSERVER_LOGGING_HEADER__".
|
||||||
|
*/
|
||||||
|
+ (void)setBuiltInLogger:(GCDWebServerBuiltInLoggerBlock)block;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Logs a message to the logging facility at the VERBOSE level.
|
||||||
|
*/
|
||||||
|
- (void)logVerbose:(NSString*)format, ... NS_FORMAT_FUNCTION(1, 2);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Logs a message to the logging facility at the INFO level.
|
||||||
|
*/
|
||||||
|
- (void)logInfo:(NSString*)format, ... NS_FORMAT_FUNCTION(1, 2);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Logs a message to the logging facility at the WARNING level.
|
||||||
|
*/
|
||||||
|
- (void)logWarning:(NSString*)format, ... NS_FORMAT_FUNCTION(1, 2);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Logs a message to the logging facility at the ERROR level.
|
||||||
|
*/
|
||||||
|
- (void)logError:(NSString*)format, ... NS_FORMAT_FUNCTION(1, 2);
|
||||||
|
|
||||||
@end
|
@end
|
||||||
|
|
||||||
#ifdef __GCDWEBSERVER_ENABLE_TESTING__
|
#ifdef __GCDWEBSERVER_ENABLE_TESTING__
|
||||||
|
|
||||||
@interface GCDWebServer (Testing)
|
@interface GCDWebServer (Testing)
|
||||||
@property(nonatomic, getter=isRecordingEnabled) BOOL recordingEnabled; // Creates files in the current directory containing the raw data for all requests and responses (directory most NOT contain prior recordings)
|
|
||||||
- (NSInteger)runTestsInDirectory:(NSString*)path withPort:(NSUInteger)port; // Returns number of failed tests or -1 if server failed to start
|
/**
|
||||||
|
* Activates recording of HTTP requests and responses which create files in the
|
||||||
|
* current directory containing the raw data for all requests and responses.
|
||||||
|
*
|
||||||
|
* @warning The current directory must not contain any prior recording files.
|
||||||
|
*/
|
||||||
|
@property(nonatomic, getter=isRecordingEnabled) BOOL recordingEnabled;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Runs tests by playing back pre-recorded HTTP requests in the given directory
|
||||||
|
* and comparing the generated responses with the pre-recorded ones.
|
||||||
|
*
|
||||||
|
* Returns the number of failed tests or -1 if server failed to start.
|
||||||
|
*/
|
||||||
|
- (NSInteger)runTestsWithOptions:(nullable NSDictionary<NSString*, id>*)options inDirectory:(NSString*)path;
|
||||||
|
|
||||||
@end
|
@end
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
NS_ASSUME_NONNULL_END
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
Copyright (c) 2012-2014, Pierre-Olivier Latour
|
Copyright (c) 2012-2019, Pierre-Olivier Latour
|
||||||
All rights reserved.
|
All rights reserved.
|
||||||
|
|
||||||
Redistribution and use in source and binary forms, with or without
|
Redistribution and use in source and binary forms, with or without
|
||||||
@@ -27,25 +27,157 @@
|
|||||||
|
|
||||||
#import "GCDWebServer.h"
|
#import "GCDWebServer.h"
|
||||||
|
|
||||||
|
NS_ASSUME_NONNULL_BEGIN
|
||||||
|
|
||||||
@class GCDWebServerHandler;
|
@class GCDWebServerHandler;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The GCDWebServerConnection class is instantiated by GCDWebServer to handle
|
||||||
|
* each new HTTP connection. Each instance stays alive until the connection is
|
||||||
|
* closed.
|
||||||
|
*
|
||||||
|
* You cannot use this class directly, but it is made public so you can
|
||||||
|
* subclass it to override some hooks. Use the GCDWebServerOption_ConnectionClass
|
||||||
|
* option for GCDWebServer to install your custom subclass.
|
||||||
|
*
|
||||||
|
* @warning The GCDWebServerConnection retains the GCDWebServer until the
|
||||||
|
* connection is closed.
|
||||||
|
*/
|
||||||
@interface GCDWebServerConnection : NSObject
|
@interface GCDWebServerConnection : NSObject
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the GCDWebServer that owns the connection.
|
||||||
|
*/
|
||||||
@property(nonatomic, readonly) GCDWebServer* server;
|
@property(nonatomic, readonly) GCDWebServer* server;
|
||||||
@property(nonatomic, readonly) NSData* localAddressData; // struct sockaddr
|
|
||||||
|
/**
|
||||||
|
* Returns YES if the connection is using IPv6.
|
||||||
|
*/
|
||||||
|
@property(nonatomic, readonly, getter=isUsingIPv6) BOOL usingIPv6;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the address of the local peer (i.e. server) of the connection
|
||||||
|
* as a raw "struct sockaddr".
|
||||||
|
*/
|
||||||
|
@property(nonatomic, readonly) NSData* localAddressData;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the address of the local peer (i.e. server) of the connection
|
||||||
|
* as a string.
|
||||||
|
*/
|
||||||
@property(nonatomic, readonly) NSString* localAddressString;
|
@property(nonatomic, readonly) NSString* localAddressString;
|
||||||
@property(nonatomic, readonly) NSData* remoteAddressData; // struct sockaddr
|
|
||||||
|
/**
|
||||||
|
* Returns the address of the remote peer (i.e. client) of the connection
|
||||||
|
* as a raw "struct sockaddr".
|
||||||
|
*/
|
||||||
|
@property(nonatomic, readonly) NSData* remoteAddressData;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the address of the remote peer (i.e. client) of the connection
|
||||||
|
* as a string.
|
||||||
|
*/
|
||||||
@property(nonatomic, readonly) NSString* remoteAddressString;
|
@property(nonatomic, readonly) NSString* remoteAddressString;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the total number of bytes received from the remote peer (i.e. client)
|
||||||
|
* so far.
|
||||||
|
*/
|
||||||
@property(nonatomic, readonly) NSUInteger totalBytesRead;
|
@property(nonatomic, readonly) NSUInteger totalBytesRead;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the total number of bytes sent to the remote peer (i.e. client) so far.
|
||||||
|
*/
|
||||||
@property(nonatomic, readonly) NSUInteger totalBytesWritten;
|
@property(nonatomic, readonly) NSUInteger totalBytesWritten;
|
||||||
|
|
||||||
@end
|
@end
|
||||||
|
|
||||||
// These methods can be called from any thread
|
/**
|
||||||
|
* Hooks to customize the behavior of GCDWebServer HTTP connections.
|
||||||
|
*
|
||||||
|
* @warning These methods can be called on any GCD thread.
|
||||||
|
* Be sure to also call "super" when overriding them.
|
||||||
|
*/
|
||||||
@interface GCDWebServerConnection (Subclassing)
|
@interface GCDWebServerConnection (Subclassing)
|
||||||
- (BOOL)open; // Return NO to reject connection e.g. after validating local or remote addresses
|
|
||||||
- (void)didReadBytes:(const void*)bytes length:(NSUInteger)length; // Called after data has been read from the connection
|
/**
|
||||||
- (void)didWriteBytes:(const void*)bytes length:(NSUInteger)length; // Called after data has been written to the connection
|
* This method is called when the connection is opened.
|
||||||
- (GCDWebServerResponse*)processRequest:(GCDWebServerRequest*)request withBlock:(GCDWebServerProcessBlock)block; // Only called if the request can be processed
|
*
|
||||||
- (GCDWebServerResponse*)replaceResponse:(GCDWebServerResponse*)response forRequest:(GCDWebServerRequest*)request; // Default implementation replaces any response matching the "ETag" or "Last-Modified-Date" header of the request by a barebone "Not-Modified" (304) one
|
* Return NO to reject the connection e.g. after validating the local
|
||||||
- (void)abortRequest:(GCDWebServerRequest*)request withStatusCode:(NSInteger)statusCode; // If request headers was malformed, "request" will be nil
|
* or remote address.
|
||||||
|
*/
|
||||||
|
- (BOOL)open;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This method is called whenever data has been received
|
||||||
|
* from the remote peer (i.e. client).
|
||||||
|
*
|
||||||
|
* @warning Do not attempt to modify this data.
|
||||||
|
*/
|
||||||
|
- (void)didReadBytes:(const void*)bytes length:(NSUInteger)length;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This method is called whenever data has been sent
|
||||||
|
* to the remote peer (i.e. client).
|
||||||
|
*
|
||||||
|
* @warning Do not attempt to modify this data.
|
||||||
|
*/
|
||||||
|
- (void)didWriteBytes:(const void*)bytes length:(NSUInteger)length;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This method is called after the HTTP headers have been received to
|
||||||
|
* allow replacing the request URL by another one.
|
||||||
|
*
|
||||||
|
* The default implementation returns the original URL.
|
||||||
|
*/
|
||||||
|
- (NSURL*)rewriteRequestURL:(NSURL*)url withMethod:(NSString*)method headers:(NSDictionary<NSString*, NSString*>*)headers;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Assuming a valid HTTP request was received, this method is called before
|
||||||
|
* the request is processed.
|
||||||
|
*
|
||||||
|
* Return a non-nil GCDWebServerResponse to bypass the request processing entirely.
|
||||||
|
*
|
||||||
|
* The default implementation checks for HTTP authentication if applicable
|
||||||
|
* and returns a barebone 401 status code response if authentication failed.
|
||||||
|
*/
|
||||||
|
- (nullable GCDWebServerResponse*)preflightRequest:(GCDWebServerRequest*)request;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Assuming a valid HTTP request was received and -preflightRequest: returned nil,
|
||||||
|
* this method is called to process the request by executing the handler's
|
||||||
|
* process block.
|
||||||
|
*/
|
||||||
|
- (void)processRequest:(GCDWebServerRequest*)request completion:(GCDWebServerCompletionBlock)completion;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Assuming a valid HTTP request was received and either -preflightRequest:
|
||||||
|
* or -processRequest:completion: returned a non-nil GCDWebServerResponse,
|
||||||
|
* this method is called to override the response.
|
||||||
|
*
|
||||||
|
* You can either modify the current response and return it, or return a
|
||||||
|
* completely new one.
|
||||||
|
*
|
||||||
|
* The default implementation replaces any response matching the "ETag" or
|
||||||
|
* "Last-Modified-Date" header of the request by a barebone "Not-Modified" (304)
|
||||||
|
* one.
|
||||||
|
*/
|
||||||
|
- (GCDWebServerResponse*)overrideResponse:(GCDWebServerResponse*)response forRequest:(GCDWebServerRequest*)request;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This method is called if any error happens while validing or processing
|
||||||
|
* the request or if no GCDWebServerResponse was generated during processing.
|
||||||
|
*
|
||||||
|
* @warning If the request was invalid (e.g. the HTTP headers were malformed),
|
||||||
|
* the "request" argument will be nil.
|
||||||
|
*/
|
||||||
|
- (void)abortRequest:(nullable GCDWebServerRequest*)request withStatusCode:(NSInteger)statusCode;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called when the connection is closed.
|
||||||
|
*/
|
||||||
- (void)close;
|
- (void)close;
|
||||||
|
|
||||||
@end
|
@end
|
||||||
|
|
||||||
|
NS_ASSUME_NONNULL_END
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
Copyright (c) 2012-2014, Pierre-Olivier Latour
|
Copyright (c) 2012-2019, Pierre-Olivier Latour
|
||||||
All rights reserved.
|
All rights reserved.
|
||||||
|
|
||||||
Redistribution and use in source and binary forms, with or without
|
Redistribution and use in source and binary forms, with or without
|
||||||
@@ -27,20 +27,88 @@
|
|||||||
|
|
||||||
#import <Foundation/Foundation.h>
|
#import <Foundation/Foundation.h>
|
||||||
|
|
||||||
|
NS_ASSUME_NONNULL_BEGIN
|
||||||
|
|
||||||
#ifdef __cplusplus
|
#ifdef __cplusplus
|
||||||
extern "C" {
|
extern "C" {
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
NSString* GCDWebServerGetMimeTypeForExtension(NSString* extension);
|
/**
|
||||||
NSString* GCDWebServerEscapeURLString(NSString* string);
|
* Converts a file extension to the corresponding MIME type.
|
||||||
NSString* GCDWebServerUnescapeURLString(NSString* string);
|
* If there is no match, "application/octet-stream" is returned.
|
||||||
NSDictionary* GCDWebServerParseURLEncodedForm(NSString* form);
|
*
|
||||||
NSString* GCDWebServerGetPrimaryIPv4Address(); // Returns IPv4 address of primary connected service on OS X or of WiFi interface on iOS if connected
|
* Overrides allow to customize the built-in mapping from extensions to MIME
|
||||||
|
* types. Keys of the dictionary must be lowercased file extensions without
|
||||||
|
* the period, and the values must be the corresponding MIME types.
|
||||||
|
*/
|
||||||
|
NSString* GCDWebServerGetMimeTypeForExtension(NSString* extension, NSDictionary<NSString*, NSString*>* _Nullable overrides);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add percent-escapes to a string so it can be used in a URL.
|
||||||
|
* The legal characters ":@/?&=+" are also escaped to ensure compatibility
|
||||||
|
* with URL encoded forms and URL queries.
|
||||||
|
*/
|
||||||
|
NSString* _Nullable GCDWebServerEscapeURLString(NSString* string);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unescapes a URL percent-encoded string.
|
||||||
|
*/
|
||||||
|
NSString* _Nullable GCDWebServerUnescapeURLString(NSString* string);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extracts the unescaped names and values from an
|
||||||
|
* "application/x-www-form-urlencoded" form.
|
||||||
|
* http://www.w3.org/TR/html401/interact/forms.html#h-17.13.4.1
|
||||||
|
*/
|
||||||
|
NSDictionary<NSString*, NSString*>* GCDWebServerParseURLEncodedForm(NSString* form);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* On OS X, returns the IPv4 or IPv6 address as a string of the primary
|
||||||
|
* connected service or nil if not available.
|
||||||
|
*
|
||||||
|
* On iOS, returns the IPv4 or IPv6 address as a string of the WiFi
|
||||||
|
* interface if connected or nil otherwise.
|
||||||
|
*/
|
||||||
|
NSString* _Nullable GCDWebServerGetPrimaryIPAddress(BOOL useIPv6);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts a date into a string using RFC822 formatting.
|
||||||
|
* https://tools.ietf.org/html/rfc822#section-5
|
||||||
|
* https://tools.ietf.org/html/rfc1123#section-5.2.14
|
||||||
|
*/
|
||||||
NSString* GCDWebServerFormatRFC822(NSDate* date);
|
NSString* GCDWebServerFormatRFC822(NSDate* date);
|
||||||
NSDate* GCDWebServerParseRFC822(NSString* string);
|
|
||||||
|
/**
|
||||||
|
* Converts a RFC822 formatted string into a date.
|
||||||
|
* https://tools.ietf.org/html/rfc822#section-5
|
||||||
|
* https://tools.ietf.org/html/rfc1123#section-5.2.14
|
||||||
|
*
|
||||||
|
* @warning Timezones other than GMT are not supported by this function.
|
||||||
|
*/
|
||||||
|
NSDate* _Nullable GCDWebServerParseRFC822(NSString* string);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts a date into a string using IOS 8601 formatting.
|
||||||
|
* http://tools.ietf.org/html/rfc3339#section-5.6
|
||||||
|
*/
|
||||||
NSString* GCDWebServerFormatISO8601(NSDate* date);
|
NSString* GCDWebServerFormatISO8601(NSDate* date);
|
||||||
NSDate* GCDWebServerParseISO8601(NSString* string);
|
|
||||||
|
/**
|
||||||
|
* Converts a ISO 8601 formatted string into a date.
|
||||||
|
* http://tools.ietf.org/html/rfc3339#section-5.6
|
||||||
|
*
|
||||||
|
* @warning Only "calendar" variant is supported at this time and timezones
|
||||||
|
* other than GMT are not supported either.
|
||||||
|
*/
|
||||||
|
NSDate* _Nullable GCDWebServerParseISO8601(NSString* string);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes "//", "/./" and "/../" components from path as well as any trailing slash.
|
||||||
|
*/
|
||||||
|
NSString* GCDWebServerNormalizePath(NSString* path);
|
||||||
|
|
||||||
#ifdef __cplusplus
|
#ifdef __cplusplus
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
NS_ASSUME_NONNULL_END
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
Copyright (c) 2012-2014, Pierre-Olivier Latour
|
Copyright (c) 2012-2019, Pierre-Olivier Latour
|
||||||
All rights reserved.
|
All rights reserved.
|
||||||
|
|
||||||
Redistribution and use in source and binary forms, with or without
|
Redistribution and use in source and binary forms, with or without
|
||||||
@@ -25,12 +25,17 @@
|
|||||||
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
#if !__has_feature(objc_arc)
|
||||||
|
#error GCDWebServer requires ARC
|
||||||
|
#endif
|
||||||
|
|
||||||
#import <TargetConditionals.h>
|
#import <TargetConditionals.h>
|
||||||
#if TARGET_OS_IPHONE
|
#if TARGET_OS_IPHONE
|
||||||
#import <MobileCoreServices/MobileCoreServices.h>
|
#import <CoreServices/CoreServices.h>
|
||||||
#else
|
#else
|
||||||
#import <SystemConfiguration/SystemConfiguration.h>
|
#import <SystemConfiguration/SystemConfiguration.h>
|
||||||
#endif
|
#endif
|
||||||
|
#import <CommonCrypto/CommonDigest.h>
|
||||||
|
|
||||||
#import <ifaddrs.h>
|
#import <ifaddrs.h>
|
||||||
#import <net/if.h>
|
#import <net/if.h>
|
||||||
@@ -42,27 +47,26 @@ static NSDateFormatter* _dateFormatterRFC822 = nil;
|
|||||||
static NSDateFormatter* _dateFormatterISO8601 = nil;
|
static NSDateFormatter* _dateFormatterISO8601 = nil;
|
||||||
static dispatch_queue_t _dateFormatterQueue = NULL;
|
static dispatch_queue_t _dateFormatterQueue = NULL;
|
||||||
|
|
||||||
// HTTP/1.1 server must use RFC822
|
// TODO: Handle RFC 850 and ANSI C's asctime() format
|
||||||
// TODO: Handle RFC 850 and ANSI C's asctime() format (http://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.3)
|
|
||||||
void GCDWebServerInitializeFunctions() {
|
void GCDWebServerInitializeFunctions() {
|
||||||
DCHECK([NSThread isMainThread]); // NSDateFormatter should be initialized on main thread
|
GWS_DCHECK([NSThread isMainThread]); // NSDateFormatter should be initialized on main thread
|
||||||
if (_dateFormatterRFC822 == nil) {
|
if (_dateFormatterRFC822 == nil) {
|
||||||
_dateFormatterRFC822 = [[NSDateFormatter alloc] init];
|
_dateFormatterRFC822 = [[NSDateFormatter alloc] init];
|
||||||
_dateFormatterRFC822.timeZone = [NSTimeZone timeZoneWithAbbreviation:@"GMT"];
|
_dateFormatterRFC822.timeZone = [NSTimeZone timeZoneWithAbbreviation:@"GMT"];
|
||||||
_dateFormatterRFC822.dateFormat = @"EEE',' dd MMM yyyy HH':'mm':'ss 'GMT'";
|
_dateFormatterRFC822.dateFormat = @"EEE',' dd MMM yyyy HH':'mm':'ss 'GMT'";
|
||||||
_dateFormatterRFC822.locale = ARC_AUTORELEASE([[NSLocale alloc] initWithLocaleIdentifier:@"en_US"]);
|
_dateFormatterRFC822.locale = [[NSLocale alloc] initWithLocaleIdentifier:@"en_US"];
|
||||||
DCHECK(_dateFormatterRFC822);
|
GWS_DCHECK(_dateFormatterRFC822);
|
||||||
}
|
}
|
||||||
if (_dateFormatterISO8601 == nil) {
|
if (_dateFormatterISO8601 == nil) {
|
||||||
_dateFormatterISO8601 = [[NSDateFormatter alloc] init];
|
_dateFormatterISO8601 = [[NSDateFormatter alloc] init];
|
||||||
_dateFormatterISO8601.timeZone = [NSTimeZone timeZoneWithAbbreviation:@"GMT"];
|
_dateFormatterISO8601.timeZone = [NSTimeZone timeZoneWithAbbreviation:@"GMT"];
|
||||||
_dateFormatterISO8601.dateFormat = @"yyyy-MM-dd'T'HH:mm:ss'+00:00'";
|
_dateFormatterISO8601.dateFormat = @"yyyy-MM-dd'T'HH:mm:ss'+00:00'";
|
||||||
_dateFormatterISO8601.locale = ARC_AUTORELEASE([[NSLocale alloc] initWithLocaleIdentifier:@"en_US"]);
|
_dateFormatterISO8601.locale = [[NSLocale alloc] initWithLocaleIdentifier:@"en_US"];
|
||||||
DCHECK(_dateFormatterISO8601);
|
GWS_DCHECK(_dateFormatterISO8601);
|
||||||
}
|
}
|
||||||
if (_dateFormatterQueue == NULL) {
|
if (_dateFormatterQueue == NULL) {
|
||||||
_dateFormatterQueue = dispatch_queue_create(NULL, DISPATCH_QUEUE_SERIAL);
|
_dateFormatterQueue = dispatch_queue_create(NULL, DISPATCH_QUEUE_SERIAL);
|
||||||
DCHECK(_dateFormatterQueue);
|
GWS_DCHECK(_dateFormatterQueue);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -79,26 +83,30 @@ NSString* GCDWebServerNormalizeHeaderValue(NSString* value) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
NSString* GCDWebServerTruncateHeaderValue(NSString* value) {
|
NSString* GCDWebServerTruncateHeaderValue(NSString* value) {
|
||||||
DCHECK([value isEqualToString:GCDWebServerNormalizeHeaderValue(value)]);
|
if (value) {
|
||||||
NSRange range = [value rangeOfString:@";"];
|
NSRange range = [value rangeOfString:@";"];
|
||||||
return range.location != NSNotFound ? [value substringToIndex:range.location] : value;
|
if (range.location != NSNotFound) {
|
||||||
|
return [value substringToIndex:range.location];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return value;
|
||||||
}
|
}
|
||||||
|
|
||||||
NSString* GCDWebServerExtractHeaderValueParameter(NSString* value, NSString* name) {
|
NSString* GCDWebServerExtractHeaderValueParameter(NSString* value, NSString* name) {
|
||||||
DCHECK([value isEqualToString:GCDWebServerNormalizeHeaderValue(value)]);
|
|
||||||
NSString* parameter = nil;
|
NSString* parameter = nil;
|
||||||
NSScanner* scanner = [[NSScanner alloc] initWithString:value];
|
if (value) {
|
||||||
[scanner setCaseSensitive:NO]; // Assume parameter names are case-insensitive
|
NSScanner* scanner = [[NSScanner alloc] initWithString:value];
|
||||||
NSString* string = [NSString stringWithFormat:@"%@=", name];
|
[scanner setCaseSensitive:NO]; // Assume parameter names are case-insensitive
|
||||||
if ([scanner scanUpToString:string intoString:NULL]) {
|
NSString* string = [NSString stringWithFormat:@"%@=", name];
|
||||||
[scanner scanString:string intoString:NULL];
|
if ([scanner scanUpToString:string intoString:NULL]) {
|
||||||
if ([scanner scanString:@"\"" intoString:NULL]) {
|
[scanner scanString:string intoString:NULL];
|
||||||
[scanner scanUpToString:@"\"" intoString:¶meter];
|
if ([scanner scanString:@"\"" intoString:NULL]) {
|
||||||
} else {
|
[scanner scanUpToString:@"\"" intoString:¶meter];
|
||||||
[scanner scanUpToCharactersFromSet:[NSCharacterSet whitespaceCharacterSet] intoString:¶meter];
|
} else {
|
||||||
|
[scanner scanUpToCharactersFromSet:[NSCharacterSet whitespaceCharacterSet] intoString:¶meter];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
ARC_RELEASE(scanner);
|
|
||||||
return parameter;
|
return parameter;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -152,27 +160,25 @@ NSString* GCDWebServerDescribeData(NSData* data, NSString* type) {
|
|||||||
NSString* charset = GCDWebServerExtractHeaderValueParameter(type, @"charset");
|
NSString* charset = GCDWebServerExtractHeaderValueParameter(type, @"charset");
|
||||||
NSString* string = [[NSString alloc] initWithData:data encoding:GCDWebServerStringEncodingFromCharset(charset)];
|
NSString* string = [[NSString alloc] initWithData:data encoding:GCDWebServerStringEncodingFromCharset(charset)];
|
||||||
if (string) {
|
if (string) {
|
||||||
return ARC_AUTORELEASE(string);
|
return string;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return [NSString stringWithFormat:@"<%lu bytes>", (unsigned long)data.length];
|
return [NSString stringWithFormat:@"<%lu bytes>", (unsigned long)data.length];
|
||||||
}
|
}
|
||||||
|
|
||||||
NSString* GCDWebServerGetMimeTypeForExtension(NSString* extension) {
|
NSString* GCDWebServerGetMimeTypeForExtension(NSString* extension, NSDictionary<NSString*, NSString*>* overrides) {
|
||||||
static NSDictionary* _overrides = nil;
|
NSDictionary* builtInOverrides = @{@"css" : @"text/css"};
|
||||||
if (_overrides == nil) {
|
|
||||||
_overrides = [[NSDictionary alloc] initWithObjectsAndKeys:
|
|
||||||
@"text/css", @"css",
|
|
||||||
nil];
|
|
||||||
}
|
|
||||||
NSString* mimeType = nil;
|
NSString* mimeType = nil;
|
||||||
extension = [extension lowercaseString];
|
extension = [extension lowercaseString];
|
||||||
if (extension.length) {
|
if (extension.length) {
|
||||||
mimeType = [_overrides objectForKey:extension];
|
mimeType = [overrides objectForKey:extension];
|
||||||
if (mimeType == nil) {
|
if (mimeType == nil) {
|
||||||
CFStringRef uti = UTTypeCreatePreferredIdentifierForTag(kUTTagClassFilenameExtension, (ARC_BRIDGE CFStringRef)extension, NULL);
|
mimeType = [builtInOverrides objectForKey:extension];
|
||||||
|
}
|
||||||
|
if (mimeType == nil) {
|
||||||
|
CFStringRef uti = UTTypeCreatePreferredIdentifierForTag(kUTTagClassFilenameExtension, (__bridge CFStringRef)extension, NULL);
|
||||||
if (uti) {
|
if (uti) {
|
||||||
mimeType = ARC_BRIDGE_RELEASE(UTTypeCopyPreferredTagWithClass(uti, kUTTagClassMIMEType));
|
mimeType = CFBridgingRelease(UTTypeCopyPreferredTagWithClass(uti, kUTTagClassMIMEType));
|
||||||
CFRelease(uti);
|
CFRelease(uti);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -181,15 +187,20 @@ NSString* GCDWebServerGetMimeTypeForExtension(NSString* extension) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
NSString* GCDWebServerEscapeURLString(NSString* string) {
|
NSString* GCDWebServerEscapeURLString(NSString* string) {
|
||||||
return ARC_BRIDGE_RELEASE(CFURLCreateStringByAddingPercentEscapes(kCFAllocatorDefault, (CFStringRef)string, NULL, CFSTR(":@/?&=+"), kCFStringEncodingUTF8));
|
#pragma clang diagnostic push
|
||||||
|
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
|
||||||
|
return CFBridgingRelease(CFURLCreateStringByAddingPercentEscapes(kCFAllocatorDefault, (CFStringRef)string, NULL, CFSTR(":@/?&=+"), kCFStringEncodingUTF8));
|
||||||
|
#pragma clang diagnostic pop
|
||||||
}
|
}
|
||||||
|
|
||||||
NSString* GCDWebServerUnescapeURLString(NSString* string) {
|
NSString* GCDWebServerUnescapeURLString(NSString* string) {
|
||||||
return ARC_BRIDGE_RELEASE(CFURLCreateStringByReplacingPercentEscapesUsingEncoding(kCFAllocatorDefault, (CFStringRef)string, CFSTR(""), kCFStringEncodingUTF8));
|
#pragma clang diagnostic push
|
||||||
|
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
|
||||||
|
return CFBridgingRelease(CFURLCreateStringByReplacingPercentEscapesUsingEncoding(kCFAllocatorDefault, (CFStringRef)string, CFSTR(""), kCFStringEncodingUTF8));
|
||||||
|
#pragma clang diagnostic pop
|
||||||
}
|
}
|
||||||
|
|
||||||
// http://www.w3.org/TR/html401/interact/forms.html#h-17.13.4.1
|
NSDictionary<NSString*, NSString*>* GCDWebServerParseURLEncodedForm(NSString* form) {
|
||||||
NSDictionary* GCDWebServerParseURLEncodedForm(NSString* form) {
|
|
||||||
NSMutableDictionary* parameters = [NSMutableDictionary dictionary];
|
NSMutableDictionary* parameters = [NSMutableDictionary dictionary];
|
||||||
NSScanner* scanner = [[NSScanner alloc] initWithString:form];
|
NSScanner* scanner = [[NSScanner alloc] initWithString:form];
|
||||||
[scanner setCharactersToBeSkipped:nil];
|
[scanner setCharactersToBeSkipped:nil];
|
||||||
@@ -199,42 +210,61 @@ NSDictionary* GCDWebServerParseURLEncodedForm(NSString* form) {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
[scanner setScanLocation:([scanner scanLocation] + 1)];
|
[scanner setScanLocation:([scanner scanLocation] + 1)];
|
||||||
|
|
||||||
NSString* value = nil;
|
NSString* value = nil;
|
||||||
if (![scanner scanUpToString:@"&" intoString:&value]) {
|
[scanner scanUpToString:@"&" intoString:&value];
|
||||||
break;
|
if (value == nil) {
|
||||||
|
value = @"";
|
||||||
}
|
}
|
||||||
|
|
||||||
key = [key stringByReplacingOccurrencesOfString:@"+" withString:@" "];
|
key = [key stringByReplacingOccurrencesOfString:@"+" withString:@" "];
|
||||||
|
NSString* unescapedKey = key ? GCDWebServerUnescapeURLString(key) : nil;
|
||||||
value = [value stringByReplacingOccurrencesOfString:@"+" withString:@" "];
|
value = [value stringByReplacingOccurrencesOfString:@"+" withString:@" "];
|
||||||
if (key && value) {
|
NSString* unescapedValue = value ? GCDWebServerUnescapeURLString(value) : nil;
|
||||||
[parameters setObject:GCDWebServerUnescapeURLString(value) forKey:GCDWebServerUnescapeURLString(key)];
|
if (unescapedKey && unescapedValue) {
|
||||||
|
[parameters setObject:unescapedValue forKey:unescapedKey];
|
||||||
} else {
|
} else {
|
||||||
DNOT_REACHED();
|
GWS_LOG_WARNING(@"Failed parsing URL encoded form for key \"%@\" and value \"%@\"", key, value);
|
||||||
|
GWS_DNOT_REACHED();
|
||||||
}
|
}
|
||||||
|
|
||||||
if ([scanner isAtEnd]) {
|
if ([scanner isAtEnd]) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
[scanner setScanLocation:([scanner scanLocation] + 1)];
|
[scanner setScanLocation:([scanner scanLocation] + 1)];
|
||||||
}
|
}
|
||||||
ARC_RELEASE(scanner);
|
|
||||||
return parameters;
|
return parameters;
|
||||||
}
|
}
|
||||||
|
|
||||||
NSString* GCDWebServerGetPrimaryIPv4Address() {
|
NSString* GCDWebServerStringFromSockAddr(const struct sockaddr* addr, BOOL includeService) {
|
||||||
|
char hostBuffer[NI_MAXHOST];
|
||||||
|
char serviceBuffer[NI_MAXSERV];
|
||||||
|
if (getnameinfo(addr, addr->sa_len, hostBuffer, sizeof(hostBuffer), serviceBuffer, sizeof(serviceBuffer), NI_NUMERICHOST | NI_NUMERICSERV | NI_NOFQDN) != 0) {
|
||||||
|
#if DEBUG
|
||||||
|
GWS_DNOT_REACHED();
|
||||||
|
#else
|
||||||
|
return @"";
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
return includeService ? [NSString stringWithFormat:@"%s:%s", hostBuffer, serviceBuffer] : (NSString*)[NSString stringWithUTF8String:hostBuffer];
|
||||||
|
}
|
||||||
|
|
||||||
|
NSString* GCDWebServerGetPrimaryIPAddress(BOOL useIPv6) {
|
||||||
NSString* address = nil;
|
NSString* address = nil;
|
||||||
#if TARGET_OS_IPHONE
|
#if TARGET_OS_IPHONE
|
||||||
#if !TARGET_IPHONE_SIMULATOR
|
#if !TARGET_IPHONE_SIMULATOR && !TARGET_OS_TV
|
||||||
const char* primaryInterface = "en0"; // WiFi interface on iOS
|
const char* primaryInterface = "en0"; // WiFi interface on iOS
|
||||||
#endif
|
#endif
|
||||||
#else
|
#else
|
||||||
const char* primaryInterface = NULL;
|
const char* primaryInterface = NULL;
|
||||||
SCDynamicStoreRef store = SCDynamicStoreCreate(kCFAllocatorDefault, CFSTR("GCDWebServer"), NULL, NULL);
|
SCDynamicStoreRef store = SCDynamicStoreCreate(kCFAllocatorDefault, CFSTR("GCDWebServer"), NULL, NULL);
|
||||||
if (store) {
|
if (store) {
|
||||||
CFPropertyListRef info = SCDynamicStoreCopyValue(store, CFSTR("State:/Network/Global/IPv4"));
|
CFPropertyListRef info = SCDynamicStoreCopyValue(store, CFSTR("State:/Network/Global/IPv4")); // There is no equivalent for IPv6 but the primary interface should be the same
|
||||||
if (info) {
|
if (info) {
|
||||||
primaryInterface = [[NSString stringWithString:[(ARC_BRIDGE NSDictionary*)info objectForKey:@"PrimaryInterface"]] UTF8String];
|
NSString* interface = [(__bridge NSDictionary*)info objectForKey:@"PrimaryInterface"];
|
||||||
|
if (interface) {
|
||||||
|
primaryInterface = [[NSString stringWithString:interface] UTF8String]; // Copy string to auto-release pool
|
||||||
|
}
|
||||||
CFRelease(info);
|
CFRelease(info);
|
||||||
}
|
}
|
||||||
CFRelease(store);
|
CFRelease(store);
|
||||||
@@ -246,19 +276,18 @@ NSString* GCDWebServerGetPrimaryIPv4Address() {
|
|||||||
struct ifaddrs* list;
|
struct ifaddrs* list;
|
||||||
if (getifaddrs(&list) >= 0) {
|
if (getifaddrs(&list) >= 0) {
|
||||||
for (struct ifaddrs* ifap = list; ifap; ifap = ifap->ifa_next) {
|
for (struct ifaddrs* ifap = list; ifap; ifap = ifap->ifa_next) {
|
||||||
#if TARGET_IPHONE_SIMULATOR
|
#if TARGET_IPHONE_SIMULATOR || TARGET_OS_TV
|
||||||
if (strcmp(ifap->ifa_name, "en0") && strcmp(ifap->ifa_name, "en1")) // Assume en0 is Ethernet and en1 is WiFi since there is no way to use SystemConfiguration framework in iOS Simulator
|
// Assume en0 is Ethernet and en1 is WiFi since there is no way to use SystemConfiguration framework in iOS Simulator
|
||||||
|
// Assumption holds for Apple TV running tvOS
|
||||||
|
if (strcmp(ifap->ifa_name, "en0") && strcmp(ifap->ifa_name, "en1"))
|
||||||
#else
|
#else
|
||||||
if (strcmp(ifap->ifa_name, primaryInterface))
|
if (strcmp(ifap->ifa_name, primaryInterface))
|
||||||
#endif
|
#endif
|
||||||
{
|
{
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if ((ifap->ifa_flags & IFF_UP) && (ifap->ifa_addr->sa_family == AF_INET)) {
|
if ((ifap->ifa_flags & IFF_UP) && ((!useIPv6 && (ifap->ifa_addr->sa_family == AF_INET)) || (useIPv6 && (ifap->ifa_addr->sa_family == AF_INET6)))) {
|
||||||
char buffer[NI_MAXHOST];
|
address = GCDWebServerStringFromSockAddr(ifap->ifa_addr, NO);
|
||||||
if (getnameinfo(ifap->ifa_addr, ifap->ifa_addr->sa_len, buffer, sizeof(buffer), NULL, 0, NI_NUMERICHOST | NI_NOFQDN) >= 0) {
|
|
||||||
address = [NSString stringWithUTF8String:buffer];
|
|
||||||
}
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -266,3 +295,40 @@ NSString* GCDWebServerGetPrimaryIPv4Address() {
|
|||||||
}
|
}
|
||||||
return address;
|
return address;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
NSString* GCDWebServerComputeMD5Digest(NSString* format, ...) {
|
||||||
|
va_list arguments;
|
||||||
|
va_start(arguments, format);
|
||||||
|
const char* string = [[[NSString alloc] initWithFormat:format arguments:arguments] UTF8String];
|
||||||
|
va_end(arguments);
|
||||||
|
unsigned char md5[CC_MD5_DIGEST_LENGTH];
|
||||||
|
#pragma clang diagnostic push
|
||||||
|
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
|
||||||
|
CC_MD5(string, (CC_LONG)strlen(string), md5);
|
||||||
|
#pragma clang diagnostic pop
|
||||||
|
char buffer[2 * CC_MD5_DIGEST_LENGTH + 1];
|
||||||
|
for (int i = 0; i < CC_MD5_DIGEST_LENGTH; ++i) {
|
||||||
|
unsigned char byte = md5[i];
|
||||||
|
unsigned char byteHi = (byte & 0xF0) >> 4;
|
||||||
|
buffer[2 * i + 0] = byteHi >= 10 ? 'a' + byteHi - 10 : '0' + byteHi;
|
||||||
|
unsigned char byteLo = byte & 0x0F;
|
||||||
|
buffer[2 * i + 1] = byteLo >= 10 ? 'a' + byteLo - 10 : '0' + byteLo;
|
||||||
|
}
|
||||||
|
buffer[2 * CC_MD5_DIGEST_LENGTH] = 0;
|
||||||
|
return (NSString*)[NSString stringWithUTF8String:buffer];
|
||||||
|
}
|
||||||
|
|
||||||
|
NSString* GCDWebServerNormalizePath(NSString* path) {
|
||||||
|
NSMutableArray* components = [[NSMutableArray alloc] init];
|
||||||
|
for (NSString* component in [path componentsSeparatedByString:@"/"]) {
|
||||||
|
if ([component isEqualToString:@".."]) {
|
||||||
|
[components removeLastObject];
|
||||||
|
} else if (component.length && ![component isEqualToString:@"."]) {
|
||||||
|
[components addObject:component];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (path.length && ([path characterAtIndex:0] == '/')) {
|
||||||
|
return [@"/" stringByAppendingString:[components componentsJoinedByString:@"/"]]; // Preserve initial slash
|
||||||
|
}
|
||||||
|
return [components componentsJoinedByString:@"/"];
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
Copyright (c) 2012-2014, Pierre-Olivier Latour
|
Copyright (c) 2012-2019, Pierre-Olivier Latour
|
||||||
All rights reserved.
|
All rights reserved.
|
||||||
|
|
||||||
Redistribution and use in source and binary forms, with or without
|
Redistribution and use in source and binary forms, with or without
|
||||||
@@ -30,12 +30,18 @@
|
|||||||
|
|
||||||
#import <Foundation/Foundation.h>
|
#import <Foundation/Foundation.h>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convenience constants for "informational" HTTP status codes.
|
||||||
|
*/
|
||||||
typedef NS_ENUM(NSInteger, GCDWebServerInformationalHTTPStatusCode) {
|
typedef NS_ENUM(NSInteger, GCDWebServerInformationalHTTPStatusCode) {
|
||||||
kGCDWebServerHTTPStatusCode_Continue = 100,
|
kGCDWebServerHTTPStatusCode_Continue = 100,
|
||||||
kGCDWebServerHTTPStatusCode_SwitchingProtocols = 101,
|
kGCDWebServerHTTPStatusCode_SwitchingProtocols = 101,
|
||||||
kGCDWebServerHTTPStatusCode_Processing = 102
|
kGCDWebServerHTTPStatusCode_Processing = 102
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convenience constants for "successful" HTTP status codes.
|
||||||
|
*/
|
||||||
typedef NS_ENUM(NSInteger, GCDWebServerSuccessfulHTTPStatusCode) {
|
typedef NS_ENUM(NSInteger, GCDWebServerSuccessfulHTTPStatusCode) {
|
||||||
kGCDWebServerHTTPStatusCode_OK = 200,
|
kGCDWebServerHTTPStatusCode_OK = 200,
|
||||||
kGCDWebServerHTTPStatusCode_Created = 201,
|
kGCDWebServerHTTPStatusCode_Created = 201,
|
||||||
@@ -48,6 +54,9 @@ typedef NS_ENUM(NSInteger, GCDWebServerSuccessfulHTTPStatusCode) {
|
|||||||
kGCDWebServerHTTPStatusCode_AlreadyReported = 208
|
kGCDWebServerHTTPStatusCode_AlreadyReported = 208
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convenience constants for "redirection" HTTP status codes.
|
||||||
|
*/
|
||||||
typedef NS_ENUM(NSInteger, GCDWebServerRedirectionHTTPStatusCode) {
|
typedef NS_ENUM(NSInteger, GCDWebServerRedirectionHTTPStatusCode) {
|
||||||
kGCDWebServerHTTPStatusCode_MultipleChoices = 300,
|
kGCDWebServerHTTPStatusCode_MultipleChoices = 300,
|
||||||
kGCDWebServerHTTPStatusCode_MovedPermanently = 301,
|
kGCDWebServerHTTPStatusCode_MovedPermanently = 301,
|
||||||
@@ -59,6 +68,9 @@ typedef NS_ENUM(NSInteger, GCDWebServerRedirectionHTTPStatusCode) {
|
|||||||
kGCDWebServerHTTPStatusCode_PermanentRedirect = 308
|
kGCDWebServerHTTPStatusCode_PermanentRedirect = 308
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convenience constants for "client error" HTTP status codes.
|
||||||
|
*/
|
||||||
typedef NS_ENUM(NSInteger, GCDWebServerClientErrorHTTPStatusCode) {
|
typedef NS_ENUM(NSInteger, GCDWebServerClientErrorHTTPStatusCode) {
|
||||||
kGCDWebServerHTTPStatusCode_BadRequest = 400,
|
kGCDWebServerHTTPStatusCode_BadRequest = 400,
|
||||||
kGCDWebServerHTTPStatusCode_Unauthorized = 401,
|
kGCDWebServerHTTPStatusCode_Unauthorized = 401,
|
||||||
@@ -87,6 +99,9 @@ typedef NS_ENUM(NSInteger, GCDWebServerClientErrorHTTPStatusCode) {
|
|||||||
kGCDWebServerHTTPStatusCode_RequestHeaderFieldsTooLarge = 431
|
kGCDWebServerHTTPStatusCode_RequestHeaderFieldsTooLarge = 431
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convenience constants for "server error" HTTP status codes.
|
||||||
|
*/
|
||||||
typedef NS_ENUM(NSInteger, GCDWebServerServerErrorHTTPStatusCode) {
|
typedef NS_ENUM(NSInteger, GCDWebServerServerErrorHTTPStatusCode) {
|
||||||
kGCDWebServerHTTPStatusCode_InternalServerError = 500,
|
kGCDWebServerHTTPStatusCode_InternalServerError = 500,
|
||||||
kGCDWebServerHTTPStatusCode_NotImplemented = 501,
|
kGCDWebServerHTTPStatusCode_NotImplemented = 501,
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
Copyright (c) 2012-2014, Pierre-Olivier Latour
|
Copyright (c) 2012-2019, Pierre-Olivier Latour
|
||||||
All rights reserved.
|
All rights reserved.
|
||||||
|
|
||||||
Redistribution and use in source and binary forms, with or without
|
Redistribution and use in source and binary forms, with or without
|
||||||
@@ -25,33 +25,12 @@
|
|||||||
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
#import <TargetConditionals.h>
|
#import <os/object.h>
|
||||||
#import <AvailabilityMacros.h>
|
#import <sys/socket.h>
|
||||||
|
|
||||||
#if __has_feature(objc_arc)
|
/**
|
||||||
#define ARC_BRIDGE __bridge
|
* All GCDWebServer headers.
|
||||||
#define ARC_BRIDGE_RELEASE(__OBJECT__) CFBridgingRelease(__OBJECT__)
|
*/
|
||||||
#define ARC_RETAIN(__OBJECT__) __OBJECT__
|
|
||||||
#define ARC_RELEASE(__OBJECT__)
|
|
||||||
#define ARC_AUTORELEASE(__OBJECT__) __OBJECT__
|
|
||||||
#define ARC_DEALLOC(__OBJECT__)
|
|
||||||
#if (TARGET_OS_IPHONE && (__IPHONE_OS_VERSION_MIN_REQUIRED >= __IPHONE_6_0)) || (!TARGET_OS_IPHONE && (__MAC_OS_X_VERSION_MIN_REQUIRED >= __MAC_10_8))
|
|
||||||
#define ARC_DISPATCH_RETAIN(__OBJECT__)
|
|
||||||
#define ARC_DISPATCH_RELEASE(__OBJECT__)
|
|
||||||
#else
|
|
||||||
#define ARC_DISPATCH_RETAIN(__OBJECT__) dispatch_retain(__OBJECT__)
|
|
||||||
#define ARC_DISPATCH_RELEASE(__OBJECT__) dispatch_release(__OBJECT__)
|
|
||||||
#endif
|
|
||||||
#else
|
|
||||||
#define ARC_BRIDGE
|
|
||||||
#define ARC_BRIDGE_RELEASE(__OBJECT__) [(id)__OBJECT__ autorelease]
|
|
||||||
#define ARC_RETAIN(__OBJECT__) [__OBJECT__ retain]
|
|
||||||
#define ARC_RELEASE(__OBJECT__) [__OBJECT__ release]
|
|
||||||
#define ARC_AUTORELEASE(__OBJECT__) [__OBJECT__ autorelease]
|
|
||||||
#define ARC_DEALLOC(__OBJECT__) [__OBJECT__ dealloc]
|
|
||||||
#define ARC_DISPATCH_RETAIN(__OBJECT__) dispatch_retain(__OBJECT__)
|
|
||||||
#define ARC_DISPATCH_RELEASE(__OBJECT__) dispatch_release(__OBJECT__)
|
|
||||||
#endif
|
|
||||||
|
|
||||||
#import "GCDWebServerHTTPStatusCodes.h"
|
#import "GCDWebServerHTTPStatusCodes.h"
|
||||||
#import "GCDWebServerFunctions.h"
|
#import "GCDWebServerFunctions.h"
|
||||||
@@ -67,92 +46,179 @@
|
|||||||
#import "GCDWebServerDataResponse.h"
|
#import "GCDWebServerDataResponse.h"
|
||||||
#import "GCDWebServerErrorResponse.h"
|
#import "GCDWebServerErrorResponse.h"
|
||||||
#import "GCDWebServerFileResponse.h"
|
#import "GCDWebServerFileResponse.h"
|
||||||
#import "GCDWebServerStreamingResponse.h"
|
#import "GCDWebServerStreamedResponse.h"
|
||||||
|
|
||||||
#ifdef __GCDWEBSERVER_LOGGING_HEADER__
|
/**
|
||||||
|
* Check if a custom logging facility should be used instead.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#if defined(__GCDWEBSERVER_LOGGING_HEADER__)
|
||||||
|
|
||||||
|
#define __GCDWEBSERVER_LOGGING_FACILITY_CUSTOM__
|
||||||
|
|
||||||
// Define __GCDWEBSERVER_LOGGING_HEADER__ as a preprocessor constant to redirect GCDWebServer logging to your own system
|
|
||||||
#import __GCDWEBSERVER_LOGGING_HEADER__
|
#import __GCDWEBSERVER_LOGGING_HEADER__
|
||||||
|
|
||||||
#else
|
/**
|
||||||
|
* Automatically detect if XLFacility is available and if so use it as a
|
||||||
|
* logging facility.
|
||||||
|
*/
|
||||||
|
|
||||||
extern GCDWebServerLogLevel GCDLogLevel;
|
#elif defined(__has_include) && __has_include("XLFacilityMacros.h")
|
||||||
extern void GCDLogMessage(GCDWebServerLogLevel level, NSString* format, ...) NS_FORMAT_FUNCTION(2, 3);
|
|
||||||
|
|
||||||
#define LOG_VERBOSE(...) do { if (GCDLogLevel <= kGCDWebServerLogLevel_Verbose) GCDLogMessage(kGCDWebServerLogLevel_Verbose, __VA_ARGS__); } while (0)
|
#define __GCDWEBSERVER_LOGGING_FACILITY_XLFACILITY__
|
||||||
#define LOG_INFO(...) do { if (GCDLogLevel <= kGCDWebServerLogLevel_Info) GCDLogMessage(kGCDWebServerLogLevel_Info, __VA_ARGS__); } while (0)
|
|
||||||
#define LOG_WARNING(...) do { if (GCDLogLevel <= kGCDWebServerLogLevel_Warning) GCDLogMessage(kGCDWebServerLogLevel_Warning, __VA_ARGS__); } while (0)
|
|
||||||
#define LOG_ERROR(...) do { if (GCDLogLevel <= kGCDWebServerLogLevel_Error) GCDLogMessage(kGCDWebServerLogLevel_Error, __VA_ARGS__); } while (0)
|
|
||||||
#define LOG_EXCEPTION(__EXCEPTION__) do { if (GCDLogLevel <= kGCDWebServerLogLevel_Exception) GCDLogMessage(kGCDWebServerLogLevel_Exception, @"%@", __EXCEPTION__); } while (0)
|
|
||||||
|
|
||||||
#ifdef NDEBUG
|
#undef XLOG_TAG
|
||||||
|
#define XLOG_TAG @"gcdwebserver.internal"
|
||||||
|
|
||||||
#define DCHECK(__CONDITION__)
|
#import "XLFacilityMacros.h"
|
||||||
#define DNOT_REACHED()
|
|
||||||
#define LOG_DEBUG(...)
|
#define GWS_LOG_DEBUG(...) XLOG_DEBUG(__VA_ARGS__)
|
||||||
|
#define GWS_LOG_VERBOSE(...) XLOG_VERBOSE(__VA_ARGS__)
|
||||||
|
#define GWS_LOG_INFO(...) XLOG_INFO(__VA_ARGS__)
|
||||||
|
#define GWS_LOG_WARNING(...) XLOG_WARNING(__VA_ARGS__)
|
||||||
|
#define GWS_LOG_ERROR(...) XLOG_ERROR(__VA_ARGS__)
|
||||||
|
|
||||||
|
#define GWS_DCHECK(__CONDITION__) XLOG_DEBUG_CHECK(__CONDITION__)
|
||||||
|
#define GWS_DNOT_REACHED() XLOG_DEBUG_UNREACHABLE()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If all of the above fail, then use GCDWebServer built-in
|
||||||
|
* logging facility.
|
||||||
|
*/
|
||||||
|
|
||||||
#else
|
#else
|
||||||
|
|
||||||
#define DCHECK(__CONDITION__) \
|
#define __GCDWEBSERVER_LOGGING_FACILITY_BUILTIN__
|
||||||
do { \
|
|
||||||
if (!(__CONDITION__)) { \
|
typedef NS_ENUM(int, GCDWebServerLoggingLevel) {
|
||||||
abort(); \
|
kGCDWebServerLoggingLevel_Debug = 0,
|
||||||
} \
|
kGCDWebServerLoggingLevel_Verbose,
|
||||||
|
kGCDWebServerLoggingLevel_Info,
|
||||||
|
kGCDWebServerLoggingLevel_Warning,
|
||||||
|
kGCDWebServerLoggingLevel_Error
|
||||||
|
};
|
||||||
|
|
||||||
|
extern GCDWebServerLoggingLevel GCDWebServerLogLevel;
|
||||||
|
extern void GCDWebServerLogMessage(GCDWebServerLoggingLevel level, NSString* _Nonnull format, ...) NS_FORMAT_FUNCTION(2, 3);
|
||||||
|
|
||||||
|
#if DEBUG
|
||||||
|
#define GWS_LOG_DEBUG(...) \
|
||||||
|
do { \
|
||||||
|
if (GCDWebServerLogLevel <= kGCDWebServerLoggingLevel_Debug) GCDWebServerLogMessage(kGCDWebServerLoggingLevel_Debug, __VA_ARGS__); \
|
||||||
} while (0)
|
} while (0)
|
||||||
#define DNOT_REACHED() abort()
|
#else
|
||||||
#define LOG_DEBUG(...) do { if (GCDLogLevel <= kGCDWebServerLogLevel_Debug) GCDLogMessage(kGCDWebServerLogLevel_Debug, __VA_ARGS__); } while (0)
|
#define GWS_LOG_DEBUG(...)
|
||||||
|
#endif
|
||||||
|
#define GWS_LOG_VERBOSE(...) \
|
||||||
|
do { \
|
||||||
|
if (GCDWebServerLogLevel <= kGCDWebServerLoggingLevel_Verbose) GCDWebServerLogMessage(kGCDWebServerLoggingLevel_Verbose, __VA_ARGS__); \
|
||||||
|
} while (0)
|
||||||
|
#define GWS_LOG_INFO(...) \
|
||||||
|
do { \
|
||||||
|
if (GCDWebServerLogLevel <= kGCDWebServerLoggingLevel_Info) GCDWebServerLogMessage(kGCDWebServerLoggingLevel_Info, __VA_ARGS__); \
|
||||||
|
} while (0)
|
||||||
|
#define GWS_LOG_WARNING(...) \
|
||||||
|
do { \
|
||||||
|
if (GCDWebServerLogLevel <= kGCDWebServerLoggingLevel_Warning) GCDWebServerLogMessage(kGCDWebServerLoggingLevel_Warning, __VA_ARGS__); \
|
||||||
|
} while (0)
|
||||||
|
#define GWS_LOG_ERROR(...) \
|
||||||
|
do { \
|
||||||
|
if (GCDWebServerLogLevel <= kGCDWebServerLoggingLevel_Error) GCDWebServerLogMessage(kGCDWebServerLoggingLevel_Error, __VA_ARGS__); \
|
||||||
|
} while (0)
|
||||||
|
|
||||||
|
#endif
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Consistency check macros used when building Debug only.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#if !defined(GWS_DCHECK) || !defined(GWS_DNOT_REACHED)
|
||||||
|
|
||||||
|
#if DEBUG
|
||||||
|
|
||||||
|
#define GWS_DCHECK(__CONDITION__) \
|
||||||
|
do { \
|
||||||
|
if (!(__CONDITION__)) { \
|
||||||
|
abort(); \
|
||||||
|
} \
|
||||||
|
} while (0)
|
||||||
|
#define GWS_DNOT_REACHED() abort()
|
||||||
|
|
||||||
|
#else
|
||||||
|
|
||||||
|
#define GWS_DCHECK(__CONDITION__)
|
||||||
|
#define GWS_DNOT_REACHED()
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
NS_ASSUME_NONNULL_BEGIN
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GCDWebServer internal constants and APIs.
|
||||||
|
*/
|
||||||
|
|
||||||
#define kGCDWebServerDefaultMimeType @"application/octet-stream"
|
#define kGCDWebServerDefaultMimeType @"application/octet-stream"
|
||||||
#define kGCDWebServerGCDQueue dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)
|
|
||||||
#define kGCDWebServerErrorDomain @"GCDWebServerErrorDomain"
|
#define kGCDWebServerErrorDomain @"GCDWebServerErrorDomain"
|
||||||
|
|
||||||
static inline BOOL GCDWebServerIsValidByteRange(NSRange range) {
|
static inline BOOL GCDWebServerIsValidByteRange(NSRange range) {
|
||||||
return ((range.location != NSNotFound) || (range.length > 0));
|
return ((range.location != NSUIntegerMax) || (range.length > 0));
|
||||||
}
|
}
|
||||||
|
|
||||||
extern void GCDWebServerInitializeFunctions();
|
static inline NSError* GCDWebServerMakePosixError(int code) {
|
||||||
extern NSString* GCDWebServerNormalizeHeaderValue(NSString* value);
|
return [NSError errorWithDomain:NSPOSIXErrorDomain code:code userInfo:@{NSLocalizedDescriptionKey : (NSString*)[NSString stringWithUTF8String:strerror(code)]}];
|
||||||
extern NSString* GCDWebServerTruncateHeaderValue(NSString* value);
|
}
|
||||||
extern NSString* GCDWebServerExtractHeaderValueParameter(NSString* header, NSString* attribute);
|
|
||||||
|
extern void GCDWebServerInitializeFunctions(void);
|
||||||
|
extern NSString* _Nullable GCDWebServerNormalizeHeaderValue(NSString* _Nullable value);
|
||||||
|
extern NSString* _Nullable GCDWebServerTruncateHeaderValue(NSString* _Nullable value);
|
||||||
|
extern NSString* _Nullable GCDWebServerExtractHeaderValueParameter(NSString* _Nullable value, NSString* attribute);
|
||||||
extern NSStringEncoding GCDWebServerStringEncodingFromCharset(NSString* charset);
|
extern NSStringEncoding GCDWebServerStringEncodingFromCharset(NSString* charset);
|
||||||
extern BOOL GCDWebServerIsTextContentType(NSString* type);
|
extern BOOL GCDWebServerIsTextContentType(NSString* type);
|
||||||
extern NSString* GCDWebServerDescribeData(NSData* data, NSString* contentType);
|
extern NSString* GCDWebServerDescribeData(NSData* data, NSString* contentType);
|
||||||
|
extern NSString* GCDWebServerComputeMD5Digest(NSString* format, ...) NS_FORMAT_FUNCTION(1, 2);
|
||||||
|
extern NSString* GCDWebServerStringFromSockAddr(const struct sockaddr* addr, BOOL includeService);
|
||||||
|
|
||||||
@interface GCDWebServerConnection ()
|
@interface GCDWebServerConnection ()
|
||||||
- (id)initWithServer:(GCDWebServer*)server localAddress:(NSData*)localAddress remoteAddress:(NSData*)remoteAddress socket:(CFSocketNativeHandle)socket;
|
- (instancetype)initWithServer:(GCDWebServer*)server localAddress:(NSData*)localAddress remoteAddress:(NSData*)remoteAddress socket:(CFSocketNativeHandle)socket;
|
||||||
@end
|
@end
|
||||||
|
|
||||||
@interface GCDWebServer ()
|
@interface GCDWebServer ()
|
||||||
@property(nonatomic, readonly) NSArray* handlers;
|
@property(nonatomic, readonly) NSMutableArray<GCDWebServerHandler*>* handlers;
|
||||||
@property(nonatomic, readonly) NSString* serverName;
|
@property(nonatomic, readonly, nullable) NSString* serverName;
|
||||||
|
@property(nonatomic, readonly, nullable) NSString* authenticationRealm;
|
||||||
|
@property(nonatomic, readonly, nullable) NSMutableDictionary<NSString*, NSString*>* authenticationBasicAccounts;
|
||||||
|
@property(nonatomic, readonly, nullable) NSMutableDictionary<NSString*, NSString*>* authenticationDigestAccounts;
|
||||||
@property(nonatomic, readonly) BOOL shouldAutomaticallyMapHEADToGET;
|
@property(nonatomic, readonly) BOOL shouldAutomaticallyMapHEADToGET;
|
||||||
|
@property(nonatomic, readonly) dispatch_queue_priority_t dispatchQueuePriority;
|
||||||
- (void)willStartConnection:(GCDWebServerConnection*)connection;
|
- (void)willStartConnection:(GCDWebServerConnection*)connection;
|
||||||
- (void)didEndConnection:(GCDWebServerConnection*)connection;
|
- (void)didEndConnection:(GCDWebServerConnection*)connection;
|
||||||
@end
|
@end
|
||||||
|
|
||||||
@interface GCDWebServerHandler : NSObject
|
@interface GCDWebServerHandler : NSObject
|
||||||
@property(nonatomic, readonly) GCDWebServerMatchBlock matchBlock;
|
@property(nonatomic, readonly) GCDWebServerMatchBlock matchBlock;
|
||||||
@property(nonatomic, readonly) GCDWebServerProcessBlock processBlock;
|
@property(nonatomic, readonly) GCDWebServerAsyncProcessBlock asyncProcessBlock;
|
||||||
- (id)initWithMatchBlock:(GCDWebServerMatchBlock)matchBlock processBlock:(GCDWebServerProcessBlock)processBlock;
|
|
||||||
@end
|
@end
|
||||||
|
|
||||||
@interface GCDWebServerRequest ()
|
@interface GCDWebServerRequest ()
|
||||||
@property(nonatomic, readonly) BOOL usesChunkedTransferEncoding;
|
@property(nonatomic, readonly) BOOL usesChunkedTransferEncoding;
|
||||||
|
@property(nonatomic) NSData* localAddressData;
|
||||||
|
@property(nonatomic) NSData* remoteAddressData;
|
||||||
- (void)prepareForWriting;
|
- (void)prepareForWriting;
|
||||||
- (BOOL)performOpen:(NSError**)error;
|
- (BOOL)performOpen:(NSError**)error;
|
||||||
- (BOOL)performWriteData:(NSData*)data error:(NSError**)error;
|
- (BOOL)performWriteData:(NSData*)data error:(NSError**)error;
|
||||||
- (BOOL)performClose:(NSError**)error;
|
- (BOOL)performClose:(NSError**)error;
|
||||||
|
- (void)setAttribute:(nullable id)attribute forKey:(NSString*)key;
|
||||||
@end
|
@end
|
||||||
|
|
||||||
@interface GCDWebServerResponse ()
|
@interface GCDWebServerResponse ()
|
||||||
@property(nonatomic, readonly) NSDictionary* additionalHeaders;
|
@property(nonatomic, readonly) NSDictionary<NSString*, NSString*>* additionalHeaders;
|
||||||
@property(nonatomic, readonly) BOOL usesChunkedTransferEncoding;
|
@property(nonatomic, readonly) BOOL usesChunkedTransferEncoding;
|
||||||
- (void)prepareForReading;
|
- (void)prepareForReading;
|
||||||
- (BOOL)performOpen:(NSError**)error;
|
- (BOOL)performOpen:(NSError**)error;
|
||||||
- (NSData*)performReadData:(NSError**)error;
|
- (void)performReadDataWithCompletion:(GCDWebServerBodyReaderCompletionBlock)block;
|
||||||
- (void)performClose;
|
- (void)performClose;
|
||||||
@end
|
@end
|
||||||
|
|
||||||
|
NS_ASSUME_NONNULL_END
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
Copyright (c) 2012-2014, Pierre-Olivier Latour
|
Copyright (c) 2012-2019, Pierre-Olivier Latour
|
||||||
All rights reserved.
|
All rights reserved.
|
||||||
|
|
||||||
Redistribution and use in source and binary forms, with or without
|
Redistribution and use in source and binary forms, with or without
|
||||||
@@ -27,25 +27,184 @@
|
|||||||
|
|
||||||
#import <Foundation/Foundation.h>
|
#import <Foundation/Foundation.h>
|
||||||
|
|
||||||
|
NS_ASSUME_NONNULL_BEGIN
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Attribute key to retrieve an NSArray containing NSStrings from a GCDWebServerRequest
|
||||||
|
* with the contents of any regular expression captures done on the request path.
|
||||||
|
*
|
||||||
|
* @warning This attribute will only be set on the request if adding a handler using
|
||||||
|
* -addHandlerForMethod:pathRegex:requestClass:processBlock:.
|
||||||
|
*/
|
||||||
|
extern NSString* const GCDWebServerRequestAttribute_RegexCaptures;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This protocol is used by the GCDWebServerConnection to communicate with
|
||||||
|
* the GCDWebServerRequest and write the received HTTP body data.
|
||||||
|
*
|
||||||
|
* Note that multiple GCDWebServerBodyWriter objects can be chained together
|
||||||
|
* internally e.g. to automatically decode gzip encoded content before
|
||||||
|
* passing it on to the GCDWebServerRequest.
|
||||||
|
*
|
||||||
|
* @warning These methods can be called on any GCD thread.
|
||||||
|
*/
|
||||||
@protocol GCDWebServerBodyWriter <NSObject>
|
@protocol GCDWebServerBodyWriter <NSObject>
|
||||||
- (BOOL)open:(NSError**)error; // Return NO on error ("error" is guaranteed to be non-NULL)
|
|
||||||
- (BOOL)writeData:(NSData*)data error:(NSError**)error; // Return NO on error ("error" is guaranteed to be non-NULL)
|
/**
|
||||||
- (BOOL)close:(NSError**)error; // Return NO on error ("error" is guaranteed to be non-NULL)
|
* This method is called before any body data is received.
|
||||||
|
*
|
||||||
|
* It should return YES on success or NO on failure and set the "error" argument
|
||||||
|
* which is guaranteed to be non-NULL.
|
||||||
|
*/
|
||||||
|
- (BOOL)open:(NSError**)error;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This method is called whenever body data has been received.
|
||||||
|
*
|
||||||
|
* It should return YES on success or NO on failure and set the "error" argument
|
||||||
|
* which is guaranteed to be non-NULL.
|
||||||
|
*/
|
||||||
|
- (BOOL)writeData:(NSData*)data error:(NSError**)error;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This method is called after all body data has been received.
|
||||||
|
*
|
||||||
|
* It should return YES on success or NO on failure and set the "error" argument
|
||||||
|
* which is guaranteed to be non-NULL.
|
||||||
|
*/
|
||||||
|
- (BOOL)close:(NSError**)error;
|
||||||
|
|
||||||
@end
|
@end
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The GCDWebServerRequest class is instantiated by the GCDWebServerConnection
|
||||||
|
* after the HTTP headers have been received. Each instance wraps a single HTTP
|
||||||
|
* request. If a body is present, the methods from the GCDWebServerBodyWriter
|
||||||
|
* protocol will be called by the GCDWebServerConnection to receive it.
|
||||||
|
*
|
||||||
|
* The default implementation of the GCDWebServerBodyWriter protocol on the class
|
||||||
|
* simply ignores the body data.
|
||||||
|
*
|
||||||
|
* @warning GCDWebServerRequest instances can be created and used on any GCD thread.
|
||||||
|
*/
|
||||||
@interface GCDWebServerRequest : NSObject <GCDWebServerBodyWriter>
|
@interface GCDWebServerRequest : NSObject <GCDWebServerBodyWriter>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the HTTP method for the request.
|
||||||
|
*/
|
||||||
@property(nonatomic, readonly) NSString* method;
|
@property(nonatomic, readonly) NSString* method;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the URL for the request.
|
||||||
|
*/
|
||||||
@property(nonatomic, readonly) NSURL* URL;
|
@property(nonatomic, readonly) NSURL* URL;
|
||||||
@property(nonatomic, readonly) NSDictionary* headers;
|
|
||||||
|
/**
|
||||||
|
* Returns the HTTP headers for the request.
|
||||||
|
*/
|
||||||
|
@property(nonatomic, readonly) NSDictionary<NSString*, NSString*>* headers;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the path component of the URL for the request.
|
||||||
|
*/
|
||||||
@property(nonatomic, readonly) NSString* path;
|
@property(nonatomic, readonly) NSString* path;
|
||||||
@property(nonatomic, readonly) NSDictionary* query; // May be nil
|
|
||||||
@property(nonatomic, readonly) NSString* contentType; // Automatically parsed from headers (nil if request has no body or set to "application/octet-stream" if a body is present without a "Content-Type" header)
|
/**
|
||||||
@property(nonatomic, readonly) NSUInteger contentLength; // Automatically parsed from headers (NSNotFound if request has no "Content-Length" header)
|
* Returns the parsed and unescaped query component of the URL for the request.
|
||||||
@property(nonatomic, readonly) NSDate* ifModifiedSince; // Automatically parsed from headers (nil if request has no "If-Modified-Since" header or it is malformatted)
|
*
|
||||||
@property(nonatomic, readonly) NSString* ifNoneMatch; // Automatically parsed from headers (nil if request has no "If-None-Match" header)
|
* @warning This property will be nil if there is no query in the URL.
|
||||||
@property(nonatomic, readonly) NSRange byteRange; // Automatically parsed from headers ([NSNotFound, 0] if request has no "Range" header, [offset, length] for byte range from beginning or [NSNotFound, -length] from end)
|
*/
|
||||||
@property(nonatomic, readonly) BOOL acceptsGzipContentEncoding; // Automatically parsed from headers
|
@property(nonatomic, readonly, nullable) NSDictionary<NSString*, NSString*>* query;
|
||||||
- (instancetype)initWithMethod:(NSString*)method url:(NSURL*)url headers:(NSDictionary*)headers path:(NSString*)path query:(NSDictionary*)query;
|
|
||||||
- (BOOL)hasBody; // Convenience method that checks if "contentType" is not nil
|
/**
|
||||||
- (BOOL)hasByteRange; // Convenience method that checks "byteRange"
|
* Returns the content type for the body of the request parsed from the
|
||||||
|
* "Content-Type" header.
|
||||||
|
*
|
||||||
|
* This property will be nil if the request has no body or set to
|
||||||
|
* "application/octet-stream" if a body is present but there was no
|
||||||
|
* "Content-Type" header.
|
||||||
|
*/
|
||||||
|
@property(nonatomic, readonly, nullable) NSString* contentType;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the content length for the body of the request parsed from the
|
||||||
|
* "Content-Length" header.
|
||||||
|
*
|
||||||
|
* This property will be set to "NSUIntegerMax" if the request has no body or
|
||||||
|
* if there is a body but no "Content-Length" header, typically because
|
||||||
|
* chunked transfer encoding is used.
|
||||||
|
*/
|
||||||
|
@property(nonatomic, readonly) NSUInteger contentLength;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the parsed "If-Modified-Since" header or nil if absent or malformed.
|
||||||
|
*/
|
||||||
|
@property(nonatomic, readonly, nullable) NSDate* ifModifiedSince;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the parsed "If-None-Match" header or nil if absent or malformed.
|
||||||
|
*/
|
||||||
|
@property(nonatomic, readonly, nullable) NSString* ifNoneMatch;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the parsed "Range" header or (NSUIntegerMax, 0) if absent or malformed.
|
||||||
|
* The range will be set to (offset, length) if expressed from the beginning
|
||||||
|
* of the entity body, or (NSUIntegerMax, length) if expressed from its end.
|
||||||
|
*/
|
||||||
|
@property(nonatomic, readonly) NSRange byteRange;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns YES if the client supports gzip content encoding according to the
|
||||||
|
* "Accept-Encoding" header.
|
||||||
|
*/
|
||||||
|
@property(nonatomic, readonly) BOOL acceptsGzipContentEncoding;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the address of the local peer (i.e. server) for the request
|
||||||
|
* as a raw "struct sockaddr".
|
||||||
|
*/
|
||||||
|
@property(nonatomic, readonly) NSData* localAddressData;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the address of the local peer (i.e. server) for the request
|
||||||
|
* as a string.
|
||||||
|
*/
|
||||||
|
@property(nonatomic, readonly) NSString* localAddressString;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the address of the remote peer (i.e. client) for the request
|
||||||
|
* as a raw "struct sockaddr".
|
||||||
|
*/
|
||||||
|
@property(nonatomic, readonly) NSData* remoteAddressData;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the address of the remote peer (i.e. client) for the request
|
||||||
|
* as a string.
|
||||||
|
*/
|
||||||
|
@property(nonatomic, readonly) NSString* remoteAddressString;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This method is the designated initializer for the class.
|
||||||
|
*/
|
||||||
|
- (instancetype)initWithMethod:(NSString*)method url:(NSURL*)url headers:(NSDictionary<NSString*, NSString*>*)headers path:(NSString*)path query:(nullable NSDictionary<NSString*, NSString*>*)query;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convenience method that checks if the contentType property is defined.
|
||||||
|
*/
|
||||||
|
- (BOOL)hasBody;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convenience method that checks if the byteRange property is defined.
|
||||||
|
*/
|
||||||
|
- (BOOL)hasByteRange;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves an attribute associated with this request using the given key.
|
||||||
|
*
|
||||||
|
* @return The attribute value for the key.
|
||||||
|
*/
|
||||||
|
- (nullable id)attributeForKey:(NSString*)key;
|
||||||
|
|
||||||
@end
|
@end
|
||||||
|
|
||||||
|
NS_ASSUME_NONNULL_END
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
Copyright (c) 2012-2014, Pierre-Olivier Latour
|
Copyright (c) 2012-2019, Pierre-Olivier Latour
|
||||||
All rights reserved.
|
All rights reserved.
|
||||||
|
|
||||||
Redistribution and use in source and binary forms, with or without
|
Redistribution and use in source and binary forms, with or without
|
||||||
@@ -25,30 +25,31 @@
|
|||||||
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
#if !__has_feature(objc_arc)
|
||||||
|
#error GCDWebServer requires ARC
|
||||||
|
#endif
|
||||||
|
|
||||||
#import <zlib.h>
|
#import <zlib.h>
|
||||||
|
|
||||||
#import "GCDWebServerPrivate.h"
|
#import "GCDWebServerPrivate.h"
|
||||||
|
|
||||||
|
NSString* const GCDWebServerRequestAttribute_RegexCaptures = @"GCDWebServerRequestAttribute_RegexCaptures";
|
||||||
|
|
||||||
#define kZlibErrorDomain @"ZlibErrorDomain"
|
#define kZlibErrorDomain @"ZlibErrorDomain"
|
||||||
#define kGZipInitialBufferSize (256 * 1024)
|
#define kGZipInitialBufferSize (256 * 1024)
|
||||||
|
|
||||||
@interface GCDWebServerBodyDecoder : NSObject <GCDWebServerBodyWriter>
|
@interface GCDWebServerBodyDecoder : NSObject <GCDWebServerBodyWriter>
|
||||||
- (id)initWithRequest:(GCDWebServerRequest*)request writer:(id<GCDWebServerBodyWriter>)writer;
|
|
||||||
@end
|
@end
|
||||||
|
|
||||||
@interface GCDWebServerGZipDecoder : GCDWebServerBodyDecoder
|
@interface GCDWebServerGZipDecoder : GCDWebServerBodyDecoder
|
||||||
@end
|
@end
|
||||||
|
|
||||||
@interface GCDWebServerBodyDecoder () {
|
@implementation GCDWebServerBodyDecoder {
|
||||||
@private
|
|
||||||
GCDWebServerRequest* __unsafe_unretained _request;
|
GCDWebServerRequest* __unsafe_unretained _request;
|
||||||
id<GCDWebServerBodyWriter> __unsafe_unretained _writer;
|
id<GCDWebServerBodyWriter> __unsafe_unretained _writer;
|
||||||
}
|
}
|
||||||
@end
|
|
||||||
|
|
||||||
@implementation GCDWebServerBodyDecoder
|
- (instancetype)initWithRequest:(GCDWebServerRequest* _Nonnull)request writer:(id<GCDWebServerBodyWriter> _Nonnull)writer {
|
||||||
|
|
||||||
- (id)initWithRequest:(GCDWebServerRequest*)request writer:(id<GCDWebServerBodyWriter>)writer {
|
|
||||||
if ((self = [super init])) {
|
if ((self = [super init])) {
|
||||||
_request = request;
|
_request = request;
|
||||||
_writer = writer;
|
_writer = writer;
|
||||||
@@ -70,35 +71,33 @@
|
|||||||
|
|
||||||
@end
|
@end
|
||||||
|
|
||||||
@interface GCDWebServerGZipDecoder () {
|
@implementation GCDWebServerGZipDecoder {
|
||||||
@private
|
|
||||||
z_stream _stream;
|
z_stream _stream;
|
||||||
BOOL _finished;
|
BOOL _finished;
|
||||||
}
|
}
|
||||||
@end
|
|
||||||
|
|
||||||
@implementation GCDWebServerGZipDecoder
|
|
||||||
|
|
||||||
- (BOOL)open:(NSError**)error {
|
- (BOOL)open:(NSError**)error {
|
||||||
int result = inflateInit2(&_stream, 15 + 16);
|
int result = inflateInit2(&_stream, 15 + 16);
|
||||||
if (result != Z_OK) {
|
if (result != Z_OK) {
|
||||||
*error = [NSError errorWithDomain:kZlibErrorDomain code:result userInfo:nil];
|
if (error) {
|
||||||
|
*error = [NSError errorWithDomain:kZlibErrorDomain code:result userInfo:nil];
|
||||||
|
}
|
||||||
return NO;
|
return NO;
|
||||||
}
|
}
|
||||||
if (![super open:error]) {
|
if (![super open:error]) {
|
||||||
deflateEnd(&_stream);
|
inflateEnd(&_stream);
|
||||||
return NO;
|
return NO;
|
||||||
}
|
}
|
||||||
return YES;
|
return YES;
|
||||||
}
|
}
|
||||||
|
|
||||||
- (BOOL)writeData:(NSData*)data error:(NSError**)error {
|
- (BOOL)writeData:(NSData*)data error:(NSError**)error {
|
||||||
DCHECK(!_finished);
|
GWS_DCHECK(!_finished);
|
||||||
_stream.next_in = (Bytef*)data.bytes;
|
_stream.next_in = (Bytef*)data.bytes;
|
||||||
_stream.avail_in = (uInt)data.length;
|
_stream.avail_in = (uInt)data.length;
|
||||||
NSMutableData* decodedData = [[NSMutableData alloc] initWithLength:kGZipInitialBufferSize];
|
NSMutableData* decodedData = [[NSMutableData alloc] initWithLength:kGZipInitialBufferSize];
|
||||||
if (decodedData == nil) {
|
if (decodedData == nil) {
|
||||||
DNOT_REACHED();
|
GWS_DNOT_REACHED();
|
||||||
return NO;
|
return NO;
|
||||||
}
|
}
|
||||||
NSUInteger length = 0;
|
NSUInteger length = 0;
|
||||||
@@ -108,8 +107,9 @@
|
|||||||
_stream.avail_out = (uInt)maxLength;
|
_stream.avail_out = (uInt)maxLength;
|
||||||
int result = inflate(&_stream, Z_NO_FLUSH);
|
int result = inflate(&_stream, Z_NO_FLUSH);
|
||||||
if ((result != Z_OK) && (result != Z_STREAM_END)) {
|
if ((result != Z_OK) && (result != Z_STREAM_END)) {
|
||||||
ARC_RELEASE(decodedData);
|
if (error) {
|
||||||
*error = [NSError errorWithDomain:kZlibErrorDomain code:result userInfo:nil];
|
*error = [NSError errorWithDomain:kZlibErrorDomain code:result userInfo:nil];
|
||||||
|
}
|
||||||
return NO;
|
return NO;
|
||||||
}
|
}
|
||||||
length += maxLength - _stream.avail_out;
|
length += maxLength - _stream.avail_out;
|
||||||
@@ -123,145 +123,115 @@
|
|||||||
}
|
}
|
||||||
decodedData.length = length;
|
decodedData.length = length;
|
||||||
BOOL success = length ? [super writeData:decodedData error:error] : YES; // No need to call writer if we have no data yet
|
BOOL success = length ? [super writeData:decodedData error:error] : YES; // No need to call writer if we have no data yet
|
||||||
ARC_RELEASE(decodedData);
|
|
||||||
return success;
|
return success;
|
||||||
}
|
}
|
||||||
|
|
||||||
- (BOOL)close:(NSError**)error {
|
- (BOOL)close:(NSError**)error {
|
||||||
DCHECK(_finished);
|
GWS_DCHECK(_finished);
|
||||||
inflateEnd(&_stream);
|
inflateEnd(&_stream);
|
||||||
return [super close:error];
|
return [super close:error];
|
||||||
}
|
}
|
||||||
|
|
||||||
@end
|
@end
|
||||||
|
|
||||||
@interface GCDWebServerRequest () {
|
@implementation GCDWebServerRequest {
|
||||||
@private
|
|
||||||
NSString* _method;
|
|
||||||
NSURL* _url;
|
|
||||||
NSDictionary* _headers;
|
|
||||||
NSString* _path;
|
|
||||||
NSDictionary* _query;
|
|
||||||
NSString* _type;
|
|
||||||
BOOL _chunked;
|
|
||||||
NSUInteger _length;
|
|
||||||
NSDate* _modifiedSince;
|
|
||||||
NSString* _noneMatch;
|
|
||||||
NSRange _range;
|
|
||||||
BOOL _gzipAccepted;
|
|
||||||
|
|
||||||
BOOL _opened;
|
BOOL _opened;
|
||||||
NSMutableArray* _decoders;
|
NSMutableArray<GCDWebServerBodyDecoder*>* _decoders;
|
||||||
id<GCDWebServerBodyWriter> __unsafe_unretained _writer;
|
id<GCDWebServerBodyWriter> __unsafe_unretained _writer;
|
||||||
|
NSMutableDictionary<NSString*, id>* _attributes;
|
||||||
}
|
}
|
||||||
@end
|
|
||||||
|
|
||||||
@implementation GCDWebServerRequest : NSObject
|
- (instancetype)initWithMethod:(NSString*)method url:(NSURL*)url headers:(NSDictionary<NSString*, NSString*>*)headers path:(NSString*)path query:(NSDictionary<NSString*, NSString*>*)query {
|
||||||
|
|
||||||
@synthesize method=_method, URL=_url, headers=_headers, path=_path, query=_query, contentType=_type, contentLength=_length, ifModifiedSince=_modifiedSince, ifNoneMatch=_noneMatch,
|
|
||||||
byteRange=_range, acceptsGzipContentEncoding=_gzipAccepted, usesChunkedTransferEncoding=_chunked;
|
|
||||||
|
|
||||||
- (instancetype)initWithMethod:(NSString*)method url:(NSURL*)url headers:(NSDictionary*)headers path:(NSString*)path query:(NSDictionary*)query {
|
|
||||||
if ((self = [super init])) {
|
if ((self = [super init])) {
|
||||||
_method = [method copy];
|
_method = [method copy];
|
||||||
_url = ARC_RETAIN(url);
|
_URL = url;
|
||||||
_headers = ARC_RETAIN(headers);
|
_headers = headers;
|
||||||
_path = [path copy];
|
_path = [path copy];
|
||||||
_query = ARC_RETAIN(query);
|
_query = query;
|
||||||
|
|
||||||
_type = ARC_RETAIN(GCDWebServerNormalizeHeaderValue([_headers objectForKey:@"Content-Type"]));
|
_contentType = GCDWebServerNormalizeHeaderValue([_headers objectForKey:@"Content-Type"]);
|
||||||
_chunked = [GCDWebServerNormalizeHeaderValue([_headers objectForKey:@"Transfer-Encoding"]) isEqualToString:@"chunked"];
|
_usesChunkedTransferEncoding = [GCDWebServerNormalizeHeaderValue([_headers objectForKey:@"Transfer-Encoding"]) isEqualToString:@"chunked"];
|
||||||
NSString* lengthHeader = [_headers objectForKey:@"Content-Length"];
|
NSString* lengthHeader = [_headers objectForKey:@"Content-Length"];
|
||||||
if (lengthHeader) {
|
if (lengthHeader) {
|
||||||
NSInteger length = [lengthHeader integerValue];
|
NSInteger length = [lengthHeader integerValue];
|
||||||
if (_chunked || (length < 0)) {
|
if (_usesChunkedTransferEncoding || (length < 0)) {
|
||||||
DNOT_REACHED();
|
GWS_LOG_WARNING(@"Invalid 'Content-Length' header '%@' for '%@' request on \"%@\"", lengthHeader, _method, _URL);
|
||||||
ARC_RELEASE(self);
|
GWS_DNOT_REACHED();
|
||||||
return nil;
|
return nil;
|
||||||
}
|
}
|
||||||
_length = length;
|
_contentLength = length;
|
||||||
if (_type == nil) {
|
if (_contentType == nil) {
|
||||||
_type = kGCDWebServerDefaultMimeType;
|
_contentType = kGCDWebServerDefaultMimeType;
|
||||||
}
|
}
|
||||||
} else if (_chunked) {
|
} else if (_usesChunkedTransferEncoding) {
|
||||||
if (_type == nil) {
|
if (_contentType == nil) {
|
||||||
_type = kGCDWebServerDefaultMimeType;
|
_contentType = kGCDWebServerDefaultMimeType;
|
||||||
}
|
}
|
||||||
_length = NSNotFound;
|
_contentLength = NSUIntegerMax;
|
||||||
} else {
|
} else {
|
||||||
if (_type) {
|
if (_contentType) {
|
||||||
DNOT_REACHED();
|
GWS_LOG_WARNING(@"Ignoring 'Content-Type' header for '%@' request on \"%@\"", _method, _URL);
|
||||||
ARC_RELEASE(self);
|
_contentType = nil; // Content-Type without Content-Length or chunked-encoding doesn't make sense
|
||||||
return nil;
|
|
||||||
}
|
}
|
||||||
_length = NSNotFound;
|
_contentLength = NSUIntegerMax;
|
||||||
}
|
}
|
||||||
|
|
||||||
NSString* modifiedHeader = [_headers objectForKey:@"If-Modified-Since"];
|
NSString* modifiedHeader = [_headers objectForKey:@"If-Modified-Since"];
|
||||||
if (modifiedHeader) {
|
if (modifiedHeader) {
|
||||||
_modifiedSince = [GCDWebServerParseRFC822(modifiedHeader) copy];
|
_ifModifiedSince = [GCDWebServerParseRFC822(modifiedHeader) copy];
|
||||||
}
|
}
|
||||||
_noneMatch = ARC_RETAIN([_headers objectForKey:@"If-None-Match"]);
|
_ifNoneMatch = [_headers objectForKey:@"If-None-Match"];
|
||||||
|
|
||||||
_range = NSMakeRange(NSNotFound, 0);
|
_byteRange = NSMakeRange(NSUIntegerMax, 0);
|
||||||
NSString* rangeHeader = GCDWebServerNormalizeHeaderValue([_headers objectForKey:@"Range"]);
|
NSString* rangeHeader = GCDWebServerNormalizeHeaderValue([_headers objectForKey:@"Range"]);
|
||||||
if (rangeHeader) {
|
if (rangeHeader) {
|
||||||
if ([rangeHeader hasPrefix:@"bytes="]) {
|
if ([rangeHeader hasPrefix:@"bytes="]) {
|
||||||
NSArray* components = [[rangeHeader substringFromIndex:6] componentsSeparatedByString:@","];
|
NSArray* components = [[rangeHeader substringFromIndex:6] componentsSeparatedByString:@","];
|
||||||
if (components.count == 1) {
|
if (components.count == 1) {
|
||||||
components = [[components firstObject] componentsSeparatedByString:@"-"];
|
components = [(NSString*)[components firstObject] componentsSeparatedByString:@"-"];
|
||||||
if (components.count == 2) {
|
if (components.count == 2) {
|
||||||
NSString* startString = [components objectAtIndex:0];
|
NSString* startString = [components objectAtIndex:0];
|
||||||
NSInteger startValue = [startString integerValue];
|
NSInteger startValue = [startString integerValue];
|
||||||
NSString* endString = [components objectAtIndex:1];
|
NSString* endString = [components objectAtIndex:1];
|
||||||
NSInteger endValue = [endString integerValue];
|
NSInteger endValue = [endString integerValue];
|
||||||
if (startString.length && (startValue >= 0) && endString.length && (endValue >= startValue)) { // The second 500 bytes: "500-999"
|
if (startString.length && (startValue >= 0) && endString.length && (endValue >= startValue)) { // The second 500 bytes: "500-999"
|
||||||
_range.location = startValue;
|
_byteRange.location = startValue;
|
||||||
_range.length = endValue - startValue + 1;
|
_byteRange.length = endValue - startValue + 1;
|
||||||
} else if (startString.length && (startValue >= 0)) { // The bytes after 9500 bytes: "9500-"
|
} else if (startString.length && (startValue >= 0)) { // The bytes after 9500 bytes: "9500-"
|
||||||
_range.location = startValue;
|
_byteRange.location = startValue;
|
||||||
_range.length = NSUIntegerMax;
|
_byteRange.length = NSUIntegerMax;
|
||||||
} else if (endString.length && (endValue > 0)) { // The final 500 bytes: "-500"
|
} else if (endString.length && (endValue > 0)) { // The final 500 bytes: "-500"
|
||||||
_range.location = NSNotFound;
|
_byteRange.location = NSUIntegerMax;
|
||||||
_range.length = endValue;
|
_byteRange.length = endValue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if ((_range.location == NSNotFound) && (_range.length == 0)) { // Ignore "Range" header if syntactically invalid
|
if ((_byteRange.location == NSUIntegerMax) && (_byteRange.length == 0)) { // Ignore "Range" header if syntactically invalid
|
||||||
LOG_WARNING(@"Failed to parse 'Range' header \"%@\" for url: %@", rangeHeader, url);
|
GWS_LOG_WARNING(@"Failed to parse 'Range' header \"%@\" for url: %@", rangeHeader, url);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if ([[_headers objectForKey:@"Accept-Encoding"] rangeOfString:@"gzip"].location != NSNotFound) {
|
if ([[_headers objectForKey:@"Accept-Encoding"] rangeOfString:@"gzip"].location != NSNotFound) {
|
||||||
_gzipAccepted = YES;
|
_acceptsGzipContentEncoding = YES;
|
||||||
}
|
}
|
||||||
|
|
||||||
_decoders = [[NSMutableArray alloc] init];
|
_decoders = [[NSMutableArray alloc] init];
|
||||||
|
_attributes = [[NSMutableDictionary alloc] init];
|
||||||
}
|
}
|
||||||
return self;
|
return self;
|
||||||
}
|
}
|
||||||
|
|
||||||
- (void)dealloc {
|
|
||||||
ARC_RELEASE(_method);
|
|
||||||
ARC_RELEASE(_url);
|
|
||||||
ARC_RELEASE(_headers);
|
|
||||||
ARC_RELEASE(_path);
|
|
||||||
ARC_RELEASE(_query);
|
|
||||||
ARC_RELEASE(_type);
|
|
||||||
ARC_RELEASE(_modifiedSince);
|
|
||||||
ARC_RELEASE(_noneMatch);
|
|
||||||
ARC_RELEASE(_decoders);
|
|
||||||
|
|
||||||
ARC_DEALLOC(super);
|
|
||||||
}
|
|
||||||
|
|
||||||
- (BOOL)hasBody {
|
- (BOOL)hasBody {
|
||||||
return _type ? YES : NO;
|
return _contentType ? YES : NO;
|
||||||
}
|
}
|
||||||
|
|
||||||
- (BOOL)hasByteRange {
|
- (BOOL)hasByteRange {
|
||||||
return GCDWebServerIsValidByteRange(_range);
|
return GCDWebServerIsValidByteRange(_byteRange);
|
||||||
|
}
|
||||||
|
|
||||||
|
- (id)attributeForKey:(NSString*)key {
|
||||||
|
return [_attributes objectForKey:key];
|
||||||
}
|
}
|
||||||
|
|
||||||
- (BOOL)open:(NSError**)error {
|
- (BOOL)open:(NSError**)error {
|
||||||
@@ -281,16 +251,15 @@
|
|||||||
if ([GCDWebServerNormalizeHeaderValue([self.headers objectForKey:@"Content-Encoding"]) isEqualToString:@"gzip"]) {
|
if ([GCDWebServerNormalizeHeaderValue([self.headers objectForKey:@"Content-Encoding"]) isEqualToString:@"gzip"]) {
|
||||||
GCDWebServerGZipDecoder* decoder = [[GCDWebServerGZipDecoder alloc] initWithRequest:self writer:_writer];
|
GCDWebServerGZipDecoder* decoder = [[GCDWebServerGZipDecoder alloc] initWithRequest:self writer:_writer];
|
||||||
[_decoders addObject:decoder];
|
[_decoders addObject:decoder];
|
||||||
ARC_RELEASE(decoder);
|
|
||||||
_writer = decoder;
|
_writer = decoder;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
- (BOOL)performOpen:(NSError**)error {
|
- (BOOL)performOpen:(NSError**)error {
|
||||||
DCHECK(_type);
|
GWS_DCHECK(_contentType);
|
||||||
DCHECK(_writer);
|
GWS_DCHECK(_writer);
|
||||||
if (_opened) {
|
if (_opened) {
|
||||||
DNOT_REACHED();
|
GWS_DNOT_REACHED();
|
||||||
return NO;
|
return NO;
|
||||||
}
|
}
|
||||||
_opened = YES;
|
_opened = YES;
|
||||||
@@ -298,15 +267,27 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
- (BOOL)performWriteData:(NSData*)data error:(NSError**)error {
|
- (BOOL)performWriteData:(NSData*)data error:(NSError**)error {
|
||||||
DCHECK(_opened);
|
GWS_DCHECK(_opened);
|
||||||
return [_writer writeData:data error:error];
|
return [_writer writeData:data error:error];
|
||||||
}
|
}
|
||||||
|
|
||||||
- (BOOL)performClose:(NSError**)error {
|
- (BOOL)performClose:(NSError**)error {
|
||||||
DCHECK(_opened);
|
GWS_DCHECK(_opened);
|
||||||
return [_writer close:error];
|
return [_writer close:error];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
- (void)setAttribute:(id)attribute forKey:(NSString*)key {
|
||||||
|
[_attributes setValue:attribute forKey:key];
|
||||||
|
}
|
||||||
|
|
||||||
|
- (NSString*)localAddressString {
|
||||||
|
return GCDWebServerStringFromSockAddr(_localAddressData.bytes, YES);
|
||||||
|
}
|
||||||
|
|
||||||
|
- (NSString*)remoteAddressString {
|
||||||
|
return GCDWebServerStringFromSockAddr(_remoteAddressData.bytes, YES);
|
||||||
|
}
|
||||||
|
|
||||||
- (NSString*)description {
|
- (NSString*)description {
|
||||||
NSMutableString* description = [NSMutableString stringWithFormat:@"%@ %@", _method, _path];
|
NSMutableString* description = [NSMutableString stringWithFormat:@"%@ %@", _method, _path];
|
||||||
for (NSString* argument in [[_query allKeys] sortedArrayUsingSelector:@selector(compare:)]) {
|
for (NSString* argument in [[_query allKeys] sortedArrayUsingSelector:@selector(compare:)]) {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
Copyright (c) 2012-2014, Pierre-Olivier Latour
|
Copyright (c) 2012-2019, Pierre-Olivier Latour
|
||||||
All rights reserved.
|
All rights reserved.
|
||||||
|
|
||||||
Redistribution and use in source and binary forms, with or without
|
Redistribution and use in source and binary forms, with or without
|
||||||
@@ -27,29 +27,186 @@
|
|||||||
|
|
||||||
#import <Foundation/Foundation.h>
|
#import <Foundation/Foundation.h>
|
||||||
|
|
||||||
|
NS_ASSUME_NONNULL_BEGIN
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The GCDWebServerBodyReaderCompletionBlock is passed by GCDWebServer to the
|
||||||
|
* GCDWebServerBodyReader object when reading data from it asynchronously.
|
||||||
|
*/
|
||||||
|
typedef void (^GCDWebServerBodyReaderCompletionBlock)(NSData* _Nullable data, NSError* _Nullable error);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This protocol is used by the GCDWebServerConnection to communicate with
|
||||||
|
* the GCDWebServerResponse and read the HTTP body data to send.
|
||||||
|
*
|
||||||
|
* Note that multiple GCDWebServerBodyReader objects can be chained together
|
||||||
|
* internally e.g. to automatically apply gzip encoding to the content before
|
||||||
|
* passing it on to the GCDWebServerResponse.
|
||||||
|
*
|
||||||
|
* @warning These methods can be called on any GCD thread.
|
||||||
|
*/
|
||||||
@protocol GCDWebServerBodyReader <NSObject>
|
@protocol GCDWebServerBodyReader <NSObject>
|
||||||
- (BOOL)open:(NSError**)error; // Return NO on error ("error" is guaranteed to be non-NULL)
|
|
||||||
- (NSData*)readData:(NSError**)error; // Must return nil on error or empty NSData if at end ("error" is guaranteed to be non-NULL)
|
@required
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This method is called before any body data is sent.
|
||||||
|
*
|
||||||
|
* It should return YES on success or NO on failure and set the "error" argument
|
||||||
|
* which is guaranteed to be non-NULL.
|
||||||
|
*/
|
||||||
|
- (BOOL)open:(NSError**)error;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This method is called whenever body data is sent.
|
||||||
|
*
|
||||||
|
* It should return a non-empty NSData if there is body data available,
|
||||||
|
* or an empty NSData there is no more body data, or nil on error and set
|
||||||
|
* the "error" argument which is guaranteed to be non-NULL.
|
||||||
|
*/
|
||||||
|
- (nullable NSData*)readData:(NSError**)error;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This method is called after all body data has been sent.
|
||||||
|
*/
|
||||||
- (void)close;
|
- (void)close;
|
||||||
|
|
||||||
|
@optional
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If this method is implemented, it will be preferred over -readData:.
|
||||||
|
*
|
||||||
|
* It must call the passed block when data is available, passing a non-empty
|
||||||
|
* NSData if there is body data available, or an empty NSData there is no more
|
||||||
|
* body data, or nil on error and pass an NSError along.
|
||||||
|
*/
|
||||||
|
- (void)asyncReadDataWithCompletion:(GCDWebServerBodyReaderCompletionBlock)block;
|
||||||
|
|
||||||
@end
|
@end
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The GCDWebServerResponse class is used to wrap a single HTTP response.
|
||||||
|
* It is instantiated by the handler of the GCDWebServer that handled the request.
|
||||||
|
* If a body is present, the methods from the GCDWebServerBodyReader protocol
|
||||||
|
* will be called by the GCDWebServerConnection to send it.
|
||||||
|
*
|
||||||
|
* The default implementation of the GCDWebServerBodyReader protocol
|
||||||
|
* on the class simply returns an empty body.
|
||||||
|
*
|
||||||
|
* @warning GCDWebServerResponse instances can be created and used on any GCD thread.
|
||||||
|
*/
|
||||||
@interface GCDWebServerResponse : NSObject <GCDWebServerBodyReader>
|
@interface GCDWebServerResponse : NSObject <GCDWebServerBodyReader>
|
||||||
@property(nonatomic, copy) NSString* contentType; // Default is nil i.e. no body (must be set if a body is present)
|
|
||||||
@property(nonatomic) NSUInteger contentLength; // Default is NSNotFound i.e. undefined (if a body is present but length is undefined, chunked transfer encoding will be enabled)
|
/**
|
||||||
@property(nonatomic) NSInteger statusCode; // Default is 200
|
* Sets the content type for the body of the response.
|
||||||
@property(nonatomic) NSUInteger cacheControlMaxAge; // Default is 0 seconds i.e. "Cache-Control: no-cache"
|
*
|
||||||
@property(nonatomic, retain) NSDate* lastModifiedDate; // Default is nil i.e. no "Last-Modified" header
|
* The default value is nil i.e. the response has no body.
|
||||||
@property(nonatomic, copy) NSString* eTag; // Default is nil i.e. no "ETag" header
|
*
|
||||||
@property(nonatomic, getter=isGZipContentEncodingEnabled) BOOL gzipContentEncodingEnabled; // Default is disabled
|
* @warning This property must be set if a body is present.
|
||||||
|
*/
|
||||||
|
@property(nonatomic, copy, nullable) NSString* contentType;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the content length for the body of the response. If a body is present
|
||||||
|
* but this property is set to "NSUIntegerMax", this means the length of the body
|
||||||
|
* cannot be known ahead of time. Chunked transfer encoding will be
|
||||||
|
* automatically enabled by the GCDWebServerConnection to comply with HTTP/1.1
|
||||||
|
* specifications.
|
||||||
|
*
|
||||||
|
* The default value is "NSUIntegerMax" i.e. the response has no body or its length
|
||||||
|
* is undefined.
|
||||||
|
*/
|
||||||
|
@property(nonatomic) NSUInteger contentLength;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the HTTP status code for the response.
|
||||||
|
*
|
||||||
|
* The default value is 200 i.e. "OK".
|
||||||
|
*/
|
||||||
|
@property(nonatomic) NSInteger statusCode;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the caching hint for the response using the "Cache-Control" header.
|
||||||
|
* This value is expressed in seconds.
|
||||||
|
*
|
||||||
|
* The default value is 0 i.e. "no-cache".
|
||||||
|
*/
|
||||||
|
@property(nonatomic) NSUInteger cacheControlMaxAge;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the last modified date for the response using the "Last-Modified" header.
|
||||||
|
*
|
||||||
|
* The default value is nil.
|
||||||
|
*/
|
||||||
|
@property(nonatomic, nullable) NSDate* lastModifiedDate;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the ETag for the response using the "ETag" header.
|
||||||
|
*
|
||||||
|
* The default value is nil.
|
||||||
|
*/
|
||||||
|
@property(nonatomic, copy, nullable) NSString* eTag;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enables gzip encoding for the response body.
|
||||||
|
*
|
||||||
|
* The default value is NO.
|
||||||
|
*
|
||||||
|
* @warning Enabling gzip encoding will remove any "Content-Length" header
|
||||||
|
* since the length of the body is not known anymore. The client will still
|
||||||
|
* be able to determine the body length when connection is closed per
|
||||||
|
* HTTP/1.1 specifications.
|
||||||
|
*/
|
||||||
|
@property(nonatomic, getter=isGZipContentEncodingEnabled) BOOL gzipContentEncodingEnabled;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates an empty response.
|
||||||
|
*/
|
||||||
+ (instancetype)response;
|
+ (instancetype)response;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This method is the designated initializer for the class.
|
||||||
|
*/
|
||||||
- (instancetype)init;
|
- (instancetype)init;
|
||||||
- (void)setValue:(NSString*)value forAdditionalHeader:(NSString*)header; // Pass nil value to remove header
|
|
||||||
- (BOOL)hasBody; // Convenience method that checks if "contentType" is not nil
|
/**
|
||||||
|
* Sets an additional HTTP header on the response.
|
||||||
|
* Pass a nil value to remove an additional header.
|
||||||
|
*
|
||||||
|
* @warning Do not attempt to override the primary headers used
|
||||||
|
* by GCDWebServerResponse like "Content-Type", "ETag", etc...
|
||||||
|
*/
|
||||||
|
- (void)setValue:(nullable NSString*)value forAdditionalHeader:(NSString*)header;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convenience method that checks if the contentType property is defined.
|
||||||
|
*/
|
||||||
|
- (BOOL)hasBody;
|
||||||
|
|
||||||
@end
|
@end
|
||||||
|
|
||||||
@interface GCDWebServerResponse (Extensions)
|
@interface GCDWebServerResponse (Extensions)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a empty response with a specific HTTP status code.
|
||||||
|
*/
|
||||||
+ (instancetype)responseWithStatusCode:(NSInteger)statusCode;
|
+ (instancetype)responseWithStatusCode:(NSInteger)statusCode;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates an HTTP redirect response to a new URL.
|
||||||
|
*/
|
||||||
+ (instancetype)responseWithRedirect:(NSURL*)location permanent:(BOOL)permanent;
|
+ (instancetype)responseWithRedirect:(NSURL*)location permanent:(BOOL)permanent;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initializes an empty response with a specific HTTP status code.
|
||||||
|
*/
|
||||||
- (instancetype)initWithStatusCode:(NSInteger)statusCode;
|
- (instancetype)initWithStatusCode:(NSInteger)statusCode;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initializes an HTTP redirect response to a new URL.
|
||||||
|
*/
|
||||||
- (instancetype)initWithRedirect:(NSURL*)location permanent:(BOOL)permanent;
|
- (instancetype)initWithRedirect:(NSURL*)location permanent:(BOOL)permanent;
|
||||||
|
|
||||||
@end
|
@end
|
||||||
|
|
||||||
|
NS_ASSUME_NONNULL_END
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
Copyright (c) 2012-2014, Pierre-Olivier Latour
|
Copyright (c) 2012-2019, Pierre-Olivier Latour
|
||||||
All rights reserved.
|
All rights reserved.
|
||||||
|
|
||||||
Redistribution and use in source and binary forms, with or without
|
Redistribution and use in source and binary forms, with or without
|
||||||
@@ -25,6 +25,10 @@
|
|||||||
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
#if !__has_feature(objc_arc)
|
||||||
|
#error GCDWebServer requires ARC
|
||||||
|
#endif
|
||||||
|
|
||||||
#import <zlib.h>
|
#import <zlib.h>
|
||||||
|
|
||||||
#import "GCDWebServerPrivate.h"
|
#import "GCDWebServerPrivate.h"
|
||||||
@@ -33,22 +37,17 @@
|
|||||||
#define kGZipInitialBufferSize (256 * 1024)
|
#define kGZipInitialBufferSize (256 * 1024)
|
||||||
|
|
||||||
@interface GCDWebServerBodyEncoder : NSObject <GCDWebServerBodyReader>
|
@interface GCDWebServerBodyEncoder : NSObject <GCDWebServerBodyReader>
|
||||||
- (id)initWithResponse:(GCDWebServerResponse*)response reader:(id<GCDWebServerBodyReader>)reader;
|
|
||||||
@end
|
@end
|
||||||
|
|
||||||
@interface GCDWebServerGZipEncoder : GCDWebServerBodyEncoder
|
@interface GCDWebServerGZipEncoder : GCDWebServerBodyEncoder
|
||||||
@end
|
@end
|
||||||
|
|
||||||
@interface GCDWebServerBodyEncoder () {
|
@implementation GCDWebServerBodyEncoder {
|
||||||
@private
|
|
||||||
GCDWebServerResponse* __unsafe_unretained _response;
|
GCDWebServerResponse* __unsafe_unretained _response;
|
||||||
id<GCDWebServerBodyReader> __unsafe_unretained _reader;
|
id<GCDWebServerBodyReader> __unsafe_unretained _reader;
|
||||||
}
|
}
|
||||||
@end
|
|
||||||
|
|
||||||
@implementation GCDWebServerBodyEncoder
|
- (instancetype)initWithResponse:(GCDWebServerResponse* _Nonnull)response reader:(id<GCDWebServerBodyReader> _Nonnull)reader {
|
||||||
|
|
||||||
- (id)initWithResponse:(GCDWebServerResponse*)response reader:(id<GCDWebServerBodyReader>)reader {
|
|
||||||
if ((self = [super init])) {
|
if ((self = [super init])) {
|
||||||
_response = response;
|
_response = response;
|
||||||
_reader = reader;
|
_reader = reader;
|
||||||
@@ -70,18 +69,14 @@
|
|||||||
|
|
||||||
@end
|
@end
|
||||||
|
|
||||||
@interface GCDWebServerGZipEncoder () {
|
@implementation GCDWebServerGZipEncoder {
|
||||||
@private
|
|
||||||
z_stream _stream;
|
z_stream _stream;
|
||||||
BOOL _finished;
|
BOOL _finished;
|
||||||
}
|
}
|
||||||
@end
|
|
||||||
|
|
||||||
@implementation GCDWebServerGZipEncoder
|
- (instancetype)initWithResponse:(GCDWebServerResponse* _Nonnull)response reader:(id<GCDWebServerBodyReader> _Nonnull)reader {
|
||||||
|
|
||||||
- (id)initWithResponse:(GCDWebServerResponse*)response reader:(id<GCDWebServerBodyReader>)reader {
|
|
||||||
if ((self = [super initWithResponse:response reader:reader])) {
|
if ((self = [super initWithResponse:response reader:reader])) {
|
||||||
response.contentLength = NSNotFound; // Make sure "Content-Length" header is not set since we don't know it (client will determine body length when connection is closed)
|
response.contentLength = NSUIntegerMax; // Make sure "Content-Length" header is not set since we don't know it
|
||||||
[response setValue:@"gzip" forAdditionalHeader:@"Content-Encoding"];
|
[response setValue:@"gzip" forAdditionalHeader:@"Content-Encoding"];
|
||||||
}
|
}
|
||||||
return self;
|
return self;
|
||||||
@@ -90,7 +85,9 @@
|
|||||||
- (BOOL)open:(NSError**)error {
|
- (BOOL)open:(NSError**)error {
|
||||||
int result = deflateInit2(&_stream, Z_DEFAULT_COMPRESSION, Z_DEFLATED, 15 + 16, 8, Z_DEFAULT_STRATEGY);
|
int result = deflateInit2(&_stream, Z_DEFAULT_COMPRESSION, Z_DEFLATED, 15 + 16, 8, Z_DEFAULT_STRATEGY);
|
||||||
if (result != Z_OK) {
|
if (result != Z_OK) {
|
||||||
*error = [NSError errorWithDomain:kZlibErrorDomain code:result userInfo:nil];
|
if (error) {
|
||||||
|
*error = [NSError errorWithDomain:kZlibErrorDomain code:result userInfo:nil];
|
||||||
|
}
|
||||||
return NO;
|
return NO;
|
||||||
}
|
}
|
||||||
if (![super open:error]) {
|
if (![super open:error]) {
|
||||||
@@ -107,7 +104,7 @@
|
|||||||
} else {
|
} else {
|
||||||
encodedData = [[NSMutableData alloc] initWithLength:kGZipInitialBufferSize];
|
encodedData = [[NSMutableData alloc] initWithLength:kGZipInitialBufferSize];
|
||||||
if (encodedData == nil) {
|
if (encodedData == nil) {
|
||||||
DNOT_REACHED();
|
GWS_DNOT_REACHED();
|
||||||
return nil;
|
return nil;
|
||||||
}
|
}
|
||||||
NSUInteger length = 0;
|
NSUInteger length = 0;
|
||||||
@@ -126,8 +123,9 @@
|
|||||||
if (result == Z_STREAM_END) {
|
if (result == Z_STREAM_END) {
|
||||||
_finished = YES;
|
_finished = YES;
|
||||||
} else if (result != Z_OK) {
|
} else if (result != Z_OK) {
|
||||||
ARC_RELEASE(encodedData);
|
if (error) {
|
||||||
*error = [NSError errorWithDomain:kZlibErrorDomain code:result userInfo:nil];
|
*error = [NSError errorWithDomain:kZlibErrorDomain code:result userInfo:nil];
|
||||||
|
}
|
||||||
return nil;
|
return nil;
|
||||||
}
|
}
|
||||||
length += maxLength - _stream.avail_out;
|
length += maxLength - _stream.avail_out;
|
||||||
@@ -136,11 +134,11 @@
|
|||||||
}
|
}
|
||||||
encodedData.length = 2 * encodedData.length; // zlib has used all the output buffer so resize it and try again in case more data is available
|
encodedData.length = 2 * encodedData.length; // zlib has used all the output buffer so resize it and try again in case more data is available
|
||||||
}
|
}
|
||||||
DCHECK(_stream.avail_in == 0);
|
GWS_DCHECK(_stream.avail_in == 0);
|
||||||
} while (length == 0); // Make sure we don't return an empty NSData if not in finished state
|
} while (length == 0); // Make sure we don't return an empty NSData if not in finished state
|
||||||
encodedData.length = length;
|
encodedData.length = length;
|
||||||
}
|
}
|
||||||
return ARC_AUTORELEASE(encodedData);
|
return encodedData;
|
||||||
}
|
}
|
||||||
|
|
||||||
- (void)close {
|
- (void)close {
|
||||||
@@ -150,65 +148,38 @@
|
|||||||
|
|
||||||
@end
|
@end
|
||||||
|
|
||||||
@interface GCDWebServerResponse () {
|
@implementation GCDWebServerResponse {
|
||||||
@private
|
|
||||||
NSString* _type;
|
|
||||||
NSUInteger _length;
|
|
||||||
NSInteger _status;
|
|
||||||
NSUInteger _maxAge;
|
|
||||||
NSDate* _lastModified;
|
|
||||||
NSString* _eTag;
|
|
||||||
NSMutableDictionary* _headers;
|
|
||||||
BOOL _chunked;
|
|
||||||
BOOL _gzipped;
|
|
||||||
|
|
||||||
BOOL _opened;
|
BOOL _opened;
|
||||||
NSMutableArray* _encoders;
|
NSMutableArray<GCDWebServerBodyEncoder*>* _encoders;
|
||||||
id<GCDWebServerBodyReader> __unsafe_unretained _reader;
|
id<GCDWebServerBodyReader> __unsafe_unretained _reader;
|
||||||
}
|
}
|
||||||
@end
|
|
||||||
|
|
||||||
@implementation GCDWebServerResponse
|
|
||||||
|
|
||||||
@synthesize contentType=_type, contentLength=_length, statusCode=_status, cacheControlMaxAge=_maxAge, lastModifiedDate=_lastModified, eTag=_eTag,
|
|
||||||
gzipContentEncodingEnabled=_gzipped, additionalHeaders=_headers;
|
|
||||||
|
|
||||||
+ (instancetype)response {
|
+ (instancetype)response {
|
||||||
return ARC_AUTORELEASE([[[self class] alloc] init]);
|
return [(GCDWebServerResponse*)[[self class] alloc] init];
|
||||||
}
|
}
|
||||||
|
|
||||||
- (instancetype)init {
|
- (instancetype)init {
|
||||||
if ((self = [super init])) {
|
if ((self = [super init])) {
|
||||||
_type = nil;
|
_contentType = nil;
|
||||||
_length = NSNotFound;
|
_contentLength = NSUIntegerMax;
|
||||||
_status = kGCDWebServerHTTPStatusCode_OK;
|
_statusCode = kGCDWebServerHTTPStatusCode_OK;
|
||||||
_maxAge = 0;
|
_cacheControlMaxAge = 0;
|
||||||
_headers = [[NSMutableDictionary alloc] init];
|
_additionalHeaders = [[NSMutableDictionary alloc] init];
|
||||||
_encoders = [[NSMutableArray alloc] init];
|
_encoders = [[NSMutableArray alloc] init];
|
||||||
}
|
}
|
||||||
return self;
|
return self;
|
||||||
}
|
}
|
||||||
|
|
||||||
- (void)dealloc {
|
|
||||||
ARC_RELEASE(_type);
|
|
||||||
ARC_RELEASE(_lastModified);
|
|
||||||
ARC_RELEASE(_eTag);
|
|
||||||
ARC_RELEASE(_headers);
|
|
||||||
ARC_RELEASE(_encoders);
|
|
||||||
|
|
||||||
ARC_DEALLOC(super);
|
|
||||||
}
|
|
||||||
|
|
||||||
- (void)setValue:(NSString*)value forAdditionalHeader:(NSString*)header {
|
- (void)setValue:(NSString*)value forAdditionalHeader:(NSString*)header {
|
||||||
[_headers setValue:value forKey:header];
|
[_additionalHeaders setValue:value forKey:header];
|
||||||
}
|
}
|
||||||
|
|
||||||
- (BOOL)hasBody {
|
- (BOOL)hasBody {
|
||||||
return _type ? YES : NO;
|
return _contentType ? YES : NO;
|
||||||
}
|
}
|
||||||
|
|
||||||
- (BOOL)usesChunkedTransferEncoding {
|
- (BOOL)usesChunkedTransferEncoding {
|
||||||
return (_type != nil) && (_length == NSNotFound);
|
return (_contentType != nil) && (_contentLength == NSUIntegerMax);
|
||||||
}
|
}
|
||||||
|
|
||||||
- (BOOL)open:(NSError**)error {
|
- (BOOL)open:(NSError**)error {
|
||||||
@@ -225,54 +196,59 @@
|
|||||||
|
|
||||||
- (void)prepareForReading {
|
- (void)prepareForReading {
|
||||||
_reader = self;
|
_reader = self;
|
||||||
if (_gzipped) {
|
if (_gzipContentEncodingEnabled) {
|
||||||
GCDWebServerGZipEncoder* encoder = [[GCDWebServerGZipEncoder alloc] initWithResponse:self reader:_reader];
|
GCDWebServerGZipEncoder* encoder = [[GCDWebServerGZipEncoder alloc] initWithResponse:self reader:_reader];
|
||||||
[_encoders addObject:encoder];
|
[_encoders addObject:encoder];
|
||||||
ARC_RELEASE(encoder);
|
|
||||||
_reader = encoder;
|
_reader = encoder;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
- (BOOL)performOpen:(NSError**)error {
|
- (BOOL)performOpen:(NSError**)error {
|
||||||
DCHECK(_type);
|
GWS_DCHECK(_contentType);
|
||||||
DCHECK(_reader);
|
GWS_DCHECK(_reader);
|
||||||
if (_opened) {
|
if (_opened) {
|
||||||
DNOT_REACHED();
|
GWS_DNOT_REACHED();
|
||||||
return NO;
|
return NO;
|
||||||
}
|
}
|
||||||
_opened = YES;
|
_opened = YES;
|
||||||
return [_reader open:error];
|
return [_reader open:error];
|
||||||
}
|
}
|
||||||
|
|
||||||
- (NSData*)performReadData:(NSError**)error {
|
- (void)performReadDataWithCompletion:(GCDWebServerBodyReaderCompletionBlock)block {
|
||||||
DCHECK(_opened);
|
GWS_DCHECK(_opened);
|
||||||
return [_reader readData:error];
|
if ([_reader respondsToSelector:@selector(asyncReadDataWithCompletion:)]) {
|
||||||
|
[_reader asyncReadDataWithCompletion:[block copy]];
|
||||||
|
} else {
|
||||||
|
NSError* error = nil;
|
||||||
|
NSData* data = [_reader readData:&error];
|
||||||
|
block(data, error);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
- (void)performClose {
|
- (void)performClose {
|
||||||
DCHECK(_opened);
|
GWS_DCHECK(_opened);
|
||||||
[_reader close];
|
[_reader close];
|
||||||
}
|
}
|
||||||
|
|
||||||
- (NSString*)description {
|
- (NSString*)description {
|
||||||
NSMutableString* description = [NSMutableString stringWithFormat:@"Status Code = %i", (int)_status];
|
NSMutableString* description = [NSMutableString stringWithFormat:@"Status Code = %i", (int)_statusCode];
|
||||||
if (_type) {
|
if (_contentType) {
|
||||||
[description appendFormat:@"\nContent Type = %@", _type];
|
[description appendFormat:@"\nContent Type = %@", _contentType];
|
||||||
}
|
}
|
||||||
if (_length != NSNotFound) {
|
if (_contentLength != NSUIntegerMax) {
|
||||||
[description appendFormat:@"\nContent Length = %lu", (unsigned long)_length];
|
[description appendFormat:@"\nContent Length = %lu", (unsigned long)_contentLength];
|
||||||
}
|
}
|
||||||
[description appendFormat:@"\nCache Control Max Age = %lu", (unsigned long)_maxAge];
|
[description appendFormat:@"\nCache Control Max Age = %lu", (unsigned long)_cacheControlMaxAge];
|
||||||
if (_lastModified) {
|
if (_lastModifiedDate) {
|
||||||
[description appendFormat:@"\nLast Modified Date = %@", _lastModified];
|
[description appendFormat:@"\nLast Modified Date = %@", _lastModifiedDate];
|
||||||
}
|
}
|
||||||
if (_eTag) {
|
if (_eTag) {
|
||||||
[description appendFormat:@"\nETag = %@", _eTag];
|
[description appendFormat:@"\nETag = %@", _eTag];
|
||||||
}
|
}
|
||||||
if (_headers.count) {
|
if (_additionalHeaders.count) {
|
||||||
[description appendString:@"\n"];
|
[description appendString:@"\n"];
|
||||||
for (NSString* header in [[_headers allKeys] sortedArrayUsingSelector:@selector(compare:)]) {
|
for (NSString* header in [[_additionalHeaders allKeys] sortedArrayUsingSelector:@selector(compare:)]) {
|
||||||
[description appendFormat:@"\n%@: %@", header, [_headers objectForKey:header]];
|
[description appendFormat:@"\n%@: %@", header, [_additionalHeaders objectForKey:header]];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return description;
|
return description;
|
||||||
@@ -283,11 +259,11 @@
|
|||||||
@implementation GCDWebServerResponse (Extensions)
|
@implementation GCDWebServerResponse (Extensions)
|
||||||
|
|
||||||
+ (instancetype)responseWithStatusCode:(NSInteger)statusCode {
|
+ (instancetype)responseWithStatusCode:(NSInteger)statusCode {
|
||||||
return ARC_AUTORELEASE([[self alloc] initWithStatusCode:statusCode]);
|
return [(GCDWebServerResponse*)[self alloc] initWithStatusCode:statusCode];
|
||||||
}
|
}
|
||||||
|
|
||||||
+ (instancetype)responseWithRedirect:(NSURL*)location permanent:(BOOL)permanent {
|
+ (instancetype)responseWithRedirect:(NSURL*)location permanent:(BOOL)permanent {
|
||||||
return ARC_AUTORELEASE([[self alloc] initWithRedirect:location permanent:permanent]);
|
return [(GCDWebServerResponse*)[self alloc] initWithRedirect:location permanent:permanent];
|
||||||
}
|
}
|
||||||
|
|
||||||
- (instancetype)initWithStatusCode:(NSInteger)statusCode {
|
- (instancetype)initWithStatusCode:(NSInteger)statusCode {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
Copyright (c) 2012-2014, Pierre-Olivier Latour
|
Copyright (c) 2012-2019, Pierre-Olivier Latour
|
||||||
All rights reserved.
|
All rights reserved.
|
||||||
|
|
||||||
Redistribution and use in source and binary forms, with or without
|
Redistribution and use in source and binary forms, with or without
|
||||||
@@ -27,11 +27,38 @@
|
|||||||
|
|
||||||
#import "GCDWebServerRequest.h"
|
#import "GCDWebServerRequest.h"
|
||||||
|
|
||||||
|
NS_ASSUME_NONNULL_BEGIN
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The GCDWebServerDataRequest subclass of GCDWebServerRequest stores the body
|
||||||
|
* of the HTTP request in memory.
|
||||||
|
*/
|
||||||
@interface GCDWebServerDataRequest : GCDWebServerRequest
|
@interface GCDWebServerDataRequest : GCDWebServerRequest
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the data for the request body.
|
||||||
|
*/
|
||||||
@property(nonatomic, readonly) NSData* data;
|
@property(nonatomic, readonly) NSData* data;
|
||||||
|
|
||||||
@end
|
@end
|
||||||
|
|
||||||
@interface GCDWebServerDataRequest (Extensions)
|
@interface GCDWebServerDataRequest (Extensions)
|
||||||
@property(nonatomic, readonly) NSString* text; // Text encoding is extracted from Content-Type or defaults to UTF-8 - Returns nil on error
|
|
||||||
@property(nonatomic, readonly) id jsonObject; // Returns nil on error
|
/**
|
||||||
|
* Returns the data for the request body interpreted as text. If the content
|
||||||
|
* type of the body is not a text one, or if an error occurs, nil is returned.
|
||||||
|
*
|
||||||
|
* The text encoding used to interpret the data is extracted from the
|
||||||
|
* "Content-Type" header or defaults to UTF-8.
|
||||||
|
*/
|
||||||
|
@property(nonatomic, readonly, nullable) NSString* text;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the data for the request body interpreted as a JSON object. If the
|
||||||
|
* content type of the body is not JSON, or if an error occurs, nil is returned.
|
||||||
|
*/
|
||||||
|
@property(nonatomic, readonly, nullable) id jsonObject;
|
||||||
|
|
||||||
@end
|
@end
|
||||||
|
|
||||||
|
NS_ASSUME_NONNULL_END
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
Copyright (c) 2012-2014, Pierre-Olivier Latour
|
Copyright (c) 2012-2019, Pierre-Olivier Latour
|
||||||
All rights reserved.
|
All rights reserved.
|
||||||
|
|
||||||
Redistribution and use in source and binary forms, with or without
|
Redistribution and use in source and binary forms, with or without
|
||||||
@@ -25,37 +25,31 @@
|
|||||||
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
#if !__has_feature(objc_arc)
|
||||||
|
#error GCDWebServer requires ARC
|
||||||
|
#endif
|
||||||
|
|
||||||
#import "GCDWebServerPrivate.h"
|
#import "GCDWebServerPrivate.h"
|
||||||
|
|
||||||
@interface GCDWebServerDataRequest () {
|
@interface GCDWebServerDataRequest ()
|
||||||
@private
|
@property(nonatomic) NSMutableData* data;
|
||||||
NSMutableData* _data;
|
@end
|
||||||
|
|
||||||
|
@implementation GCDWebServerDataRequest {
|
||||||
NSString* _text;
|
NSString* _text;
|
||||||
id _jsonObject;
|
id _jsonObject;
|
||||||
}
|
}
|
||||||
@end
|
|
||||||
|
|
||||||
@implementation GCDWebServerDataRequest
|
|
||||||
|
|
||||||
@synthesize data=_data;
|
|
||||||
|
|
||||||
- (void)dealloc {
|
|
||||||
ARC_RELEASE(_data);
|
|
||||||
ARC_RELEASE(_text);
|
|
||||||
ARC_RELEASE(_jsonObject);
|
|
||||||
|
|
||||||
ARC_DEALLOC(super);
|
|
||||||
}
|
|
||||||
|
|
||||||
- (BOOL)open:(NSError**)error {
|
- (BOOL)open:(NSError**)error {
|
||||||
if (self.contentLength != NSNotFound) {
|
if (self.contentLength != NSUIntegerMax) {
|
||||||
_data = [[NSMutableData alloc] initWithCapacity:self.contentLength];
|
_data = [[NSMutableData alloc] initWithCapacity:self.contentLength];
|
||||||
} else {
|
} else {
|
||||||
_data = [[NSMutableData alloc] init];
|
_data = [[NSMutableData alloc] init];
|
||||||
}
|
}
|
||||||
if (_data == nil) {
|
if (_data == nil) {
|
||||||
*error = [NSError errorWithDomain:kGCDWebServerErrorDomain code:-1 userInfo:@{NSLocalizedDescriptionKey: @"Failed allocating memory"}];
|
if (error) {
|
||||||
|
*error = [NSError errorWithDomain:kGCDWebServerErrorDomain code:-1 userInfo:@{NSLocalizedDescriptionKey : @"Failed allocating memory"}];
|
||||||
|
}
|
||||||
return NO;
|
return NO;
|
||||||
}
|
}
|
||||||
return YES;
|
return YES;
|
||||||
@@ -74,7 +68,7 @@
|
|||||||
NSMutableString* description = [NSMutableString stringWithString:[super description]];
|
NSMutableString* description = [NSMutableString stringWithString:[super description]];
|
||||||
if (_data) {
|
if (_data) {
|
||||||
[description appendString:@"\n\n"];
|
[description appendString:@"\n\n"];
|
||||||
[description appendString:GCDWebServerDescribeData(_data, self.contentType)];
|
[description appendString:GCDWebServerDescribeData(_data, (NSString*)self.contentType)];
|
||||||
}
|
}
|
||||||
return description;
|
return description;
|
||||||
}
|
}
|
||||||
@@ -89,7 +83,7 @@
|
|||||||
NSString* charset = GCDWebServerExtractHeaderValueParameter(self.contentType, @"charset");
|
NSString* charset = GCDWebServerExtractHeaderValueParameter(self.contentType, @"charset");
|
||||||
_text = [[NSString alloc] initWithData:self.data encoding:GCDWebServerStringEncodingFromCharset(charset)];
|
_text = [[NSString alloc] initWithData:self.data encoding:GCDWebServerStringEncodingFromCharset(charset)];
|
||||||
} else {
|
} else {
|
||||||
DNOT_REACHED();
|
GWS_DNOT_REACHED();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return _text;
|
return _text;
|
||||||
@@ -97,10 +91,11 @@
|
|||||||
|
|
||||||
- (id)jsonObject {
|
- (id)jsonObject {
|
||||||
if (_jsonObject == nil) {
|
if (_jsonObject == nil) {
|
||||||
if ([self.contentType isEqualToString:@"application/json"] || [self.contentType isEqualToString:@"text/json"] || [self.contentType isEqualToString:@"text/javascript"]) {
|
NSString* mimeType = GCDWebServerTruncateHeaderValue(self.contentType);
|
||||||
_jsonObject = ARC_RETAIN([NSJSONSerialization JSONObjectWithData:_data options:0 error:NULL]);
|
if ([mimeType isEqualToString:@"application/json"] || [mimeType isEqualToString:@"text/json"] || [mimeType isEqualToString:@"text/javascript"]) {
|
||||||
|
_jsonObject = [NSJSONSerialization JSONObjectWithData:_data options:0 error:NULL];
|
||||||
} else {
|
} else {
|
||||||
DNOT_REACHED();
|
GWS_DNOT_REACHED();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return _jsonObject;
|
return _jsonObject;
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
Copyright (c) 2012-2014, Pierre-Olivier Latour
|
Copyright (c) 2012-2019, Pierre-Olivier Latour
|
||||||
All rights reserved.
|
All rights reserved.
|
||||||
|
|
||||||
Redistribution and use in source and binary forms, with or without
|
Redistribution and use in source and binary forms, with or without
|
||||||
@@ -27,6 +27,23 @@
|
|||||||
|
|
||||||
#import "GCDWebServerRequest.h"
|
#import "GCDWebServerRequest.h"
|
||||||
|
|
||||||
|
NS_ASSUME_NONNULL_BEGIN
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The GCDWebServerFileRequest subclass of GCDWebServerRequest stores the body
|
||||||
|
* of the HTTP request to a file on disk.
|
||||||
|
*/
|
||||||
@interface GCDWebServerFileRequest : GCDWebServerRequest
|
@interface GCDWebServerFileRequest : GCDWebServerRequest
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the path to the temporary file containing the request body.
|
||||||
|
*
|
||||||
|
* @warning This temporary file will be automatically deleted when the
|
||||||
|
* GCDWebServerFileRequest is deallocated. If you want to preserve this file,
|
||||||
|
* you must move it to a different location beforehand.
|
||||||
|
*/
|
||||||
@property(nonatomic, readonly) NSString* temporaryPath;
|
@property(nonatomic, readonly) NSString* temporaryPath;
|
||||||
|
|
||||||
@end
|
@end
|
||||||
|
|
||||||
|
NS_ASSUME_NONNULL_END
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
Copyright (c) 2012-2014, Pierre-Olivier Latour
|
Copyright (c) 2012-2019, Pierre-Olivier Latour
|
||||||
All rights reserved.
|
All rights reserved.
|
||||||
|
|
||||||
Redistribution and use in source and binary forms, with or without
|
Redistribution and use in source and binary forms, with or without
|
||||||
@@ -25,41 +25,33 @@
|
|||||||
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
#if !__has_feature(objc_arc)
|
||||||
|
#error GCDWebServer requires ARC
|
||||||
|
#endif
|
||||||
|
|
||||||
#import "GCDWebServerPrivate.h"
|
#import "GCDWebServerPrivate.h"
|
||||||
|
|
||||||
@interface GCDWebServerFileRequest () {
|
@implementation GCDWebServerFileRequest {
|
||||||
@private
|
|
||||||
NSString* _temporaryPath;
|
|
||||||
int _file;
|
int _file;
|
||||||
}
|
}
|
||||||
@end
|
|
||||||
|
|
||||||
static inline NSError* _MakePosixError(int code) {
|
- (instancetype)initWithMethod:(NSString*)method url:(NSURL*)url headers:(NSDictionary<NSString*, NSString*>*)headers path:(NSString*)path query:(NSDictionary<NSString*, NSString*>*)query {
|
||||||
return [NSError errorWithDomain:NSPOSIXErrorDomain code:code userInfo:@{NSLocalizedDescriptionKey: [NSString stringWithFormat:@"%s", strerror(code)]}];
|
|
||||||
}
|
|
||||||
|
|
||||||
@implementation GCDWebServerFileRequest
|
|
||||||
|
|
||||||
@synthesize temporaryPath=_temporaryPath;
|
|
||||||
|
|
||||||
- (instancetype)initWithMethod:(NSString*)method url:(NSURL*)url headers:(NSDictionary*)headers path:(NSString*)path query:(NSDictionary*)query {
|
|
||||||
if ((self = [super initWithMethod:method url:url headers:headers path:path query:query])) {
|
if ((self = [super initWithMethod:method url:url headers:headers path:path query:query])) {
|
||||||
_temporaryPath = ARC_RETAIN([NSTemporaryDirectory() stringByAppendingPathComponent:[[NSProcessInfo processInfo] globallyUniqueString]]);
|
_temporaryPath = [NSTemporaryDirectory() stringByAppendingPathComponent:[[NSProcessInfo processInfo] globallyUniqueString]];
|
||||||
}
|
}
|
||||||
return self;
|
return self;
|
||||||
}
|
}
|
||||||
|
|
||||||
- (void)dealloc {
|
- (void)dealloc {
|
||||||
unlink([_temporaryPath fileSystemRepresentation]);
|
unlink([_temporaryPath fileSystemRepresentation]);
|
||||||
ARC_RELEASE(_temporaryPath);
|
|
||||||
|
|
||||||
ARC_DEALLOC(super);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
- (BOOL)open:(NSError**)error {
|
- (BOOL)open:(NSError**)error {
|
||||||
_file = open([_temporaryPath fileSystemRepresentation], O_CREAT | O_TRUNC | O_WRONLY, S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH);
|
_file = open([_temporaryPath fileSystemRepresentation], O_CREAT | O_TRUNC | O_WRONLY, S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH);
|
||||||
if (_file <= 0) {
|
if (_file <= 0) {
|
||||||
*error = _MakePosixError(errno);
|
if (error) {
|
||||||
|
*error = GCDWebServerMakePosixError(errno);
|
||||||
|
}
|
||||||
return NO;
|
return NO;
|
||||||
}
|
}
|
||||||
return YES;
|
return YES;
|
||||||
@@ -67,7 +59,9 @@ static inline NSError* _MakePosixError(int code) {
|
|||||||
|
|
||||||
- (BOOL)writeData:(NSData*)data error:(NSError**)error {
|
- (BOOL)writeData:(NSData*)data error:(NSError**)error {
|
||||||
if (write(_file, data.bytes, data.length) != (ssize_t)data.length) {
|
if (write(_file, data.bytes, data.length) != (ssize_t)data.length) {
|
||||||
*error = _MakePosixError(errno);
|
if (error) {
|
||||||
|
*error = GCDWebServerMakePosixError(errno);
|
||||||
|
}
|
||||||
return NO;
|
return NO;
|
||||||
}
|
}
|
||||||
return YES;
|
return YES;
|
||||||
@@ -75,21 +69,23 @@ static inline NSError* _MakePosixError(int code) {
|
|||||||
|
|
||||||
- (BOOL)close:(NSError**)error {
|
- (BOOL)close:(NSError**)error {
|
||||||
if (close(_file) < 0) {
|
if (close(_file) < 0) {
|
||||||
*error = _MakePosixError(errno);
|
if (error) {
|
||||||
|
*error = GCDWebServerMakePosixError(errno);
|
||||||
|
}
|
||||||
return NO;
|
return NO;
|
||||||
}
|
}
|
||||||
#ifdef __GCDWEBSERVER_ENABLE_TESTING__
|
#ifdef __GCDWEBSERVER_ENABLE_TESTING__
|
||||||
NSString* creationDateHeader = [self.headers objectForKey:@"X-GCDWebServer-CreationDate"];
|
NSString* creationDateHeader = [self.headers objectForKey:@"X-GCDWebServer-CreationDate"];
|
||||||
if (creationDateHeader) {
|
if (creationDateHeader) {
|
||||||
NSDate* date = GCDWebServerParseISO8601(creationDateHeader);
|
NSDate* date = GCDWebServerParseISO8601(creationDateHeader);
|
||||||
if (!date || ![[NSFileManager defaultManager] setAttributes:@{NSFileCreationDate: date} ofItemAtPath:_temporaryPath error:error]) {
|
if (!date || ![[NSFileManager defaultManager] setAttributes:@{NSFileCreationDate : date} ofItemAtPath:_temporaryPath error:error]) {
|
||||||
return NO;
|
return NO;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
NSString* modifiedDateHeader = [self.headers objectForKey:@"X-GCDWebServer-ModifiedDate"];
|
NSString* modifiedDateHeader = [self.headers objectForKey:@"X-GCDWebServer-ModifiedDate"];
|
||||||
if (modifiedDateHeader) {
|
if (modifiedDateHeader) {
|
||||||
NSDate* date = GCDWebServerParseRFC822(modifiedDateHeader);
|
NSDate* date = GCDWebServerParseRFC822(modifiedDateHeader);
|
||||||
if (!date || ![[NSFileManager defaultManager] setAttributes:@{NSFileModificationDate: date} ofItemAtPath:_temporaryPath error:error]) {
|
if (!date || ![[NSFileManager defaultManager] setAttributes:@{NSFileModificationDate : date} ofItemAtPath:_temporaryPath error:error]) {
|
||||||
return NO;
|
return NO;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
Copyright (c) 2012-2014, Pierre-Olivier Latour
|
Copyright (c) 2012-2019, Pierre-Olivier Latour
|
||||||
All rights reserved.
|
All rights reserved.
|
||||||
|
|
||||||
Redistribution and use in source and binary forms, with or without
|
Redistribution and use in source and binary forms, with or without
|
||||||
@@ -27,23 +27,110 @@
|
|||||||
|
|
||||||
#import "GCDWebServerRequest.h"
|
#import "GCDWebServerRequest.h"
|
||||||
|
|
||||||
|
NS_ASSUME_NONNULL_BEGIN
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The GCDWebServerMultiPart class is an abstract class that wraps the content
|
||||||
|
* of a part.
|
||||||
|
*/
|
||||||
@interface GCDWebServerMultiPart : NSObject
|
@interface GCDWebServerMultiPart : NSObject
|
||||||
@property(nonatomic, readonly) NSString* contentType; // Defaults to "text/plain" per specification if undefined
|
|
||||||
|
/**
|
||||||
|
* Returns the control name retrieved from the part headers.
|
||||||
|
*/
|
||||||
|
@property(nonatomic, readonly) NSString* controlName;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the content type retrieved from the part headers or "text/plain"
|
||||||
|
* if not available (per HTTP specifications).
|
||||||
|
*/
|
||||||
|
@property(nonatomic, readonly) NSString* contentType;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the MIME type component of the content type for the part.
|
||||||
|
*/
|
||||||
@property(nonatomic, readonly) NSString* mimeType;
|
@property(nonatomic, readonly) NSString* mimeType;
|
||||||
|
|
||||||
@end
|
@end
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The GCDWebServerMultiPartArgument subclass of GCDWebServerMultiPart wraps
|
||||||
|
* the content of a part as data in memory.
|
||||||
|
*/
|
||||||
@interface GCDWebServerMultiPartArgument : GCDWebServerMultiPart
|
@interface GCDWebServerMultiPartArgument : GCDWebServerMultiPart
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the data for the part.
|
||||||
|
*/
|
||||||
@property(nonatomic, readonly) NSData* data;
|
@property(nonatomic, readonly) NSData* data;
|
||||||
@property(nonatomic, readonly) NSString* string; // May be nil (only valid for text mime types)
|
|
||||||
|
/**
|
||||||
|
* Returns the data for the part interpreted as text. If the content
|
||||||
|
* type of the part is not a text one, or if an error occurs, nil is returned.
|
||||||
|
*
|
||||||
|
* The text encoding used to interpret the data is extracted from the
|
||||||
|
* "Content-Type" header or defaults to UTF-8.
|
||||||
|
*/
|
||||||
|
@property(nonatomic, readonly, nullable) NSString* string;
|
||||||
|
|
||||||
@end
|
@end
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The GCDWebServerMultiPartFile subclass of GCDWebServerMultiPart wraps
|
||||||
|
* the content of a part as a file on disk.
|
||||||
|
*/
|
||||||
@interface GCDWebServerMultiPartFile : GCDWebServerMultiPart
|
@interface GCDWebServerMultiPartFile : GCDWebServerMultiPart
|
||||||
@property(nonatomic, readonly) NSString* fileName; // May be nil
|
|
||||||
|
/**
|
||||||
|
* Returns the file name retrieved from the part headers.
|
||||||
|
*/
|
||||||
|
@property(nonatomic, readonly) NSString* fileName;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the path to the temporary file containing the part data.
|
||||||
|
*
|
||||||
|
* @warning This temporary file will be automatically deleted when the
|
||||||
|
* GCDWebServerMultiPartFile is deallocated. If you want to preserve this file,
|
||||||
|
* you must move it to a different location beforehand.
|
||||||
|
*/
|
||||||
@property(nonatomic, readonly) NSString* temporaryPath;
|
@property(nonatomic, readonly) NSString* temporaryPath;
|
||||||
|
|
||||||
@end
|
@end
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The GCDWebServerMultiPartFormRequest subclass of GCDWebServerRequest
|
||||||
|
* parses the body of the HTTP request as a multipart encoded form.
|
||||||
|
*/
|
||||||
@interface GCDWebServerMultiPartFormRequest : GCDWebServerRequest
|
@interface GCDWebServerMultiPartFormRequest : GCDWebServerRequest
|
||||||
@property(nonatomic, readonly) NSDictionary* arguments;
|
|
||||||
@property(nonatomic, readonly) NSDictionary* files;
|
/**
|
||||||
|
* Returns the argument parts from the multipart encoded form as
|
||||||
|
* name / GCDWebServerMultiPartArgument pairs.
|
||||||
|
*/
|
||||||
|
@property(nonatomic, readonly) NSArray<GCDWebServerMultiPartArgument*>* arguments;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the files parts from the multipart encoded form as
|
||||||
|
* name / GCDWebServerMultiPartFile pairs.
|
||||||
|
*/
|
||||||
|
@property(nonatomic, readonly) NSArray<GCDWebServerMultiPartFile*>* files;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the MIME type for multipart encoded forms
|
||||||
|
* i.e. "multipart/form-data".
|
||||||
|
*/
|
||||||
+ (NSString*)mimeType;
|
+ (NSString*)mimeType;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the first argument for a given control name or nil if not found.
|
||||||
|
*/
|
||||||
|
- (nullable GCDWebServerMultiPartArgument*)firstArgumentForControlName:(NSString*)name;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the first file for a given control name or nil if not found.
|
||||||
|
*/
|
||||||
|
- (nullable GCDWebServerMultiPartFile*)firstFileForControlName:(NSString*)name;
|
||||||
|
|
||||||
@end
|
@end
|
||||||
|
|
||||||
|
NS_ASSUME_NONNULL_END
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
Copyright (c) 2012-2014, Pierre-Olivier Latour
|
Copyright (c) 2012-2019, Pierre-Olivier Latour
|
||||||
All rights reserved.
|
All rights reserved.
|
||||||
|
|
||||||
Redistribution and use in source and binary forms, with or without
|
Redistribution and use in source and binary forms, with or without
|
||||||
@@ -25,65 +25,48 @@
|
|||||||
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
#if !__has_feature(objc_arc)
|
||||||
|
#error GCDWebServer requires ARC
|
||||||
|
#endif
|
||||||
|
|
||||||
#import "GCDWebServerPrivate.h"
|
#import "GCDWebServerPrivate.h"
|
||||||
|
|
||||||
#define kMultiPartBufferSize (256 * 1024)
|
#define kMultiPartBufferSize (256 * 1024)
|
||||||
|
|
||||||
enum {
|
typedef enum {
|
||||||
kParserState_Undefined = 0,
|
kParserState_Undefined = 0,
|
||||||
kParserState_Start,
|
kParserState_Start,
|
||||||
kParserState_Headers,
|
kParserState_Headers,
|
||||||
kParserState_Content,
|
kParserState_Content,
|
||||||
kParserState_End
|
kParserState_End
|
||||||
};
|
} ParserState;
|
||||||
|
|
||||||
|
@interface GCDWebServerMIMEStreamParser : NSObject
|
||||||
|
@end
|
||||||
|
|
||||||
static NSData* _newlineData = nil;
|
static NSData* _newlineData = nil;
|
||||||
static NSData* _newlinesData = nil;
|
static NSData* _newlinesData = nil;
|
||||||
static NSData* _dashNewlineData = nil;
|
static NSData* _dashNewlineData = nil;
|
||||||
|
|
||||||
@interface GCDWebServerMultiPart () {
|
|
||||||
@private
|
|
||||||
NSString* _contentType;
|
|
||||||
NSString* _mimeType;
|
|
||||||
}
|
|
||||||
@end
|
|
||||||
|
|
||||||
@implementation GCDWebServerMultiPart
|
@implementation GCDWebServerMultiPart
|
||||||
|
|
||||||
@synthesize contentType=_contentType, mimeType=_mimeType;
|
- (instancetype)initWithControlName:(NSString* _Nonnull)name contentType:(NSString* _Nonnull)type {
|
||||||
|
|
||||||
- (id)initWithContentType:(NSString*)contentType {
|
|
||||||
if ((self = [super init])) {
|
if ((self = [super init])) {
|
||||||
_contentType = [contentType copy];
|
_controlName = [name copy];
|
||||||
_mimeType = ARC_RETAIN(GCDWebServerTruncateHeaderValue(_contentType));
|
_contentType = [type copy];
|
||||||
|
_mimeType = (NSString*)GCDWebServerTruncateHeaderValue(_contentType);
|
||||||
}
|
}
|
||||||
return self;
|
return self;
|
||||||
}
|
}
|
||||||
|
|
||||||
- (void)dealloc {
|
|
||||||
ARC_RELEASE(_contentType);
|
|
||||||
ARC_RELEASE(_mimeType);
|
|
||||||
|
|
||||||
ARC_DEALLOC(super);
|
|
||||||
}
|
|
||||||
|
|
||||||
@end
|
|
||||||
|
|
||||||
@interface GCDWebServerMultiPartArgument () {
|
|
||||||
@private
|
|
||||||
NSData* _data;
|
|
||||||
NSString* _string;
|
|
||||||
}
|
|
||||||
@end
|
@end
|
||||||
|
|
||||||
@implementation GCDWebServerMultiPartArgument
|
@implementation GCDWebServerMultiPartArgument
|
||||||
|
|
||||||
@synthesize data=_data, string=_string;
|
- (instancetype)initWithControlName:(NSString* _Nonnull)name contentType:(NSString* _Nonnull)type data:(NSData* _Nonnull)data {
|
||||||
|
if ((self = [super initWithControlName:name contentType:type])) {
|
||||||
|
_data = data;
|
||||||
|
|
||||||
- (id)initWithContentType:(NSString*)contentType data:(NSData*)data {
|
|
||||||
if ((self = [super initWithContentType:contentType])) {
|
|
||||||
_data = ARC_RETAIN(data);
|
|
||||||
|
|
||||||
if ([self.contentType hasPrefix:@"text/"]) {
|
if ([self.contentType hasPrefix:@"text/"]) {
|
||||||
NSString* charset = GCDWebServerExtractHeaderValueParameter(self.contentType, @"charset");
|
NSString* charset = GCDWebServerExtractHeaderValueParameter(self.contentType, @"charset");
|
||||||
_string = [[NSString alloc] initWithData:_data encoding:GCDWebServerStringEncodingFromCharset(charset)];
|
_string = [[NSString alloc] initWithData:_data encoding:GCDWebServerStringEncodingFromCharset(charset)];
|
||||||
@@ -92,32 +75,16 @@ static NSData* _dashNewlineData = nil;
|
|||||||
return self;
|
return self;
|
||||||
}
|
}
|
||||||
|
|
||||||
- (void)dealloc {
|
|
||||||
ARC_RELEASE(_data);
|
|
||||||
ARC_RELEASE(_string);
|
|
||||||
|
|
||||||
ARC_DEALLOC(super);
|
|
||||||
}
|
|
||||||
|
|
||||||
- (NSString*)description {
|
- (NSString*)description {
|
||||||
return [NSString stringWithFormat:@"<%@ | '%@' | %lu bytes>", [self class], self.mimeType, (unsigned long)_data.length];
|
return [NSString stringWithFormat:@"<%@ | '%@' | %lu bytes>", [self class], self.mimeType, (unsigned long)_data.length];
|
||||||
}
|
}
|
||||||
|
|
||||||
@end
|
@end
|
||||||
|
|
||||||
@interface GCDWebServerMultiPartFile () {
|
|
||||||
@private
|
|
||||||
NSString* _fileName;
|
|
||||||
NSString* _temporaryPath;
|
|
||||||
}
|
|
||||||
@end
|
|
||||||
|
|
||||||
@implementation GCDWebServerMultiPartFile
|
@implementation GCDWebServerMultiPartFile
|
||||||
|
|
||||||
@synthesize fileName=_fileName, temporaryPath=_temporaryPath;
|
- (instancetype)initWithControlName:(NSString* _Nonnull)name contentType:(NSString* _Nonnull)type fileName:(NSString* _Nonnull)fileName temporaryPath:(NSString* _Nonnull)temporaryPath {
|
||||||
|
if ((self = [super initWithControlName:name contentType:type])) {
|
||||||
- (id)initWithContentType:(NSString*)contentType fileName:(NSString*)fileName temporaryPath:(NSString*)temporaryPath {
|
|
||||||
if ((self = [super initWithContentType:contentType])) {
|
|
||||||
_fileName = [fileName copy];
|
_fileName = [fileName copy];
|
||||||
_temporaryPath = [temporaryPath copy];
|
_temporaryPath = [temporaryPath copy];
|
||||||
}
|
}
|
||||||
@@ -126,11 +93,6 @@ static NSData* _dashNewlineData = nil;
|
|||||||
|
|
||||||
- (void)dealloc {
|
- (void)dealloc {
|
||||||
unlink([_temporaryPath fileSystemRepresentation]);
|
unlink([_temporaryPath fileSystemRepresentation]);
|
||||||
|
|
||||||
ARC_RELEASE(_fileName);
|
|
||||||
ARC_RELEASE(_temporaryPath);
|
|
||||||
|
|
||||||
ARC_DEALLOC(super);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
- (NSString*)description {
|
- (NSString*)description {
|
||||||
@@ -139,247 +101,302 @@ static NSData* _dashNewlineData = nil;
|
|||||||
|
|
||||||
@end
|
@end
|
||||||
|
|
||||||
@interface GCDWebServerMultiPartFormRequest () {
|
@implementation GCDWebServerMIMEStreamParser {
|
||||||
@private
|
|
||||||
NSData* _boundary;
|
NSData* _boundary;
|
||||||
|
NSString* _defaultcontrolName;
|
||||||
NSUInteger _parserState;
|
ParserState _state;
|
||||||
NSMutableData* _parserData;
|
NSMutableData* _data;
|
||||||
|
NSMutableArray<GCDWebServerMultiPartArgument*>* _arguments;
|
||||||
|
NSMutableArray<GCDWebServerMultiPartFile*>* _files;
|
||||||
|
|
||||||
NSString* _controlName;
|
NSString* _controlName;
|
||||||
NSString* _fileName;
|
NSString* _fileName;
|
||||||
NSString* _contentType;
|
NSString* _contentType;
|
||||||
NSString* _tmpPath;
|
NSString* _tmpPath;
|
||||||
int _tmpFile;
|
int _tmpFile;
|
||||||
|
GCDWebServerMIMEStreamParser* _subParser;
|
||||||
NSMutableDictionary* _arguments;
|
|
||||||
NSMutableDictionary* _files;
|
|
||||||
}
|
}
|
||||||
@end
|
|
||||||
|
|
||||||
@implementation GCDWebServerMultiPartFormRequest
|
|
||||||
|
|
||||||
@synthesize arguments=_arguments, files=_files;
|
|
||||||
|
|
||||||
+ (void)initialize {
|
+ (void)initialize {
|
||||||
if (_newlineData == nil) {
|
if (_newlineData == nil) {
|
||||||
_newlineData = [[NSData alloc] initWithBytes:"\r\n" length:2];
|
_newlineData = [[NSData alloc] initWithBytes:"\r\n" length:2];
|
||||||
DCHECK(_newlineData);
|
GWS_DCHECK(_newlineData);
|
||||||
}
|
}
|
||||||
if (_newlinesData == nil) {
|
if (_newlinesData == nil) {
|
||||||
_newlinesData = [[NSData alloc] initWithBytes:"\r\n\r\n" length:4];
|
_newlinesData = [[NSData alloc] initWithBytes:"\r\n\r\n" length:4];
|
||||||
DCHECK(_newlinesData);
|
GWS_DCHECK(_newlinesData);
|
||||||
}
|
}
|
||||||
if (_dashNewlineData == nil) {
|
if (_dashNewlineData == nil) {
|
||||||
_dashNewlineData = [[NSData alloc] initWithBytes:"--\r\n" length:4];
|
_dashNewlineData = [[NSData alloc] initWithBytes:"--\r\n" length:4];
|
||||||
DCHECK(_dashNewlineData);
|
GWS_DCHECK(_dashNewlineData);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
- (instancetype)initWithBoundary:(NSString* _Nonnull)boundary defaultControlName:(NSString* _Nullable)name arguments:(NSMutableArray<GCDWebServerMultiPartArgument*>* _Nonnull)arguments files:(NSMutableArray<GCDWebServerMultiPartFile*>* _Nonnull)files {
|
||||||
|
NSData* data = boundary.length ? [[NSString stringWithFormat:@"--%@", boundary] dataUsingEncoding:NSASCIIStringEncoding] : nil;
|
||||||
|
if (data == nil) {
|
||||||
|
GWS_DNOT_REACHED();
|
||||||
|
return nil;
|
||||||
|
}
|
||||||
|
if ((self = [super init])) {
|
||||||
|
_boundary = data;
|
||||||
|
_defaultcontrolName = name;
|
||||||
|
_arguments = arguments;
|
||||||
|
_files = files;
|
||||||
|
_data = [[NSMutableData alloc] initWithCapacity:kMultiPartBufferSize];
|
||||||
|
_state = kParserState_Start;
|
||||||
|
}
|
||||||
|
return self;
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)dealloc {
|
||||||
|
if (_tmpFile > 0) {
|
||||||
|
close(_tmpFile);
|
||||||
|
unlink([_tmpPath fileSystemRepresentation]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// http://www.w3.org/TR/html401/interact/forms.html#h-17.13.4.2
|
||||||
|
- (BOOL)_parseData {
|
||||||
|
BOOL success = YES;
|
||||||
|
|
||||||
|
if (_state == kParserState_Headers) {
|
||||||
|
NSRange range = [_data rangeOfData:_newlinesData options:0 range:NSMakeRange(0, _data.length)];
|
||||||
|
if (range.location != NSNotFound) {
|
||||||
|
_controlName = nil;
|
||||||
|
_fileName = nil;
|
||||||
|
_contentType = nil;
|
||||||
|
_tmpPath = nil;
|
||||||
|
_subParser = nil;
|
||||||
|
NSString* headers = [[NSString alloc] initWithData:[_data subdataWithRange:NSMakeRange(0, range.location)] encoding:NSUTF8StringEncoding];
|
||||||
|
if (headers) {
|
||||||
|
for (NSString* header in [headers componentsSeparatedByString:@"\r\n"]) {
|
||||||
|
NSRange subRange = [header rangeOfString:@":"];
|
||||||
|
if (subRange.location != NSNotFound) {
|
||||||
|
NSString* name = [header substringToIndex:subRange.location];
|
||||||
|
NSString* value = [[header substringFromIndex:(subRange.location + subRange.length)] stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]];
|
||||||
|
if ([name caseInsensitiveCompare:@"Content-Type"] == NSOrderedSame) {
|
||||||
|
_contentType = GCDWebServerNormalizeHeaderValue(value);
|
||||||
|
} else if ([name caseInsensitiveCompare:@"Content-Disposition"] == NSOrderedSame) {
|
||||||
|
NSString* contentDisposition = GCDWebServerNormalizeHeaderValue(value);
|
||||||
|
if ([GCDWebServerTruncateHeaderValue(contentDisposition) isEqualToString:@"form-data"]) {
|
||||||
|
_controlName = GCDWebServerExtractHeaderValueParameter(contentDisposition, @"name");
|
||||||
|
_fileName = GCDWebServerExtractHeaderValueParameter(contentDisposition, @"filename");
|
||||||
|
} else if ([GCDWebServerTruncateHeaderValue(contentDisposition) isEqualToString:@"file"]) {
|
||||||
|
_controlName = _defaultcontrolName;
|
||||||
|
_fileName = GCDWebServerExtractHeaderValueParameter(contentDisposition, @"filename");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
GWS_DNOT_REACHED();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (_contentType == nil) {
|
||||||
|
_contentType = @"text/plain";
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
GWS_LOG_ERROR(@"Failed decoding headers in part of 'multipart/form-data'");
|
||||||
|
GWS_DNOT_REACHED();
|
||||||
|
}
|
||||||
|
if (_controlName) {
|
||||||
|
if ([GCDWebServerTruncateHeaderValue(_contentType) isEqualToString:@"multipart/mixed"]) {
|
||||||
|
NSString* boundary = GCDWebServerExtractHeaderValueParameter(_contentType, @"boundary");
|
||||||
|
_subParser = [[GCDWebServerMIMEStreamParser alloc] initWithBoundary:boundary defaultControlName:_controlName arguments:_arguments files:_files];
|
||||||
|
if (_subParser == nil) {
|
||||||
|
GWS_DNOT_REACHED();
|
||||||
|
success = NO;
|
||||||
|
}
|
||||||
|
} else if (_fileName) {
|
||||||
|
NSString* path = [NSTemporaryDirectory() stringByAppendingPathComponent:[[NSProcessInfo processInfo] globallyUniqueString]];
|
||||||
|
_tmpFile = open([path fileSystemRepresentation], O_CREAT | O_TRUNC | O_WRONLY, S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH);
|
||||||
|
if (_tmpFile > 0) {
|
||||||
|
_tmpPath = [path copy];
|
||||||
|
} else {
|
||||||
|
GWS_DNOT_REACHED();
|
||||||
|
success = NO;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
GWS_DNOT_REACHED();
|
||||||
|
success = NO;
|
||||||
|
}
|
||||||
|
|
||||||
|
[_data replaceBytesInRange:NSMakeRange(0, range.location + range.length) withBytes:NULL length:0];
|
||||||
|
_state = kParserState_Content;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((_state == kParserState_Start) || (_state == kParserState_Content)) {
|
||||||
|
NSRange range = [_data rangeOfData:_boundary options:0 range:NSMakeRange(0, _data.length)];
|
||||||
|
if (range.location != NSNotFound) {
|
||||||
|
NSRange subRange = NSMakeRange(range.location + range.length, _data.length - range.location - range.length);
|
||||||
|
NSRange subRange1 = [_data rangeOfData:_newlineData options:NSDataSearchAnchored range:subRange];
|
||||||
|
NSRange subRange2 = [_data rangeOfData:_dashNewlineData options:NSDataSearchAnchored range:subRange];
|
||||||
|
if ((subRange1.location != NSNotFound) || (subRange2.location != NSNotFound)) {
|
||||||
|
if (_state == kParserState_Content) {
|
||||||
|
const void* dataBytes = _data.bytes;
|
||||||
|
NSUInteger dataLength = range.location - 2;
|
||||||
|
if (_subParser) {
|
||||||
|
if (![_subParser appendBytes:dataBytes length:(dataLength + 2)] || ![_subParser isAtEnd]) {
|
||||||
|
GWS_DNOT_REACHED();
|
||||||
|
success = NO;
|
||||||
|
}
|
||||||
|
_subParser = nil;
|
||||||
|
} else if (_tmpPath) {
|
||||||
|
ssize_t result = write(_tmpFile, dataBytes, dataLength);
|
||||||
|
if (result == (ssize_t)dataLength) {
|
||||||
|
if (close(_tmpFile) == 0) {
|
||||||
|
_tmpFile = 0;
|
||||||
|
GCDWebServerMultiPartFile* file = [[GCDWebServerMultiPartFile alloc] initWithControlName:_controlName contentType:_contentType fileName:_fileName temporaryPath:_tmpPath];
|
||||||
|
[_files addObject:file];
|
||||||
|
} else {
|
||||||
|
GWS_DNOT_REACHED();
|
||||||
|
success = NO;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
GWS_DNOT_REACHED();
|
||||||
|
success = NO;
|
||||||
|
}
|
||||||
|
_tmpPath = nil;
|
||||||
|
} else {
|
||||||
|
NSData* data = [[NSData alloc] initWithBytes:(void*)dataBytes length:dataLength];
|
||||||
|
GCDWebServerMultiPartArgument* argument = [[GCDWebServerMultiPartArgument alloc] initWithControlName:_controlName contentType:_contentType data:data];
|
||||||
|
[_arguments addObject:argument];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (subRange1.location != NSNotFound) {
|
||||||
|
[_data replaceBytesInRange:NSMakeRange(0, subRange1.location + subRange1.length) withBytes:NULL length:0];
|
||||||
|
_state = kParserState_Headers;
|
||||||
|
success = [self _parseData];
|
||||||
|
} else {
|
||||||
|
_state = kParserState_End;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
NSUInteger margin = 2 * _boundary.length;
|
||||||
|
if (_data.length > margin) {
|
||||||
|
NSUInteger length = _data.length - margin;
|
||||||
|
if (_subParser) {
|
||||||
|
if ([_subParser appendBytes:_data.bytes length:length]) {
|
||||||
|
[_data replaceBytesInRange:NSMakeRange(0, length) withBytes:NULL length:0];
|
||||||
|
} else {
|
||||||
|
GWS_DNOT_REACHED();
|
||||||
|
success = NO;
|
||||||
|
}
|
||||||
|
} else if (_tmpPath) {
|
||||||
|
ssize_t result = write(_tmpFile, _data.bytes, length);
|
||||||
|
if (result == (ssize_t)length) {
|
||||||
|
[_data replaceBytesInRange:NSMakeRange(0, length) withBytes:NULL length:0];
|
||||||
|
} else {
|
||||||
|
GWS_DNOT_REACHED();
|
||||||
|
success = NO;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return success;
|
||||||
|
}
|
||||||
|
|
||||||
|
- (BOOL)appendBytes:(const void*)bytes length:(NSUInteger)length {
|
||||||
|
[_data appendBytes:bytes length:length];
|
||||||
|
return [self _parseData];
|
||||||
|
}
|
||||||
|
|
||||||
|
- (BOOL)isAtEnd {
|
||||||
|
return (_state == kParserState_End);
|
||||||
|
}
|
||||||
|
|
||||||
|
@end
|
||||||
|
|
||||||
|
@interface GCDWebServerMultiPartFormRequest ()
|
||||||
|
@property(nonatomic) NSMutableArray<GCDWebServerMultiPartArgument*>* arguments;
|
||||||
|
@property(nonatomic) NSMutableArray<GCDWebServerMultiPartFile*>* files;
|
||||||
|
@end
|
||||||
|
|
||||||
|
@implementation GCDWebServerMultiPartFormRequest {
|
||||||
|
GCDWebServerMIMEStreamParser* _parser;
|
||||||
|
}
|
||||||
|
|
||||||
+ (NSString*)mimeType {
|
+ (NSString*)mimeType {
|
||||||
return @"multipart/form-data";
|
return @"multipart/form-data";
|
||||||
}
|
}
|
||||||
|
|
||||||
- (instancetype)initWithMethod:(NSString*)method url:(NSURL*)url headers:(NSDictionary*)headers path:(NSString*)path query:(NSDictionary*)query {
|
- (instancetype)initWithMethod:(NSString*)method url:(NSURL*)url headers:(NSDictionary<NSString*, NSString*>*)headers path:(NSString*)path query:(NSDictionary<NSString*, NSString*>*)query {
|
||||||
if ((self = [super initWithMethod:method url:url headers:headers path:path query:query])) {
|
if ((self = [super initWithMethod:method url:url headers:headers path:path query:query])) {
|
||||||
NSString* boundary = GCDWebServerExtractHeaderValueParameter(self.contentType, @"boundary");
|
_arguments = [[NSMutableArray alloc] init];
|
||||||
if (boundary) {
|
_files = [[NSMutableArray alloc] init];
|
||||||
NSData* data = [[NSString stringWithFormat:@"--%@", boundary] dataUsingEncoding:NSASCIIStringEncoding];
|
|
||||||
_boundary = ARC_RETAIN(data);
|
|
||||||
}
|
|
||||||
if (_boundary == nil) {
|
|
||||||
DNOT_REACHED();
|
|
||||||
ARC_RELEASE(self);
|
|
||||||
return nil;
|
|
||||||
}
|
|
||||||
|
|
||||||
_arguments = [[NSMutableDictionary alloc] init];
|
|
||||||
_files = [[NSMutableDictionary alloc] init];
|
|
||||||
}
|
}
|
||||||
return self;
|
return self;
|
||||||
}
|
}
|
||||||
|
|
||||||
- (void)dealloc {
|
|
||||||
ARC_RELEASE(_arguments);
|
|
||||||
ARC_RELEASE(_files);
|
|
||||||
ARC_RELEASE(_boundary);
|
|
||||||
|
|
||||||
ARC_DEALLOC(super);
|
|
||||||
}
|
|
||||||
|
|
||||||
- (BOOL)open:(NSError**)error {
|
- (BOOL)open:(NSError**)error {
|
||||||
_parserData = [[NSMutableData alloc] initWithCapacity:kMultiPartBufferSize];
|
NSString* boundary = GCDWebServerExtractHeaderValueParameter(self.contentType, @"boundary");
|
||||||
_parserState = kParserState_Start;
|
_parser = [[GCDWebServerMIMEStreamParser alloc] initWithBoundary:boundary defaultControlName:nil arguments:_arguments files:_files];
|
||||||
|
if (_parser == nil) {
|
||||||
|
if (error) {
|
||||||
|
*error = [NSError errorWithDomain:kGCDWebServerErrorDomain code:-1 userInfo:@{NSLocalizedDescriptionKey : @"Failed starting to parse multipart form data"}];
|
||||||
|
}
|
||||||
|
return NO;
|
||||||
|
}
|
||||||
return YES;
|
return YES;
|
||||||
}
|
}
|
||||||
|
|
||||||
// http://www.w3.org/TR/html401/interact/forms.html#h-17.13.4.2
|
|
||||||
- (BOOL)_parseData {
|
|
||||||
BOOL success = YES;
|
|
||||||
|
|
||||||
if (_parserState == kParserState_Headers) {
|
|
||||||
NSRange range = [_parserData rangeOfData:_newlinesData options:0 range:NSMakeRange(0, _parserData.length)];
|
|
||||||
if (range.location != NSNotFound) {
|
|
||||||
|
|
||||||
ARC_RELEASE(_controlName);
|
|
||||||
_controlName = nil;
|
|
||||||
ARC_RELEASE(_fileName);
|
|
||||||
_fileName = nil;
|
|
||||||
ARC_RELEASE(_contentType);
|
|
||||||
_contentType = nil;
|
|
||||||
ARC_RELEASE(_tmpPath);
|
|
||||||
_tmpPath = nil;
|
|
||||||
CFHTTPMessageRef message = CFHTTPMessageCreateEmpty(kCFAllocatorDefault, true);
|
|
||||||
const char* temp = "GET / HTTP/1.0\r\n";
|
|
||||||
CFHTTPMessageAppendBytes(message, (const UInt8*)temp, strlen(temp));
|
|
||||||
CFHTTPMessageAppendBytes(message, _parserData.bytes, range.location + range.length);
|
|
||||||
if (CFHTTPMessageIsHeaderComplete(message)) {
|
|
||||||
NSString* controlName = nil;
|
|
||||||
NSString* fileName = nil;
|
|
||||||
NSDictionary* headers = ARC_BRIDGE_RELEASE(CFHTTPMessageCopyAllHeaderFields(message));
|
|
||||||
NSString* contentDisposition = GCDWebServerNormalizeHeaderValue([headers objectForKey:@"Content-Disposition"]);
|
|
||||||
if ([GCDWebServerTruncateHeaderValue(contentDisposition) isEqualToString:@"form-data"]) {
|
|
||||||
controlName = GCDWebServerExtractHeaderValueParameter(contentDisposition, @"name");
|
|
||||||
fileName = GCDWebServerExtractHeaderValueParameter(contentDisposition, @"filename");
|
|
||||||
}
|
|
||||||
_controlName = [controlName copy];
|
|
||||||
_fileName = [fileName copy];
|
|
||||||
_contentType = ARC_RETAIN(GCDWebServerNormalizeHeaderValue([headers objectForKey:@"Content-Type"]));
|
|
||||||
if (_contentType == nil) {
|
|
||||||
_contentType = @"text/plain";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
CFRelease(message);
|
|
||||||
if (_controlName) {
|
|
||||||
if (_fileName) {
|
|
||||||
NSString* path = [NSTemporaryDirectory() stringByAppendingPathComponent:[[NSProcessInfo processInfo] globallyUniqueString]];
|
|
||||||
_tmpFile = open([path fileSystemRepresentation], O_CREAT | O_TRUNC | O_WRONLY, S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH);
|
|
||||||
if (_tmpFile > 0) {
|
|
||||||
_tmpPath = [path copy];
|
|
||||||
} else {
|
|
||||||
DNOT_REACHED();
|
|
||||||
success = NO;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
DNOT_REACHED();
|
|
||||||
success = NO;
|
|
||||||
}
|
|
||||||
|
|
||||||
[_parserData replaceBytesInRange:NSMakeRange(0, range.location + range.length) withBytes:NULL length:0];
|
|
||||||
_parserState = kParserState_Content;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if ((_parserState == kParserState_Start) || (_parserState == kParserState_Content)) {
|
|
||||||
NSRange range = [_parserData rangeOfData:_boundary options:0 range:NSMakeRange(0, _parserData.length)];
|
|
||||||
if (range.location != NSNotFound) {
|
|
||||||
NSRange subRange = NSMakeRange(range.location + range.length, _parserData.length - range.location - range.length);
|
|
||||||
NSRange subRange1 = [_parserData rangeOfData:_newlineData options:NSDataSearchAnchored range:subRange];
|
|
||||||
NSRange subRange2 = [_parserData rangeOfData:_dashNewlineData options:NSDataSearchAnchored range:subRange];
|
|
||||||
if ((subRange1.location != NSNotFound) || (subRange2.location != NSNotFound)) {
|
|
||||||
|
|
||||||
if (_parserState == kParserState_Content) {
|
|
||||||
const void* dataBytes = _parserData.bytes;
|
|
||||||
NSUInteger dataLength = range.location - 2;
|
|
||||||
if (_tmpPath) {
|
|
||||||
ssize_t result = write(_tmpFile, dataBytes, dataLength);
|
|
||||||
if (result == (ssize_t)dataLength) {
|
|
||||||
if (close(_tmpFile) == 0) {
|
|
||||||
_tmpFile = 0;
|
|
||||||
GCDWebServerMultiPartFile* file = [[GCDWebServerMultiPartFile alloc] initWithContentType:_contentType fileName:_fileName temporaryPath:_tmpPath];
|
|
||||||
[_files setObject:file forKey:_controlName];
|
|
||||||
ARC_RELEASE(file);
|
|
||||||
} else {
|
|
||||||
DNOT_REACHED();
|
|
||||||
success = NO;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
DNOT_REACHED();
|
|
||||||
success = NO;
|
|
||||||
}
|
|
||||||
ARC_RELEASE(_tmpPath);
|
|
||||||
_tmpPath = nil;
|
|
||||||
} else {
|
|
||||||
NSData* data = [[NSData alloc] initWithBytes:(void*)dataBytes length:dataLength];
|
|
||||||
GCDWebServerMultiPartArgument* argument = [[GCDWebServerMultiPartArgument alloc] initWithContentType:_contentType data:data];
|
|
||||||
[_arguments setObject:argument forKey:_controlName];
|
|
||||||
ARC_RELEASE(argument);
|
|
||||||
ARC_RELEASE(data);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (subRange1.location != NSNotFound) {
|
|
||||||
[_parserData replaceBytesInRange:NSMakeRange(0, subRange1.location + subRange1.length) withBytes:NULL length:0];
|
|
||||||
_parserState = kParserState_Headers;
|
|
||||||
success = [self _parseData];
|
|
||||||
} else {
|
|
||||||
_parserState = kParserState_End;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
NSUInteger margin = 2 * _boundary.length;
|
|
||||||
if (_tmpPath && (_parserData.length > margin)) {
|
|
||||||
NSUInteger length = _parserData.length - margin;
|
|
||||||
ssize_t result = write(_tmpFile, _parserData.bytes, length);
|
|
||||||
if (result == (ssize_t)length) {
|
|
||||||
[_parserData replaceBytesInRange:NSMakeRange(0, length) withBytes:NULL length:0];
|
|
||||||
} else {
|
|
||||||
DNOT_REACHED();
|
|
||||||
success = NO;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return success;
|
|
||||||
}
|
|
||||||
|
|
||||||
- (BOOL)writeData:(NSData*)data error:(NSError**)error {
|
- (BOOL)writeData:(NSData*)data error:(NSError**)error {
|
||||||
[_parserData appendBytes:data.bytes length:data.length];
|
if (![_parser appendBytes:data.bytes length:data.length]) {
|
||||||
if (![self _parseData]) {
|
if (error) {
|
||||||
*error = [NSError errorWithDomain:kGCDWebServerErrorDomain code:-1 userInfo:@{NSLocalizedDescriptionKey: @"Failed parsing multipart form data"}];
|
*error = [NSError errorWithDomain:kGCDWebServerErrorDomain code:-1 userInfo:@{NSLocalizedDescriptionKey : @"Failed continuing to parse multipart form data"}];
|
||||||
|
}
|
||||||
return NO;
|
return NO;
|
||||||
}
|
}
|
||||||
return YES;
|
return YES;
|
||||||
}
|
}
|
||||||
|
|
||||||
- (BOOL)close:(NSError**)error {
|
- (BOOL)close:(NSError**)error {
|
||||||
ARC_RELEASE(_parserData);
|
BOOL atEnd = [_parser isAtEnd];
|
||||||
_parserData = nil;
|
_parser = nil;
|
||||||
ARC_RELEASE(_controlName);
|
if (!atEnd) {
|
||||||
_controlName = nil;
|
if (error) {
|
||||||
ARC_RELEASE(_fileName);
|
*error = [NSError errorWithDomain:kGCDWebServerErrorDomain code:-1 userInfo:@{NSLocalizedDescriptionKey : @"Failed finishing to parse multipart form data"}];
|
||||||
_fileName = nil;
|
}
|
||||||
ARC_RELEASE(_contentType);
|
|
||||||
_contentType = nil;
|
|
||||||
if (_tmpFile > 0) {
|
|
||||||
close(_tmpFile);
|
|
||||||
unlink([_tmpPath fileSystemRepresentation]);
|
|
||||||
_tmpFile = 0;
|
|
||||||
}
|
|
||||||
ARC_RELEASE(_tmpPath);
|
|
||||||
_tmpPath = nil;
|
|
||||||
if (_parserState != kParserState_End) {
|
|
||||||
*error = [NSError errorWithDomain:kGCDWebServerErrorDomain code:-1 userInfo:@{NSLocalizedDescriptionKey: @"Failed parsing multipart form data"}];
|
|
||||||
return NO;
|
return NO;
|
||||||
}
|
}
|
||||||
return YES;
|
return YES;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
- (GCDWebServerMultiPartArgument*)firstArgumentForControlName:(NSString*)name {
|
||||||
|
for (GCDWebServerMultiPartArgument* argument in _arguments) {
|
||||||
|
if ([argument.controlName isEqualToString:name]) {
|
||||||
|
return argument;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil;
|
||||||
|
}
|
||||||
|
|
||||||
|
- (GCDWebServerMultiPartFile*)firstFileForControlName:(NSString*)name {
|
||||||
|
for (GCDWebServerMultiPartFile* file in _files) {
|
||||||
|
if ([file.controlName isEqualToString:name]) {
|
||||||
|
return file;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil;
|
||||||
|
}
|
||||||
|
|
||||||
- (NSString*)description {
|
- (NSString*)description {
|
||||||
NSMutableString* description = [NSMutableString stringWithString:[super description]];
|
NSMutableString* description = [NSMutableString stringWithString:[super description]];
|
||||||
if (_arguments.count) {
|
if (_arguments.count) {
|
||||||
[description appendString:@"\n"];
|
[description appendString:@"\n"];
|
||||||
for (NSString* key in [[_arguments allKeys] sortedArrayUsingSelector:@selector(compare:)]) {
|
for (GCDWebServerMultiPartArgument* argument in _arguments) {
|
||||||
GCDWebServerMultiPartArgument* argument = [_arguments objectForKey:key];
|
[description appendFormat:@"\n%@ (%@)\n", argument.controlName, argument.contentType];
|
||||||
[description appendFormat:@"\n%@ (%@)\n", key, argument.contentType];
|
|
||||||
[description appendString:GCDWebServerDescribeData(argument.data, argument.contentType)];
|
[description appendString:GCDWebServerDescribeData(argument.data, argument.contentType)];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (_files.count) {
|
if (_files.count) {
|
||||||
[description appendString:@"\n"];
|
[description appendString:@"\n"];
|
||||||
for (NSString* key in [[_files allKeys] sortedArrayUsingSelector:@selector(compare:)]) {
|
for (GCDWebServerMultiPartFile* file in _files) {
|
||||||
GCDWebServerMultiPartFile* file = [_files objectForKey:key];
|
[description appendFormat:@"\n%@ (%@): %@\n{%@}", file.controlName, file.contentType, file.fileName, file.temporaryPath];
|
||||||
[description appendFormat:@"\n%@ (%@): %@\n{%@}", key, file.contentType, file.fileName, file.temporaryPath];
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return description;
|
return description;
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
Copyright (c) 2012-2014, Pierre-Olivier Latour
|
Copyright (c) 2012-2019, Pierre-Olivier Latour
|
||||||
All rights reserved.
|
All rights reserved.
|
||||||
|
|
||||||
Redistribution and use in source and binary forms, with or without
|
Redistribution and use in source and binary forms, with or without
|
||||||
@@ -27,7 +27,29 @@
|
|||||||
|
|
||||||
#import "GCDWebServerDataRequest.h"
|
#import "GCDWebServerDataRequest.h"
|
||||||
|
|
||||||
|
NS_ASSUME_NONNULL_BEGIN
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The GCDWebServerURLEncodedFormRequest subclass of GCDWebServerRequest
|
||||||
|
* parses the body of the HTTP request as a URL encoded form using
|
||||||
|
* GCDWebServerParseURLEncodedForm().
|
||||||
|
*/
|
||||||
@interface GCDWebServerURLEncodedFormRequest : GCDWebServerDataRequest
|
@interface GCDWebServerURLEncodedFormRequest : GCDWebServerDataRequest
|
||||||
@property(nonatomic, readonly) NSDictionary* arguments; // Text encoding is extracted from Content-Type or defaults to UTF-8
|
|
||||||
|
/**
|
||||||
|
* Returns the unescaped control names and values for the URL encoded form.
|
||||||
|
*
|
||||||
|
* The text encoding used to interpret the data is extracted from the
|
||||||
|
* "Content-Type" header or defaults to UTF-8.
|
||||||
|
*/
|
||||||
|
@property(nonatomic, readonly) NSDictionary<NSString*, NSString*>* arguments;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the MIME type for URL encoded forms
|
||||||
|
* i.e. "application/x-www-form-urlencoded".
|
||||||
|
*/
|
||||||
+ (NSString*)mimeType;
|
+ (NSString*)mimeType;
|
||||||
|
|
||||||
@end
|
@end
|
||||||
|
|
||||||
|
NS_ASSUME_NONNULL_END
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
Copyright (c) 2012-2014, Pierre-Olivier Latour
|
Copyright (c) 2012-2019, Pierre-Olivier Latour
|
||||||
All rights reserved.
|
All rights reserved.
|
||||||
|
|
||||||
Redistribution and use in source and binary forms, with or without
|
Redistribution and use in source and binary forms, with or without
|
||||||
@@ -25,39 +25,26 @@
|
|||||||
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
#import "GCDWebServerPrivate.h"
|
#if !__has_feature(objc_arc)
|
||||||
|
#error GCDWebServer requires ARC
|
||||||
|
#endif
|
||||||
|
|
||||||
@interface GCDWebServerURLEncodedFormRequest () {
|
#import "GCDWebServerPrivate.h"
|
||||||
@private
|
|
||||||
NSDictionary* _arguments;
|
|
||||||
}
|
|
||||||
@end
|
|
||||||
|
|
||||||
@implementation GCDWebServerURLEncodedFormRequest
|
@implementation GCDWebServerURLEncodedFormRequest
|
||||||
|
|
||||||
@synthesize arguments=_arguments;
|
|
||||||
|
|
||||||
+ (NSString*)mimeType {
|
+ (NSString*)mimeType {
|
||||||
return @"application/x-www-form-urlencoded";
|
return @"application/x-www-form-urlencoded";
|
||||||
}
|
}
|
||||||
|
|
||||||
- (void)dealloc {
|
|
||||||
ARC_RELEASE(_arguments);
|
|
||||||
|
|
||||||
ARC_DEALLOC(super);
|
|
||||||
}
|
|
||||||
|
|
||||||
- (BOOL)close:(NSError**)error {
|
- (BOOL)close:(NSError**)error {
|
||||||
if (![super close:error]) {
|
if (![super close:error]) {
|
||||||
return NO;
|
return NO;
|
||||||
}
|
}
|
||||||
|
|
||||||
NSString* charset = GCDWebServerExtractHeaderValueParameter(self.contentType, @"charset");
|
NSString* charset = GCDWebServerExtractHeaderValueParameter(self.contentType, @"charset");
|
||||||
NSString* string = [[NSString alloc] initWithData:self.data encoding:GCDWebServerStringEncodingFromCharset(charset)];
|
NSString* string = [[NSString alloc] initWithData:self.data encoding:GCDWebServerStringEncodingFromCharset(charset)];
|
||||||
_arguments = ARC_RETAIN(GCDWebServerParseURLEncodedForm(string));
|
_arguments = GCDWebServerParseURLEncodedForm(string);
|
||||||
DCHECK(_arguments);
|
|
||||||
ARC_RELEASE(string);
|
|
||||||
|
|
||||||
return YES;
|
return YES;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
Copyright (c) 2012-2014, Pierre-Olivier Latour
|
Copyright (c) 2012-2019, Pierre-Olivier Latour
|
||||||
All rights reserved.
|
All rights reserved.
|
||||||
|
|
||||||
Redistribution and use in source and binary forms, with or without
|
Redistribution and use in source and binary forms, with or without
|
||||||
@@ -27,20 +27,87 @@
|
|||||||
|
|
||||||
#import "GCDWebServerResponse.h"
|
#import "GCDWebServerResponse.h"
|
||||||
|
|
||||||
|
NS_ASSUME_NONNULL_BEGIN
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The GCDWebServerDataResponse subclass of GCDWebServerResponse reads the body
|
||||||
|
* of the HTTP response from memory.
|
||||||
|
*/
|
||||||
@interface GCDWebServerDataResponse : GCDWebServerResponse
|
@interface GCDWebServerDataResponse : GCDWebServerResponse
|
||||||
|
@property(nonatomic, copy) NSString* contentType; // Redeclare as non-null
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a response with data in memory and a given content type.
|
||||||
|
*/
|
||||||
+ (instancetype)responseWithData:(NSData*)data contentType:(NSString*)type;
|
+ (instancetype)responseWithData:(NSData*)data contentType:(NSString*)type;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This method is the designated initializer for the class.
|
||||||
|
*/
|
||||||
- (instancetype)initWithData:(NSData*)data contentType:(NSString*)type;
|
- (instancetype)initWithData:(NSData*)data contentType:(NSString*)type;
|
||||||
|
|
||||||
@end
|
@end
|
||||||
|
|
||||||
@interface GCDWebServerDataResponse (Extensions)
|
@interface GCDWebServerDataResponse (Extensions)
|
||||||
+ (instancetype)responseWithText:(NSString*)text;
|
|
||||||
+ (instancetype)responseWithHTML:(NSString*)html;
|
/**
|
||||||
+ (instancetype)responseWithHTMLTemplate:(NSString*)path variables:(NSDictionary*)variables;
|
* Creates a data response from text encoded using UTF-8.
|
||||||
+ (instancetype)responseWithJSONObject:(id)object;
|
*/
|
||||||
+ (instancetype)responseWithJSONObject:(id)object contentType:(NSString*)type;
|
+ (nullable instancetype)responseWithText:(NSString*)text;
|
||||||
- (instancetype)initWithText:(NSString*)text; // Encodes using UTF-8
|
|
||||||
- (instancetype)initWithHTML:(NSString*)html; // Encodes using UTF-8
|
/**
|
||||||
- (instancetype)initWithHTMLTemplate:(NSString*)path variables:(NSDictionary*)variables; // Simple template system that replaces all occurences of "%variable%" with corresponding value (encodes using UTF-8)
|
* Creates a data response from HTML encoded using UTF-8.
|
||||||
- (instancetype)initWithJSONObject:(id)object;
|
*/
|
||||||
- (instancetype)initWithJSONObject:(id)object contentType:(NSString*)type;
|
+ (nullable instancetype)responseWithHTML:(NSString*)html;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a data response from an HTML template encoded using UTF-8.
|
||||||
|
* See -initWithHTMLTemplate:variables: for details.
|
||||||
|
*/
|
||||||
|
+ (nullable instancetype)responseWithHTMLTemplate:(NSString*)path variables:(NSDictionary<NSString*, NSString*>*)variables;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a data response from a serialized JSON object and the default
|
||||||
|
* "application/json" content type.
|
||||||
|
*/
|
||||||
|
+ (nullable instancetype)responseWithJSONObject:(id)object;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a data response from a serialized JSON object and a custom
|
||||||
|
* content type.
|
||||||
|
*/
|
||||||
|
+ (nullable instancetype)responseWithJSONObject:(id)object contentType:(NSString*)type;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initializes a data response from text encoded using UTF-8.
|
||||||
|
*/
|
||||||
|
- (nullable instancetype)initWithText:(NSString*)text;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initializes a data response from HTML encoded using UTF-8.
|
||||||
|
*/
|
||||||
|
- (nullable instancetype)initWithHTML:(NSString*)html;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initializes a data response from an HTML template encoded using UTF-8.
|
||||||
|
*
|
||||||
|
* All occurences of "%variable%" within the HTML template are replaced with
|
||||||
|
* their corresponding values.
|
||||||
|
*/
|
||||||
|
- (nullable instancetype)initWithHTMLTemplate:(NSString*)path variables:(NSDictionary<NSString*, NSString*>*)variables;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initializes a data response from a serialized JSON object and the default
|
||||||
|
* "application/json" content type.
|
||||||
|
*/
|
||||||
|
- (nullable instancetype)initWithJSONObject:(id)object;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initializes a data response from a serialized JSON object and a custom
|
||||||
|
* content type.
|
||||||
|
*/
|
||||||
|
- (nullable instancetype)initWithJSONObject:(id)object contentType:(NSString*)type;
|
||||||
|
|
||||||
@end
|
@end
|
||||||
|
|
||||||
|
NS_ASSUME_NONNULL_END
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
Copyright (c) 2012-2014, Pierre-Olivier Latour
|
Copyright (c) 2012-2019, Pierre-Olivier Latour
|
||||||
All rights reserved.
|
All rights reserved.
|
||||||
|
|
||||||
Redistribution and use in source and binary forms, with or without
|
Redistribution and use in source and binary forms, with or without
|
||||||
@@ -25,43 +25,33 @@
|
|||||||
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
#if !__has_feature(objc_arc)
|
||||||
|
#error GCDWebServer requires ARC
|
||||||
|
#endif
|
||||||
|
|
||||||
#import "GCDWebServerPrivate.h"
|
#import "GCDWebServerPrivate.h"
|
||||||
|
|
||||||
@interface GCDWebServerDataResponse () {
|
@implementation GCDWebServerDataResponse {
|
||||||
@private
|
|
||||||
NSData* _data;
|
NSData* _data;
|
||||||
BOOL _done;
|
BOOL _done;
|
||||||
}
|
}
|
||||||
@end
|
|
||||||
|
|
||||||
@implementation GCDWebServerDataResponse
|
@dynamic contentType;
|
||||||
|
|
||||||
+ (instancetype)responseWithData:(NSData*)data contentType:(NSString*)type {
|
+ (instancetype)responseWithData:(NSData*)data contentType:(NSString*)type {
|
||||||
return ARC_AUTORELEASE([[[self class] alloc] initWithData:data contentType:type]);
|
return [(GCDWebServerDataResponse*)[[self class] alloc] initWithData:data contentType:type];
|
||||||
}
|
}
|
||||||
|
|
||||||
- (instancetype)initWithData:(NSData*)data contentType:(NSString*)type {
|
- (instancetype)initWithData:(NSData*)data contentType:(NSString*)type {
|
||||||
if (data == nil) {
|
|
||||||
DNOT_REACHED();
|
|
||||||
ARC_RELEASE(self);
|
|
||||||
return nil;
|
|
||||||
}
|
|
||||||
|
|
||||||
if ((self = [super init])) {
|
if ((self = [super init])) {
|
||||||
_data = ARC_RETAIN(data);
|
_data = data;
|
||||||
|
|
||||||
self.contentType = type;
|
self.contentType = type;
|
||||||
self.contentLength = data.length;
|
self.contentLength = data.length;
|
||||||
}
|
}
|
||||||
return self;
|
return self;
|
||||||
}
|
}
|
||||||
|
|
||||||
- (void)dealloc {
|
|
||||||
ARC_RELEASE(_data);
|
|
||||||
|
|
||||||
ARC_DEALLOC(super);
|
|
||||||
}
|
|
||||||
|
|
||||||
- (NSData*)readData:(NSError**)error {
|
- (NSData*)readData:(NSError**)error {
|
||||||
NSData* data;
|
NSData* data;
|
||||||
if (_done) {
|
if (_done) {
|
||||||
@@ -85,30 +75,29 @@
|
|||||||
@implementation GCDWebServerDataResponse (Extensions)
|
@implementation GCDWebServerDataResponse (Extensions)
|
||||||
|
|
||||||
+ (instancetype)responseWithText:(NSString*)text {
|
+ (instancetype)responseWithText:(NSString*)text {
|
||||||
return ARC_AUTORELEASE([[self alloc] initWithText:text]);
|
return [(GCDWebServerDataResponse*)[self alloc] initWithText:text];
|
||||||
}
|
}
|
||||||
|
|
||||||
+ (instancetype)responseWithHTML:(NSString*)html {
|
+ (instancetype)responseWithHTML:(NSString*)html {
|
||||||
return ARC_AUTORELEASE([[self alloc] initWithHTML:html]);
|
return [(GCDWebServerDataResponse*)[self alloc] initWithHTML:html];
|
||||||
}
|
}
|
||||||
|
|
||||||
+ (instancetype)responseWithHTMLTemplate:(NSString*)path variables:(NSDictionary*)variables {
|
+ (instancetype)responseWithHTMLTemplate:(NSString*)path variables:(NSDictionary<NSString*, NSString*>*)variables {
|
||||||
return ARC_AUTORELEASE([[self alloc] initWithHTMLTemplate:path variables:variables]);
|
return [(GCDWebServerDataResponse*)[self alloc] initWithHTMLTemplate:path variables:variables];
|
||||||
}
|
}
|
||||||
|
|
||||||
+ (instancetype)responseWithJSONObject:(id)object {
|
+ (instancetype)responseWithJSONObject:(id)object {
|
||||||
return ARC_AUTORELEASE([[self alloc] initWithJSONObject:object]);
|
return [(GCDWebServerDataResponse*)[self alloc] initWithJSONObject:object];
|
||||||
}
|
}
|
||||||
|
|
||||||
+ (instancetype)responseWithJSONObject:(id)object contentType:(NSString*)type {
|
+ (instancetype)responseWithJSONObject:(id)object contentType:(NSString*)type {
|
||||||
return ARC_AUTORELEASE([[self alloc] initWithJSONObject:object contentType:type]);
|
return [(GCDWebServerDataResponse*)[self alloc] initWithJSONObject:object contentType:type];
|
||||||
}
|
}
|
||||||
|
|
||||||
- (instancetype)initWithText:(NSString*)text {
|
- (instancetype)initWithText:(NSString*)text {
|
||||||
NSData* data = [text dataUsingEncoding:NSUTF8StringEncoding];
|
NSData* data = [text dataUsingEncoding:NSUTF8StringEncoding];
|
||||||
if (data == nil) {
|
if (data == nil) {
|
||||||
DNOT_REACHED();
|
GWS_DNOT_REACHED();
|
||||||
ARC_RELEASE(self);
|
|
||||||
return nil;
|
return nil;
|
||||||
}
|
}
|
||||||
return [self initWithData:data contentType:@"text/plain; charset=utf-8"];
|
return [self initWithData:data contentType:@"text/plain; charset=utf-8"];
|
||||||
@@ -117,21 +106,18 @@
|
|||||||
- (instancetype)initWithHTML:(NSString*)html {
|
- (instancetype)initWithHTML:(NSString*)html {
|
||||||
NSData* data = [html dataUsingEncoding:NSUTF8StringEncoding];
|
NSData* data = [html dataUsingEncoding:NSUTF8StringEncoding];
|
||||||
if (data == nil) {
|
if (data == nil) {
|
||||||
DNOT_REACHED();
|
GWS_DNOT_REACHED();
|
||||||
ARC_RELEASE(self);
|
|
||||||
return nil;
|
return nil;
|
||||||
}
|
}
|
||||||
return [self initWithData:data contentType:@"text/html; charset=utf-8"];
|
return [self initWithData:data contentType:@"text/html; charset=utf-8"];
|
||||||
}
|
}
|
||||||
|
|
||||||
- (instancetype)initWithHTMLTemplate:(NSString*)path variables:(NSDictionary*)variables {
|
- (instancetype)initWithHTMLTemplate:(NSString*)path variables:(NSDictionary<NSString*, NSString*>*)variables {
|
||||||
NSMutableString* html = [[NSMutableString alloc] initWithContentsOfFile:path encoding:NSUTF8StringEncoding error:NULL];
|
NSMutableString* html = [[NSMutableString alloc] initWithContentsOfFile:path encoding:NSUTF8StringEncoding error:NULL];
|
||||||
[variables enumerateKeysAndObjectsUsingBlock:^(NSString* key, NSString* value, BOOL* stop) {
|
[variables enumerateKeysAndObjectsUsingBlock:^(NSString* key, NSString* value, BOOL* stop) {
|
||||||
[html replaceOccurrencesOfString:[NSString stringWithFormat:@"%%%@%%", key] withString:value options:0 range:NSMakeRange(0, html.length)];
|
[html replaceOccurrencesOfString:[NSString stringWithFormat:@"%%%@%%", key] withString:value options:0 range:NSMakeRange(0, html.length)];
|
||||||
}];
|
}];
|
||||||
id response = [self initWithHTML:html];
|
return [self initWithHTML:html];
|
||||||
ARC_RELEASE(html);
|
|
||||||
return response;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
- (instancetype)initWithJSONObject:(id)object {
|
- (instancetype)initWithJSONObject:(id)object {
|
||||||
@@ -141,7 +127,7 @@
|
|||||||
- (instancetype)initWithJSONObject:(id)object contentType:(NSString*)type {
|
- (instancetype)initWithJSONObject:(id)object contentType:(NSString*)type {
|
||||||
NSData* data = [NSJSONSerialization dataWithJSONObject:object options:0 error:NULL];
|
NSData* data = [NSJSONSerialization dataWithJSONObject:object options:0 error:NULL];
|
||||||
if (data == nil) {
|
if (data == nil) {
|
||||||
ARC_RELEASE(self);
|
GWS_DNOT_REACHED();
|
||||||
return nil;
|
return nil;
|
||||||
}
|
}
|
||||||
return [self initWithData:data contentType:type];
|
return [self initWithData:data contentType:type];
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
Copyright (c) 2012-2014, Pierre-Olivier Latour
|
Copyright (c) 2012-2019, Pierre-Olivier Latour
|
||||||
All rights reserved.
|
All rights reserved.
|
||||||
|
|
||||||
Redistribution and use in source and binary forms, with or without
|
Redistribution and use in source and binary forms, with or without
|
||||||
@@ -28,14 +28,58 @@
|
|||||||
#import "GCDWebServerDataResponse.h"
|
#import "GCDWebServerDataResponse.h"
|
||||||
#import "GCDWebServerHTTPStatusCodes.h"
|
#import "GCDWebServerHTTPStatusCodes.h"
|
||||||
|
|
||||||
// Returns responses with an HTML body containing the error message
|
NS_ASSUME_NONNULL_BEGIN
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The GCDWebServerDataResponse subclass of GCDWebServerDataResponse generates
|
||||||
|
* an HTML body from an HTTP status code and an error message.
|
||||||
|
*/
|
||||||
@interface GCDWebServerErrorResponse : GCDWebServerDataResponse
|
@interface GCDWebServerErrorResponse : GCDWebServerDataResponse
|
||||||
+ (instancetype)responseWithClientError:(GCDWebServerClientErrorHTTPStatusCode)errorCode message:(NSString*)format, ... NS_FORMAT_FUNCTION(2,3);
|
|
||||||
+ (instancetype)responseWithServerError:(GCDWebServerServerErrorHTTPStatusCode)errorCode message:(NSString*)format, ... NS_FORMAT_FUNCTION(2,3);
|
/**
|
||||||
+ (instancetype)responseWithClientError:(GCDWebServerClientErrorHTTPStatusCode)errorCode underlyingError:(NSError*)underlyingError message:(NSString*)format, ... NS_FORMAT_FUNCTION(3,4);
|
* Creates a client error response with the corresponding HTTP status code.
|
||||||
+ (instancetype)responseWithServerError:(GCDWebServerServerErrorHTTPStatusCode)errorCode underlyingError:(NSError*)underlyingError message:(NSString*)format, ... NS_FORMAT_FUNCTION(3,4);
|
*/
|
||||||
- (instancetype)initWithClientError:(GCDWebServerClientErrorHTTPStatusCode)errorCode message:(NSString*)format, ... NS_FORMAT_FUNCTION(2,3);
|
+ (instancetype)responseWithClientError:(GCDWebServerClientErrorHTTPStatusCode)errorCode message:(NSString*)format, ... NS_FORMAT_FUNCTION(2, 3);
|
||||||
- (instancetype)initWithServerError:(GCDWebServerServerErrorHTTPStatusCode)errorCode message:(NSString*)format, ... NS_FORMAT_FUNCTION(2,3);
|
|
||||||
- (instancetype)initWithClientError:(GCDWebServerClientErrorHTTPStatusCode)errorCode underlyingError:(NSError*)underlyingError message:(NSString*)format, ... NS_FORMAT_FUNCTION(3,4);
|
/**
|
||||||
- (instancetype)initWithServerError:(GCDWebServerServerErrorHTTPStatusCode)errorCode underlyingError:(NSError*)underlyingError message:(NSString*)format, ... NS_FORMAT_FUNCTION(3,4);
|
* Creates a server error response with the corresponding HTTP status code.
|
||||||
|
*/
|
||||||
|
+ (instancetype)responseWithServerError:(GCDWebServerServerErrorHTTPStatusCode)errorCode message:(NSString*)format, ... NS_FORMAT_FUNCTION(2, 3);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a client error response with the corresponding HTTP status code
|
||||||
|
* and an underlying NSError.
|
||||||
|
*/
|
||||||
|
+ (instancetype)responseWithClientError:(GCDWebServerClientErrorHTTPStatusCode)errorCode underlyingError:(nullable NSError*)underlyingError message:(NSString*)format, ... NS_FORMAT_FUNCTION(3, 4);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a server error response with the corresponding HTTP status code
|
||||||
|
* and an underlying NSError.
|
||||||
|
*/
|
||||||
|
+ (instancetype)responseWithServerError:(GCDWebServerServerErrorHTTPStatusCode)errorCode underlyingError:(nullable NSError*)underlyingError message:(NSString*)format, ... NS_FORMAT_FUNCTION(3, 4);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initializes a client error response with the corresponding HTTP status code.
|
||||||
|
*/
|
||||||
|
- (instancetype)initWithClientError:(GCDWebServerClientErrorHTTPStatusCode)errorCode message:(NSString*)format, ... NS_FORMAT_FUNCTION(2, 3);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initializes a server error response with the corresponding HTTP status code.
|
||||||
|
*/
|
||||||
|
- (instancetype)initWithServerError:(GCDWebServerServerErrorHTTPStatusCode)errorCode message:(NSString*)format, ... NS_FORMAT_FUNCTION(2, 3);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initializes a client error response with the corresponding HTTP status code
|
||||||
|
* and an underlying NSError.
|
||||||
|
*/
|
||||||
|
- (instancetype)initWithClientError:(GCDWebServerClientErrorHTTPStatusCode)errorCode underlyingError:(nullable NSError*)underlyingError message:(NSString*)format, ... NS_FORMAT_FUNCTION(3, 4);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initializes a server error response with the corresponding HTTP status code
|
||||||
|
* and an underlying NSError.
|
||||||
|
*/
|
||||||
|
- (instancetype)initWithServerError:(GCDWebServerServerErrorHTTPStatusCode)errorCode underlyingError:(nullable NSError*)underlyingError message:(NSString*)format, ... NS_FORMAT_FUNCTION(3, 4);
|
||||||
|
|
||||||
@end
|
@end
|
||||||
|
|
||||||
|
NS_ASSUME_NONNULL_END
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
Copyright (c) 2012-2014, Pierre-Olivier Latour
|
Copyright (c) 2012-2019, Pierre-Olivier Latour
|
||||||
All rights reserved.
|
All rights reserved.
|
||||||
|
|
||||||
Redistribution and use in source and binary forms, with or without
|
Redistribution and use in source and binary forms, with or without
|
||||||
@@ -25,46 +25,46 @@
|
|||||||
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
#import "GCDWebServerPrivate.h"
|
#if !__has_feature(objc_arc)
|
||||||
|
#error GCDWebServer requires ARC
|
||||||
|
#endif
|
||||||
|
|
||||||
@interface GCDWebServerErrorResponse ()
|
#import "GCDWebServerPrivate.h"
|
||||||
- (instancetype)initWithStatusCode:(NSInteger)statusCode underlyingError:(NSError*)underlyingError messageFormat:(NSString*)format arguments:(va_list)arguments;
|
|
||||||
@end
|
|
||||||
|
|
||||||
@implementation GCDWebServerErrorResponse
|
@implementation GCDWebServerErrorResponse
|
||||||
|
|
||||||
+ (instancetype)responseWithClientError:(GCDWebServerClientErrorHTTPStatusCode)errorCode message:(NSString*)format, ... {
|
+ (instancetype)responseWithClientError:(GCDWebServerClientErrorHTTPStatusCode)errorCode message:(NSString*)format, ... {
|
||||||
DCHECK(((NSInteger)errorCode >= 400) && ((NSInteger)errorCode < 500));
|
GWS_DCHECK(((NSInteger)errorCode >= 400) && ((NSInteger)errorCode < 500));
|
||||||
va_list arguments;
|
va_list arguments;
|
||||||
va_start(arguments, format);
|
va_start(arguments, format);
|
||||||
GCDWebServerErrorResponse* response = ARC_AUTORELEASE([[self alloc] initWithStatusCode:errorCode underlyingError:nil messageFormat:format arguments:arguments]);
|
GCDWebServerErrorResponse* response = [(GCDWebServerErrorResponse*)[self alloc] initWithStatusCode:errorCode underlyingError:nil messageFormat:format arguments:arguments];
|
||||||
va_end(arguments);
|
va_end(arguments);
|
||||||
return response;
|
return response;
|
||||||
}
|
}
|
||||||
|
|
||||||
+ (instancetype)responseWithServerError:(GCDWebServerServerErrorHTTPStatusCode)errorCode message:(NSString*)format, ... {
|
+ (instancetype)responseWithServerError:(GCDWebServerServerErrorHTTPStatusCode)errorCode message:(NSString*)format, ... {
|
||||||
DCHECK(((NSInteger)errorCode >= 500) && ((NSInteger)errorCode < 600));
|
GWS_DCHECK(((NSInteger)errorCode >= 500) && ((NSInteger)errorCode < 600));
|
||||||
va_list arguments;
|
va_list arguments;
|
||||||
va_start(arguments, format);
|
va_start(arguments, format);
|
||||||
GCDWebServerErrorResponse* response = ARC_AUTORELEASE([[self alloc] initWithStatusCode:errorCode underlyingError:nil messageFormat:format arguments:arguments]);
|
GCDWebServerErrorResponse* response = [(GCDWebServerErrorResponse*)[self alloc] initWithStatusCode:errorCode underlyingError:nil messageFormat:format arguments:arguments];
|
||||||
va_end(arguments);
|
va_end(arguments);
|
||||||
return response;
|
return response;
|
||||||
}
|
}
|
||||||
|
|
||||||
+ (instancetype)responseWithClientError:(GCDWebServerClientErrorHTTPStatusCode)errorCode underlyingError:(NSError*)underlyingError message:(NSString*)format, ... {
|
+ (instancetype)responseWithClientError:(GCDWebServerClientErrorHTTPStatusCode)errorCode underlyingError:(NSError*)underlyingError message:(NSString*)format, ... {
|
||||||
DCHECK(((NSInteger)errorCode >= 400) && ((NSInteger)errorCode < 500));
|
GWS_DCHECK(((NSInteger)errorCode >= 400) && ((NSInteger)errorCode < 500));
|
||||||
va_list arguments;
|
va_list arguments;
|
||||||
va_start(arguments, format);
|
va_start(arguments, format);
|
||||||
GCDWebServerErrorResponse* response = ARC_AUTORELEASE([[self alloc] initWithStatusCode:errorCode underlyingError:underlyingError messageFormat:format arguments:arguments]);
|
GCDWebServerErrorResponse* response = [(GCDWebServerErrorResponse*)[self alloc] initWithStatusCode:errorCode underlyingError:underlyingError messageFormat:format arguments:arguments];
|
||||||
va_end(arguments);
|
va_end(arguments);
|
||||||
return response;
|
return response;
|
||||||
}
|
}
|
||||||
|
|
||||||
+ (instancetype)responseWithServerError:(GCDWebServerServerErrorHTTPStatusCode)errorCode underlyingError:(NSError*)underlyingError message:(NSString*)format, ... {
|
+ (instancetype)responseWithServerError:(GCDWebServerServerErrorHTTPStatusCode)errorCode underlyingError:(NSError*)underlyingError message:(NSString*)format, ... {
|
||||||
DCHECK(((NSInteger)errorCode >= 500) && ((NSInteger)errorCode < 600));
|
GWS_DCHECK(((NSInteger)errorCode >= 500) && ((NSInteger)errorCode < 600));
|
||||||
va_list arguments;
|
va_list arguments;
|
||||||
va_start(arguments, format);
|
va_start(arguments, format);
|
||||||
GCDWebServerErrorResponse* response = ARC_AUTORELEASE([[self alloc] initWithStatusCode:errorCode underlyingError:underlyingError messageFormat:format arguments:arguments]);
|
GCDWebServerErrorResponse* response = [(GCDWebServerErrorResponse*)[self alloc] initWithStatusCode:errorCode underlyingError:underlyingError messageFormat:format arguments:arguments];
|
||||||
va_end(arguments);
|
va_end(arguments);
|
||||||
return response;
|
return response;
|
||||||
}
|
}
|
||||||
@@ -82,12 +82,11 @@ static inline NSString* _EscapeHTMLString(NSString* string) {
|
|||||||
if ((self = [self initWithHTML:html])) {
|
if ((self = [self initWithHTML:html])) {
|
||||||
self.statusCode = statusCode;
|
self.statusCode = statusCode;
|
||||||
}
|
}
|
||||||
ARC_RELEASE(message);
|
|
||||||
return self;
|
return self;
|
||||||
}
|
}
|
||||||
|
|
||||||
- (instancetype)initWithClientError:(GCDWebServerClientErrorHTTPStatusCode)errorCode message:(NSString*)format, ... {
|
- (instancetype)initWithClientError:(GCDWebServerClientErrorHTTPStatusCode)errorCode message:(NSString*)format, ... {
|
||||||
DCHECK(((NSInteger)errorCode >= 400) && ((NSInteger)errorCode < 500));
|
GWS_DCHECK(((NSInteger)errorCode >= 400) && ((NSInteger)errorCode < 500));
|
||||||
va_list arguments;
|
va_list arguments;
|
||||||
va_start(arguments, format);
|
va_start(arguments, format);
|
||||||
self = [self initWithStatusCode:errorCode underlyingError:nil messageFormat:format arguments:arguments];
|
self = [self initWithStatusCode:errorCode underlyingError:nil messageFormat:format arguments:arguments];
|
||||||
@@ -96,7 +95,7 @@ static inline NSString* _EscapeHTMLString(NSString* string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
- (instancetype)initWithServerError:(GCDWebServerServerErrorHTTPStatusCode)errorCode message:(NSString*)format, ... {
|
- (instancetype)initWithServerError:(GCDWebServerServerErrorHTTPStatusCode)errorCode message:(NSString*)format, ... {
|
||||||
DCHECK(((NSInteger)errorCode >= 500) && ((NSInteger)errorCode < 600));
|
GWS_DCHECK(((NSInteger)errorCode >= 500) && ((NSInteger)errorCode < 600));
|
||||||
va_list arguments;
|
va_list arguments;
|
||||||
va_start(arguments, format);
|
va_start(arguments, format);
|
||||||
self = [self initWithStatusCode:errorCode underlyingError:nil messageFormat:format arguments:arguments];
|
self = [self initWithStatusCode:errorCode underlyingError:nil messageFormat:format arguments:arguments];
|
||||||
@@ -105,7 +104,7 @@ static inline NSString* _EscapeHTMLString(NSString* string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
- (instancetype)initWithClientError:(GCDWebServerClientErrorHTTPStatusCode)errorCode underlyingError:(NSError*)underlyingError message:(NSString*)format, ... {
|
- (instancetype)initWithClientError:(GCDWebServerClientErrorHTTPStatusCode)errorCode underlyingError:(NSError*)underlyingError message:(NSString*)format, ... {
|
||||||
DCHECK(((NSInteger)errorCode >= 400) && ((NSInteger)errorCode < 500));
|
GWS_DCHECK(((NSInteger)errorCode >= 400) && ((NSInteger)errorCode < 500));
|
||||||
va_list arguments;
|
va_list arguments;
|
||||||
va_start(arguments, format);
|
va_start(arguments, format);
|
||||||
self = [self initWithStatusCode:errorCode underlyingError:underlyingError messageFormat:format arguments:arguments];
|
self = [self initWithStatusCode:errorCode underlyingError:underlyingError messageFormat:format arguments:arguments];
|
||||||
@@ -114,7 +113,7 @@ static inline NSString* _EscapeHTMLString(NSString* string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
- (instancetype)initWithServerError:(GCDWebServerServerErrorHTTPStatusCode)errorCode underlyingError:(NSError*)underlyingError message:(NSString*)format, ... {
|
- (instancetype)initWithServerError:(GCDWebServerServerErrorHTTPStatusCode)errorCode underlyingError:(NSError*)underlyingError message:(NSString*)format, ... {
|
||||||
DCHECK(((NSInteger)errorCode >= 500) && ((NSInteger)errorCode < 600));
|
GWS_DCHECK(((NSInteger)errorCode >= 500) && ((NSInteger)errorCode < 600));
|
||||||
va_list arguments;
|
va_list arguments;
|
||||||
va_start(arguments, format);
|
va_start(arguments, format);
|
||||||
self = [self initWithStatusCode:errorCode underlyingError:underlyingError messageFormat:format arguments:arguments];
|
self = [self initWithStatusCode:errorCode underlyingError:underlyingError messageFormat:format arguments:arguments];
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
Copyright (c) 2012-2014, Pierre-Olivier Latour
|
Copyright (c) 2012-2019, Pierre-Olivier Latour
|
||||||
All rights reserved.
|
All rights reserved.
|
||||||
|
|
||||||
Redistribution and use in source and binary forms, with or without
|
Redistribution and use in source and binary forms, with or without
|
||||||
@@ -27,13 +27,82 @@
|
|||||||
|
|
||||||
#import "GCDWebServerResponse.h"
|
#import "GCDWebServerResponse.h"
|
||||||
|
|
||||||
|
NS_ASSUME_NONNULL_BEGIN
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The GCDWebServerFileResponse subclass of GCDWebServerResponse reads the body
|
||||||
|
* of the HTTP response from a file on disk.
|
||||||
|
*
|
||||||
|
* It will automatically set the contentType, lastModifiedDate and eTag
|
||||||
|
* properties of the GCDWebServerResponse according to the file extension and
|
||||||
|
* metadata.
|
||||||
|
*/
|
||||||
@interface GCDWebServerFileResponse : GCDWebServerResponse
|
@interface GCDWebServerFileResponse : GCDWebServerResponse
|
||||||
+ (instancetype)responseWithFile:(NSString*)path;
|
@property(nonatomic, copy) NSString* contentType; // Redeclare as non-null
|
||||||
+ (instancetype)responseWithFile:(NSString*)path isAttachment:(BOOL)attachment;
|
@property(nonatomic) NSDate* lastModifiedDate; // Redeclare as non-null
|
||||||
+ (instancetype)responseWithFile:(NSString*)path byteRange:(NSRange)range;
|
@property(nonatomic, copy) NSString* eTag; // Redeclare as non-null
|
||||||
+ (instancetype)responseWithFile:(NSString*)path byteRange:(NSRange)range isAttachment:(BOOL)attachment;
|
|
||||||
- (instancetype)initWithFile:(NSString*)path;
|
/**
|
||||||
- (instancetype)initWithFile:(NSString*)path isAttachment:(BOOL)attachment; // If in attachment mode, "Content-Disposition" header will be set accordingly
|
* Creates a response with the contents of a file.
|
||||||
- (instancetype)initWithFile:(NSString*)path byteRange:(NSRange)range; // Pass [NSNotFound, 0] to disable byte range entirely, [offset, length] to enable byte range from beginning of file or [NSNotFound, -length] from end of file
|
*/
|
||||||
- (instancetype)initWithFile:(NSString*)path byteRange:(NSRange)range isAttachment:(BOOL)attachment;
|
+ (nullable instancetype)responseWithFile:(NSString*)path;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a response like +responseWithFile: and sets the "Content-Disposition"
|
||||||
|
* HTTP header for a download if the "attachment" argument is YES.
|
||||||
|
*/
|
||||||
|
+ (nullable instancetype)responseWithFile:(NSString*)path isAttachment:(BOOL)attachment;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a response like +responseWithFile: but restricts the file contents
|
||||||
|
* to a specific byte range.
|
||||||
|
*
|
||||||
|
* See -initWithFile:byteRange: for details.
|
||||||
|
*/
|
||||||
|
+ (nullable instancetype)responseWithFile:(NSString*)path byteRange:(NSRange)range;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a response like +responseWithFile:byteRange: and sets the
|
||||||
|
* "Content-Disposition" HTTP header for a download if the "attachment"
|
||||||
|
* argument is YES.
|
||||||
|
*/
|
||||||
|
+ (nullable instancetype)responseWithFile:(NSString*)path byteRange:(NSRange)range isAttachment:(BOOL)attachment;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initializes a response with the contents of a file.
|
||||||
|
*/
|
||||||
|
- (nullable instancetype)initWithFile:(NSString*)path;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initializes a response like +responseWithFile: and sets the
|
||||||
|
* "Content-Disposition" HTTP header for a download if the "attachment"
|
||||||
|
* argument is YES.
|
||||||
|
*/
|
||||||
|
- (nullable instancetype)initWithFile:(NSString*)path isAttachment:(BOOL)attachment;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initializes a response like -initWithFile: but restricts the file contents
|
||||||
|
* to a specific byte range. This range should be set to (NSUIntegerMax, 0) for
|
||||||
|
* the full file, (offset, length) if expressed from the beginning of the file,
|
||||||
|
* or (NSUIntegerMax, length) if expressed from the end of the file. The "offset"
|
||||||
|
* and "length" values will be automatically adjusted to be compatible with the
|
||||||
|
* actual size of the file.
|
||||||
|
*
|
||||||
|
* This argument would typically be set to the value of the byteRange property
|
||||||
|
* of the current GCDWebServerRequest.
|
||||||
|
*/
|
||||||
|
- (nullable instancetype)initWithFile:(NSString*)path byteRange:(NSRange)range;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This method is the designated initializer for the class.
|
||||||
|
*
|
||||||
|
* If MIME type overrides are specified, they allow to customize the built-in
|
||||||
|
* mapping from extensions to MIME types. Keys of the dictionary must be lowercased
|
||||||
|
* file extensions without the period, and the values must be the corresponding
|
||||||
|
* MIME types.
|
||||||
|
*/
|
||||||
|
- (nullable instancetype)initWithFile:(NSString*)path byteRange:(NSRange)range isAttachment:(BOOL)attachment mimeTypeOverrides:(nullable NSDictionary<NSString*, NSString*>*)overrides;
|
||||||
|
|
||||||
@end
|
@end
|
||||||
|
|
||||||
|
NS_ASSUME_NONNULL_END
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
Copyright (c) 2012-2014, Pierre-Olivier Latour
|
Copyright (c) 2012-2019, Pierre-Olivier Latour
|
||||||
All rights reserved.
|
All rights reserved.
|
||||||
|
|
||||||
Redistribution and use in source and binary forms, with or without
|
Redistribution and use in source and binary forms, with or without
|
||||||
@@ -25,126 +25,130 @@
|
|||||||
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
#if !__has_feature(objc_arc)
|
||||||
|
#error GCDWebServer requires ARC
|
||||||
|
#endif
|
||||||
|
|
||||||
#import <sys/stat.h>
|
#import <sys/stat.h>
|
||||||
|
|
||||||
#import "GCDWebServerPrivate.h"
|
#import "GCDWebServerPrivate.h"
|
||||||
|
|
||||||
#define kFileReadBufferSize (32 * 1024)
|
#define kFileReadBufferSize (32 * 1024)
|
||||||
|
|
||||||
@interface GCDWebServerFileResponse () {
|
@implementation GCDWebServerFileResponse {
|
||||||
@private
|
|
||||||
NSString* _path;
|
NSString* _path;
|
||||||
NSUInteger _offset;
|
NSUInteger _offset;
|
||||||
NSUInteger _size;
|
NSUInteger _size;
|
||||||
int _file;
|
int _file;
|
||||||
}
|
}
|
||||||
@end
|
|
||||||
|
|
||||||
static inline NSError* _MakePosixError(int code) {
|
@dynamic contentType, lastModifiedDate, eTag;
|
||||||
return [NSError errorWithDomain:NSPOSIXErrorDomain code:code userInfo:@{NSLocalizedDescriptionKey: [NSString stringWithFormat:@"%s", strerror(code)]}];
|
|
||||||
}
|
|
||||||
|
|
||||||
@implementation GCDWebServerFileResponse
|
|
||||||
|
|
||||||
+ (instancetype)responseWithFile:(NSString*)path {
|
+ (instancetype)responseWithFile:(NSString*)path {
|
||||||
return ARC_AUTORELEASE([[[self class] alloc] initWithFile:path]);
|
return [(GCDWebServerFileResponse*)[[self class] alloc] initWithFile:path];
|
||||||
}
|
}
|
||||||
|
|
||||||
+ (instancetype)responseWithFile:(NSString*)path isAttachment:(BOOL)attachment {
|
+ (instancetype)responseWithFile:(NSString*)path isAttachment:(BOOL)attachment {
|
||||||
return ARC_AUTORELEASE([[[self class] alloc] initWithFile:path isAttachment:attachment]);
|
return [(GCDWebServerFileResponse*)[[self class] alloc] initWithFile:path isAttachment:attachment];
|
||||||
}
|
}
|
||||||
|
|
||||||
+ (instancetype)responseWithFile:(NSString*)path byteRange:(NSRange)range {
|
+ (instancetype)responseWithFile:(NSString*)path byteRange:(NSRange)range {
|
||||||
return ARC_AUTORELEASE([[[self class] alloc] initWithFile:path byteRange:range]);
|
return [(GCDWebServerFileResponse*)[[self class] alloc] initWithFile:path byteRange:range];
|
||||||
}
|
}
|
||||||
|
|
||||||
+ (instancetype)responseWithFile:(NSString*)path byteRange:(NSRange)range isAttachment:(BOOL)attachment {
|
+ (instancetype)responseWithFile:(NSString*)path byteRange:(NSRange)range isAttachment:(BOOL)attachment {
|
||||||
return ARC_AUTORELEASE([[[self class] alloc] initWithFile:path byteRange:range isAttachment:attachment]);
|
return [(GCDWebServerFileResponse*)[[self class] alloc] initWithFile:path byteRange:range isAttachment:attachment mimeTypeOverrides:nil];
|
||||||
}
|
}
|
||||||
|
|
||||||
- (instancetype)initWithFile:(NSString*)path {
|
- (instancetype)initWithFile:(NSString*)path {
|
||||||
return [self initWithFile:path byteRange:NSMakeRange(NSNotFound, 0) isAttachment:NO];
|
return [self initWithFile:path byteRange:NSMakeRange(NSUIntegerMax, 0) isAttachment:NO mimeTypeOverrides:nil];
|
||||||
}
|
}
|
||||||
|
|
||||||
- (instancetype)initWithFile:(NSString*)path isAttachment:(BOOL)attachment {
|
- (instancetype)initWithFile:(NSString*)path isAttachment:(BOOL)attachment {
|
||||||
return [self initWithFile:path byteRange:NSMakeRange(NSNotFound, 0) isAttachment:attachment];
|
return [self initWithFile:path byteRange:NSMakeRange(NSUIntegerMax, 0) isAttachment:attachment mimeTypeOverrides:nil];
|
||||||
}
|
}
|
||||||
|
|
||||||
- (instancetype)initWithFile:(NSString*)path byteRange:(NSRange)range {
|
- (instancetype)initWithFile:(NSString*)path byteRange:(NSRange)range {
|
||||||
return [self initWithFile:path byteRange:range isAttachment:NO];
|
return [self initWithFile:path byteRange:range isAttachment:NO mimeTypeOverrides:nil];
|
||||||
}
|
}
|
||||||
|
|
||||||
static inline NSDate* _NSDateFromTimeSpec(const struct timespec* t) {
|
static inline NSDate* _NSDateFromTimeSpec(const struct timespec* t) {
|
||||||
return [NSDate dateWithTimeIntervalSince1970:((NSTimeInterval)t->tv_sec + (NSTimeInterval)t->tv_nsec / 1000000000.0)];
|
return [NSDate dateWithTimeIntervalSince1970:((NSTimeInterval)t->tv_sec + (NSTimeInterval)t->tv_nsec / 1000000000.0)];
|
||||||
}
|
}
|
||||||
|
|
||||||
- (instancetype)initWithFile:(NSString*)path byteRange:(NSRange)range isAttachment:(BOOL)attachment {
|
- (instancetype)initWithFile:(NSString*)path byteRange:(NSRange)range isAttachment:(BOOL)attachment mimeTypeOverrides:(NSDictionary<NSString*, NSString*>*)overrides {
|
||||||
struct stat info;
|
struct stat info;
|
||||||
if (lstat([path fileSystemRepresentation], &info) || !(info.st_mode & S_IFREG)) {
|
if (lstat([path fileSystemRepresentation], &info) || !(info.st_mode & S_IFREG)) {
|
||||||
DNOT_REACHED();
|
GWS_DNOT_REACHED();
|
||||||
ARC_RELEASE(self);
|
|
||||||
return nil;
|
return nil;
|
||||||
}
|
}
|
||||||
if (GCDWebServerIsValidByteRange(range)) {
|
#ifndef __LP64__
|
||||||
if (range.location != NSNotFound) {
|
if (info.st_size >= (off_t)4294967295) { // In 32 bit mode, we can't handle files greater than 4 GiBs (don't use "NSUIntegerMax" here to avoid potential unsigned to signed conversion issues)
|
||||||
range.location = MIN(range.location, (NSUInteger)info.st_size);
|
GWS_DNOT_REACHED();
|
||||||
range.length = MIN(range.length, (NSUInteger)info.st_size - range.location);
|
return nil;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
NSUInteger fileSize = (NSUInteger)info.st_size;
|
||||||
|
|
||||||
|
BOOL hasByteRange = GCDWebServerIsValidByteRange(range);
|
||||||
|
if (hasByteRange) {
|
||||||
|
if (range.location != NSUIntegerMax) {
|
||||||
|
range.location = MIN(range.location, fileSize);
|
||||||
|
range.length = MIN(range.length, fileSize - range.location);
|
||||||
} else {
|
} else {
|
||||||
range.length = MIN(range.length, (NSUInteger)info.st_size);
|
range.length = MIN(range.length, fileSize);
|
||||||
range.location = (NSUInteger)info.st_size - range.length;
|
range.location = fileSize - range.length;
|
||||||
}
|
}
|
||||||
if (range.length == 0) {
|
if (range.length == 0) {
|
||||||
ARC_RELEASE(self);
|
|
||||||
return nil; // TODO: Return 416 status code and "Content-Range: bytes */{file length}" header
|
return nil; // TODO: Return 416 status code and "Content-Range: bytes */{file length}" header
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
range.location = 0;
|
||||||
|
range.length = fileSize;
|
||||||
}
|
}
|
||||||
|
|
||||||
if ((self = [super init])) {
|
if ((self = [super init])) {
|
||||||
_path = [path copy];
|
_path = [path copy];
|
||||||
if (range.location != NSNotFound) {
|
_offset = range.location;
|
||||||
_offset = range.location;
|
_size = range.length;
|
||||||
_size = range.length;
|
if (hasByteRange) {
|
||||||
[self setStatusCode:kGCDWebServerHTTPStatusCode_PartialContent];
|
[self setStatusCode:kGCDWebServerHTTPStatusCode_PartialContent];
|
||||||
[self setValue:[NSString stringWithFormat:@"bytes %i-%i/%i", (int)range.location, (int)(range.location + range.length - 1), (int)info.st_size] forAdditionalHeader:@"Content-Range"];
|
[self setValue:[NSString stringWithFormat:@"bytes %lu-%lu/%lu", (unsigned long)_offset, (unsigned long)(_offset + _size - 1), (unsigned long)fileSize] forAdditionalHeader:@"Content-Range"];
|
||||||
LOG_DEBUG(@"Using content bytes range [%i-%i] for file \"%@\"", (int)range.location, (int)(range.location + range.length - 1), path);
|
GWS_LOG_DEBUG(@"Using content bytes range [%lu-%lu] for file \"%@\"", (unsigned long)_offset, (unsigned long)(_offset + _size - 1), path);
|
||||||
} else {
|
|
||||||
_offset = 0;
|
|
||||||
_size = (NSUInteger)info.st_size;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (attachment) { // TODO: Use http://tools.ietf.org/html/rfc5987 to encode file names with special characters instead of using lossy conversion to ISO 8859-1
|
if (attachment) {
|
||||||
NSData* data = [[path lastPathComponent] dataUsingEncoding:NSISOLatin1StringEncoding allowLossyConversion:YES];
|
NSString* fileName = [path lastPathComponent];
|
||||||
NSString* fileName = data ? [[NSString alloc] initWithData:data encoding:NSISOLatin1StringEncoding] : nil;
|
NSData* data = [[fileName stringByReplacingOccurrencesOfString:@"\"" withString:@""] dataUsingEncoding:NSISOLatin1StringEncoding allowLossyConversion:YES];
|
||||||
if (fileName) {
|
NSString* lossyFileName = data ? [[NSString alloc] initWithData:data encoding:NSISOLatin1StringEncoding] : nil;
|
||||||
[self setValue:[NSString stringWithFormat:@"attachment; filename=\"%@\"", fileName] forAdditionalHeader:@"Content-Disposition"];
|
if (lossyFileName) {
|
||||||
ARC_RELEASE(fileName);
|
NSString* value = [NSString stringWithFormat:@"attachment; filename=\"%@\"; filename*=UTF-8''%@", lossyFileName, GCDWebServerEscapeURLString(fileName)];
|
||||||
|
[self setValue:value forAdditionalHeader:@"Content-Disposition"];
|
||||||
} else {
|
} else {
|
||||||
DNOT_REACHED();
|
GWS_DNOT_REACHED();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
self.contentType = GCDWebServerGetMimeTypeForExtension([path pathExtension]);
|
self.contentType = GCDWebServerGetMimeTypeForExtension([_path pathExtension], overrides);
|
||||||
self.contentLength = (range.location != NSNotFound ? range.length : (NSUInteger)info.st_size);
|
self.contentLength = _size;
|
||||||
self.lastModifiedDate = _NSDateFromTimeSpec(&info.st_mtimespec);
|
self.lastModifiedDate = _NSDateFromTimeSpec(&info.st_mtimespec);
|
||||||
self.eTag = [NSString stringWithFormat:@"%llu/%li/%li", info.st_ino, info.st_mtimespec.tv_sec, info.st_mtimespec.tv_nsec];
|
self.eTag = [NSString stringWithFormat:@"%llu/%li/%li", info.st_ino, info.st_mtimespec.tv_sec, info.st_mtimespec.tv_nsec];
|
||||||
}
|
}
|
||||||
return self;
|
return self;
|
||||||
}
|
}
|
||||||
|
|
||||||
- (void)dealloc {
|
|
||||||
ARC_RELEASE(_path);
|
|
||||||
|
|
||||||
ARC_DEALLOC(super);
|
|
||||||
}
|
|
||||||
|
|
||||||
- (BOOL)open:(NSError**)error {
|
- (BOOL)open:(NSError**)error {
|
||||||
_file = open([_path fileSystemRepresentation], O_NOFOLLOW | O_RDONLY);
|
_file = open([_path fileSystemRepresentation], O_NOFOLLOW | O_RDONLY);
|
||||||
if (_file <= 0) {
|
if (_file <= 0) {
|
||||||
*error = _MakePosixError(errno);
|
if (error) {
|
||||||
|
*error = GCDWebServerMakePosixError(errno);
|
||||||
|
}
|
||||||
return NO;
|
return NO;
|
||||||
}
|
}
|
||||||
if (lseek(_file, _offset, SEEK_SET) != (off_t)_offset) {
|
if (lseek(_file, _offset, SEEK_SET) != (off_t)_offset) {
|
||||||
*error = _MakePosixError(errno);
|
if (error) {
|
||||||
|
*error = GCDWebServerMakePosixError(errno);
|
||||||
|
}
|
||||||
close(_file);
|
close(_file);
|
||||||
return NO;
|
return NO;
|
||||||
}
|
}
|
||||||
@@ -156,14 +160,16 @@ static inline NSDate* _NSDateFromTimeSpec(const struct timespec* t) {
|
|||||||
NSMutableData* data = [[NSMutableData alloc] initWithLength:length];
|
NSMutableData* data = [[NSMutableData alloc] initWithLength:length];
|
||||||
ssize_t result = read(_file, data.mutableBytes, length);
|
ssize_t result = read(_file, data.mutableBytes, length);
|
||||||
if (result < 0) {
|
if (result < 0) {
|
||||||
*error = _MakePosixError(errno);
|
if (error) {
|
||||||
|
*error = GCDWebServerMakePosixError(errno);
|
||||||
|
}
|
||||||
return nil;
|
return nil;
|
||||||
}
|
}
|
||||||
if (result > 0) {
|
if (result > 0) {
|
||||||
[data setLength:result];
|
[data setLength:result];
|
||||||
_size -= result;
|
_size -= result;
|
||||||
}
|
}
|
||||||
return ARC_AUTORELEASE(data);
|
return data;
|
||||||
}
|
}
|
||||||
|
|
||||||
- (void)close {
|
- (void)close {
|
||||||
|
|||||||
80
GCDWebServer/Responses/GCDWebServerStreamedResponse.h
Normal file
80
GCDWebServer/Responses/GCDWebServerStreamedResponse.h
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
/*
|
||||||
|
Copyright (c) 2012-2019, Pierre-Olivier Latour
|
||||||
|
All rights reserved.
|
||||||
|
|
||||||
|
Redistribution and use in source and binary forms, with or without
|
||||||
|
modification, are permitted provided that the following conditions are met:
|
||||||
|
* Redistributions of source code must retain the above copyright
|
||||||
|
notice, this list of conditions and the following disclaimer.
|
||||||
|
* Redistributions in binary form must reproduce the above copyright
|
||||||
|
notice, this list of conditions and the following disclaimer in the
|
||||||
|
documentation and/or other materials provided with the distribution.
|
||||||
|
* The name of Pierre-Olivier Latour may not be used to endorse
|
||||||
|
or promote products derived from this software without specific
|
||||||
|
prior written permission.
|
||||||
|
|
||||||
|
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
||||||
|
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||||
|
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||||
|
DISCLAIMED. IN NO EVENT SHALL PIERRE-OLIVIER LATOUR BE LIABLE FOR ANY
|
||||||
|
DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
||||||
|
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||||
|
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
|
||||||
|
ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||||
|
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||||
|
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#import "GCDWebServerResponse.h"
|
||||||
|
|
||||||
|
NS_ASSUME_NONNULL_BEGIN
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The GCDWebServerStreamBlock is called to stream the data for the HTTP body.
|
||||||
|
* The block must return either a chunk of data, an empty NSData when done, or
|
||||||
|
* nil on error and set the "error" argument which is guaranteed to be non-NULL.
|
||||||
|
*/
|
||||||
|
typedef NSData* _Nullable (^GCDWebServerStreamBlock)(NSError** error);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The GCDWebServerAsyncStreamBlock works like the GCDWebServerStreamBlock
|
||||||
|
* except the streamed data can be returned at a later time allowing for
|
||||||
|
* truly asynchronous generation of the data.
|
||||||
|
*
|
||||||
|
* The block must call "completionBlock" passing the new chunk of data when ready,
|
||||||
|
* an empty NSData when done, or nil on error and pass a NSError.
|
||||||
|
*
|
||||||
|
* The block cannot call "completionBlock" more than once per invocation.
|
||||||
|
*/
|
||||||
|
typedef void (^GCDWebServerAsyncStreamBlock)(GCDWebServerBodyReaderCompletionBlock completionBlock);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The GCDWebServerStreamedResponse subclass of GCDWebServerResponse streams
|
||||||
|
* the body of the HTTP response using a GCD block.
|
||||||
|
*/
|
||||||
|
@interface GCDWebServerStreamedResponse : GCDWebServerResponse
|
||||||
|
@property(nonatomic, copy) NSString* contentType; // Redeclare as non-null
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a response with streamed data and a given content type.
|
||||||
|
*/
|
||||||
|
+ (instancetype)responseWithContentType:(NSString*)type streamBlock:(GCDWebServerStreamBlock)block;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a response with async streamed data and a given content type.
|
||||||
|
*/
|
||||||
|
+ (instancetype)responseWithContentType:(NSString*)type asyncStreamBlock:(GCDWebServerAsyncStreamBlock)block;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initializes a response with streamed data and a given content type.
|
||||||
|
*/
|
||||||
|
- (instancetype)initWithContentType:(NSString*)type streamBlock:(GCDWebServerStreamBlock)block;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This method is the designated initializer for the class.
|
||||||
|
*/
|
||||||
|
- (instancetype)initWithContentType:(NSString*)type asyncStreamBlock:(GCDWebServerAsyncStreamBlock)block;
|
||||||
|
|
||||||
|
@end
|
||||||
|
|
||||||
|
NS_ASSUME_NONNULL_END
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
Copyright (c) 2012-2014, Pierre-Olivier Latour
|
Copyright (c) 2012-2019, Pierre-Olivier Latour
|
||||||
All rights reserved.
|
All rights reserved.
|
||||||
|
|
||||||
Redistribution and use in source and binary forms, with or without
|
Redistribution and use in source and binary forms, with or without
|
||||||
@@ -25,37 +25,46 @@
|
|||||||
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
#if !__has_feature(objc_arc)
|
||||||
|
#error GCDWebServer requires ARC
|
||||||
|
#endif
|
||||||
|
|
||||||
#import "GCDWebServerPrivate.h"
|
#import "GCDWebServerPrivate.h"
|
||||||
|
|
||||||
@interface GCDWebServerStreamingResponse () {
|
@implementation GCDWebServerStreamedResponse {
|
||||||
@private
|
GCDWebServerAsyncStreamBlock _block;
|
||||||
GCDWebServerStreamBlock _block;
|
|
||||||
}
|
}
|
||||||
@end
|
|
||||||
|
|
||||||
@implementation GCDWebServerStreamingResponse
|
@dynamic contentType;
|
||||||
|
|
||||||
+ (instancetype)responseWithContentType:(NSString*)type streamBlock:(GCDWebServerStreamBlock)block {
|
+ (instancetype)responseWithContentType:(NSString*)type streamBlock:(GCDWebServerStreamBlock)block {
|
||||||
return ARC_AUTORELEASE([[[self class] alloc] initWithContentType:type streamBlock:block]);
|
return [(GCDWebServerStreamedResponse*)[[self class] alloc] initWithContentType:type streamBlock:block];
|
||||||
|
}
|
||||||
|
|
||||||
|
+ (instancetype)responseWithContentType:(NSString*)type asyncStreamBlock:(GCDWebServerAsyncStreamBlock)block {
|
||||||
|
return [(GCDWebServerStreamedResponse*)[[self class] alloc] initWithContentType:type asyncStreamBlock:block];
|
||||||
}
|
}
|
||||||
|
|
||||||
- (instancetype)initWithContentType:(NSString*)type streamBlock:(GCDWebServerStreamBlock)block {
|
- (instancetype)initWithContentType:(NSString*)type streamBlock:(GCDWebServerStreamBlock)block {
|
||||||
|
return [self initWithContentType:type
|
||||||
|
asyncStreamBlock:^(GCDWebServerBodyReaderCompletionBlock completionBlock) {
|
||||||
|
NSError* error = nil;
|
||||||
|
NSData* data = block(&error);
|
||||||
|
completionBlock(data, error);
|
||||||
|
}];
|
||||||
|
}
|
||||||
|
|
||||||
|
- (instancetype)initWithContentType:(NSString*)type asyncStreamBlock:(GCDWebServerAsyncStreamBlock)block {
|
||||||
if ((self = [super init])) {
|
if ((self = [super init])) {
|
||||||
_block = [block copy];
|
_block = [block copy];
|
||||||
|
|
||||||
self.contentType = type;
|
self.contentType = type;
|
||||||
}
|
}
|
||||||
return self;
|
return self;
|
||||||
}
|
}
|
||||||
|
|
||||||
- (void)dealloc {
|
- (void)asyncReadDataWithCompletion:(GCDWebServerBodyReaderCompletionBlock)block {
|
||||||
ARC_RELEASE(_block);
|
_block(block);
|
||||||
|
|
||||||
ARC_DEALLOC(super);
|
|
||||||
}
|
|
||||||
|
|
||||||
- (NSData*)readData:(NSError**)error {
|
|
||||||
return _block(error);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
- (NSString*)description {
|
- (NSString*)description {
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
Copyright (c) 2012-2014, Pierre-Olivier Latour
|
Copyright (c) 2012-2019, Pierre-Olivier Latour
|
||||||
All rights reserved.
|
All rights reserved.
|
||||||
|
|
||||||
Redistribution and use in source and binary forms, with or without
|
Redistribution and use in source and binary forms, with or without
|
||||||
|
Before Width: | Height: | Size: 62 KiB After Width: | Height: | Size: 62 KiB |
@@ -1,5 +1,5 @@
|
|||||||
<!--
|
<!--
|
||||||
Copyright (c) 2012-2014, Pierre-Olivier Latour
|
Copyright (c) 2012-2019, Pierre-Olivier Latour
|
||||||
All rights reserved.
|
All rights reserved.
|
||||||
|
|
||||||
Redistribution and use in source and binary forms, with or without
|
Redistribution and use in source and binary forms, with or without
|
||||||
@@ -65,7 +65,7 @@
|
|||||||
<div id="alerts"></div>
|
<div id="alerts"></div>
|
||||||
|
|
||||||
<div class="btn-toolbar">
|
<div class="btn-toolbar">
|
||||||
<button type="button" class="btn btn-primary fileinput-button">
|
<button type="button" class="btn btn-primary fileinput-button" id="upload-file">
|
||||||
<span class="glyphicon glyphicon-upload"></span> Upload Files…
|
<span class="glyphicon glyphicon-upload"></span> Upload Files…
|
||||||
<input id="fileupload" type="file" name="files[]" multiple>
|
<input id="fileupload" type="file" name="files[]" multiple>
|
||||||
</button>
|
</button>
|
||||||
0
GCDWebUploader/GCDWebUploader.bundle/js/html5shiv.min.js → GCDWebUploader/GCDWebUploader.bundle/Contents/Resources/js/html5shiv.min.js
vendored
Executable file → Normal file
0
GCDWebUploader/GCDWebUploader.bundle/js/html5shiv.min.js → GCDWebUploader/GCDWebUploader.bundle/Contents/Resources/js/html5shiv.min.js
vendored
Executable file → Normal file
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
Copyright (c) 2012-2014, Pierre-Olivier Latour
|
Copyright (c) 2012-2019, Pierre-Olivier Latour
|
||||||
All rights reserved.
|
All rights reserved.
|
||||||
|
|
||||||
Redistribution and use in source and binary forms, with or without
|
Redistribution and use in source and binary forms, with or without
|
||||||
@@ -178,6 +178,17 @@ function _reload(path) {
|
|||||||
|
|
||||||
$(document).ready(function() {
|
$(document).ready(function() {
|
||||||
|
|
||||||
|
// Workaround Firefox and IE not showing file selection dialog when clicking on "upload-file" <button>
|
||||||
|
// Making it a <div> instead also works but then it the button doesn't work anymore with tab selection or accessibility
|
||||||
|
$("#upload-file").click(function(event) {
|
||||||
|
$("#fileupload").click();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Prevent event bubbling when using workaround above
|
||||||
|
$("#fileupload").click(function(event) {
|
||||||
|
event.stopPropagation();
|
||||||
|
});
|
||||||
|
|
||||||
$("#fileupload").fileupload({
|
$("#fileupload").fileupload({
|
||||||
dropZone: $(document),
|
dropZone: $(document),
|
||||||
pasteZone: null,
|
pasteZone: null,
|
||||||
0
GCDWebUploader/GCDWebUploader.bundle/js/respond.min.js → GCDWebUploader/GCDWebUploader.bundle/Contents/Resources/js/respond.min.js
vendored
Executable file → Normal file
0
GCDWebUploader/GCDWebUploader.bundle/js/respond.min.js → GCDWebUploader/GCDWebUploader.bundle/Contents/Resources/js/respond.min.js
vendored
Executable file → Normal file
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
Copyright (c) 2012-2014, Pierre-Olivier Latour
|
Copyright (c) 2012-2019, Pierre-Olivier Latour
|
||||||
All rights reserved.
|
All rights reserved.
|
||||||
|
|
||||||
Redistribution and use in source and binary forms, with or without
|
Redistribution and use in source and binary forms, with or without
|
||||||
@@ -27,35 +27,175 @@
|
|||||||
|
|
||||||
#import "GCDWebServer.h"
|
#import "GCDWebServer.h"
|
||||||
|
|
||||||
|
NS_ASSUME_NONNULL_BEGIN
|
||||||
|
|
||||||
@class GCDWebUploader;
|
@class GCDWebUploader;
|
||||||
|
|
||||||
// These methods are always called on main thread
|
/**
|
||||||
|
* Delegate methods for GCDWebUploader.
|
||||||
|
*
|
||||||
|
* @warning These methods are always called on the main thread in a serialized way.
|
||||||
|
*/
|
||||||
@protocol GCDWebUploaderDelegate <GCDWebServerDelegate>
|
@protocol GCDWebUploaderDelegate <GCDWebServerDelegate>
|
||||||
@optional
|
@optional
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This method is called whenever a file has been downloaded.
|
||||||
|
*/
|
||||||
- (void)webUploader:(GCDWebUploader*)uploader didDownloadFileAtPath:(NSString*)path;
|
- (void)webUploader:(GCDWebUploader*)uploader didDownloadFileAtPath:(NSString*)path;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This method is called whenever a file has been uploaded.
|
||||||
|
*/
|
||||||
- (void)webUploader:(GCDWebUploader*)uploader didUploadFileAtPath:(NSString*)path;
|
- (void)webUploader:(GCDWebUploader*)uploader didUploadFileAtPath:(NSString*)path;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This method is called whenever a file or directory has been moved.
|
||||||
|
*/
|
||||||
- (void)webUploader:(GCDWebUploader*)uploader didMoveItemFromPath:(NSString*)fromPath toPath:(NSString*)toPath;
|
- (void)webUploader:(GCDWebUploader*)uploader didMoveItemFromPath:(NSString*)fromPath toPath:(NSString*)toPath;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This method is called whenever a file or directory has been deleted.
|
||||||
|
*/
|
||||||
- (void)webUploader:(GCDWebUploader*)uploader didDeleteItemAtPath:(NSString*)path;
|
- (void)webUploader:(GCDWebUploader*)uploader didDeleteItemAtPath:(NSString*)path;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This method is called whenever a directory has been created.
|
||||||
|
*/
|
||||||
- (void)webUploader:(GCDWebUploader*)uploader didCreateDirectoryAtPath:(NSString*)path;
|
- (void)webUploader:(GCDWebUploader*)uploader didCreateDirectoryAtPath:(NSString*)path;
|
||||||
|
|
||||||
@end
|
@end
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The GCDWebUploader subclass of GCDWebServer implements an HTML 5 web browser
|
||||||
|
* interface for uploading or downloading files, and moving or deleting files
|
||||||
|
* or directories.
|
||||||
|
*
|
||||||
|
* See the README.md file for more information about the features of GCDWebUploader.
|
||||||
|
*
|
||||||
|
* @warning For GCDWebUploader to work, "GCDWebUploader.bundle" must be added
|
||||||
|
* to the resources of the Xcode target.
|
||||||
|
*/
|
||||||
@interface GCDWebUploader : GCDWebServer
|
@interface GCDWebUploader : GCDWebServer
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the upload directory as specified when the uploader was initialized.
|
||||||
|
*/
|
||||||
@property(nonatomic, readonly) NSString* uploadDirectory;
|
@property(nonatomic, readonly) NSString* uploadDirectory;
|
||||||
@property(nonatomic, assign) id<GCDWebUploaderDelegate> delegate;
|
|
||||||
@property(nonatomic, copy) NSArray* allowedFileExtensions; // Default is nil i.e. all file extensions are allowed
|
/**
|
||||||
@property(nonatomic) BOOL showHiddenFiles; // Default is NO
|
* Sets the delegate for the uploader.
|
||||||
@property(nonatomic, copy) NSString* title; // Default is application name (must be HTML escaped)
|
*/
|
||||||
@property(nonatomic, copy) NSString* header; // Default is same as title (must be HTML escaped)
|
@property(nonatomic, weak, nullable) id<GCDWebUploaderDelegate> delegate;
|
||||||
@property(nonatomic, copy) NSString* prologue; // Default is mini help (must be raw HTML)
|
|
||||||
@property(nonatomic, copy) NSString* epilogue; // Default is nothing (must be raw HTML)
|
/**
|
||||||
@property(nonatomic, copy) NSString* footer; // Default is application name and version (must be HTML escaped)
|
* Sets which files are allowed to be operated on depending on their extension.
|
||||||
|
*
|
||||||
|
* The default value is nil i.e. all file extensions are allowed.
|
||||||
|
*/
|
||||||
|
@property(nonatomic, copy) NSArray<NSString*>* allowedFileExtensions;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets if files and directories whose name start with a period are allowed to
|
||||||
|
* be operated on.
|
||||||
|
*
|
||||||
|
* The default value is NO.
|
||||||
|
*/
|
||||||
|
@property(nonatomic) BOOL allowHiddenItems;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the title for the uploader web interface.
|
||||||
|
*
|
||||||
|
* The default value is the application name.
|
||||||
|
*
|
||||||
|
* @warning Any reserved HTML characters in the string value for this property
|
||||||
|
* must have been replaced by character entities e.g. "&" becomes "&".
|
||||||
|
*/
|
||||||
|
@property(nonatomic, copy) NSString* title;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the header for the uploader web interface.
|
||||||
|
*
|
||||||
|
* The default value is the same as the title property.
|
||||||
|
*
|
||||||
|
* @warning Any reserved HTML characters in the string value for this property
|
||||||
|
* must have been replaced by character entities e.g. "&" becomes "&".
|
||||||
|
*/
|
||||||
|
@property(nonatomic, copy) NSString* header;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the prologue for the uploader web interface.
|
||||||
|
*
|
||||||
|
* The default value is a short help text.
|
||||||
|
*
|
||||||
|
* @warning The string value for this property must be raw HTML
|
||||||
|
* e.g. "<p>Some text</p>"
|
||||||
|
*/
|
||||||
|
@property(nonatomic, copy) NSString* prologue;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the epilogue for the uploader web interface.
|
||||||
|
*
|
||||||
|
* The default value is nil i.e. no epilogue.
|
||||||
|
*
|
||||||
|
* @warning The string value for this property must be raw HTML
|
||||||
|
* e.g. "<p>Some text</p>"
|
||||||
|
*/
|
||||||
|
@property(nonatomic, copy) NSString* epilogue;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the footer for the uploader web interface.
|
||||||
|
*
|
||||||
|
* The default value is the application name and version.
|
||||||
|
*
|
||||||
|
* @warning Any reserved HTML characters in the string value for this property
|
||||||
|
* must have been replaced by character entities e.g. "&" becomes "&".
|
||||||
|
*/
|
||||||
|
@property(nonatomic, copy) NSString* footer;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This method is the designated initializer for the class.
|
||||||
|
*/
|
||||||
- (instancetype)initWithUploadDirectory:(NSString*)path;
|
- (instancetype)initWithUploadDirectory:(NSString*)path;
|
||||||
|
|
||||||
@end
|
@end
|
||||||
|
|
||||||
// These methods can be called from any thread
|
/**
|
||||||
|
* Hooks to customize the behavior of GCDWebUploader.
|
||||||
|
*
|
||||||
|
* @warning These methods can be called on any GCD thread.
|
||||||
|
*/
|
||||||
@interface GCDWebUploader (Subclassing)
|
@interface GCDWebUploader (Subclassing)
|
||||||
- (BOOL)shouldUploadFileAtPath:(NSString*)path withTemporaryFile:(NSString*)tempPath; // Default implementation returns YES
|
|
||||||
- (BOOL)shouldMoveItemFromPath:(NSString*)fromPath toPath:(NSString*)toPath; // Default implementation returns YES
|
/**
|
||||||
- (BOOL)shouldDeleteItemAtPath:(NSString*)path; // Default implementation returns YES
|
* This method is called to check if a file upload is allowed to complete.
|
||||||
- (BOOL)shouldCreateDirectoryAtPath:(NSString*)path; // Default implementation returns YES
|
* The uploaded file is available for inspection at "tempPath".
|
||||||
|
*
|
||||||
|
* The default implementation returns YES.
|
||||||
|
*/
|
||||||
|
- (BOOL)shouldUploadFileAtPath:(NSString*)path withTemporaryFile:(NSString*)tempPath;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This method is called to check if a file or directory is allowed to be moved.
|
||||||
|
*
|
||||||
|
* The default implementation returns YES.
|
||||||
|
*/
|
||||||
|
- (BOOL)shouldMoveItemFromPath:(NSString*)fromPath toPath:(NSString*)toPath;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This method is called to check if a file or directory is allowed to be deleted.
|
||||||
|
*
|
||||||
|
* The default implementation returns YES.
|
||||||
|
*/
|
||||||
|
- (BOOL)shouldDeleteItemAtPath:(NSString*)path;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This method is called to check if a directory is allowed to be created.
|
||||||
|
*
|
||||||
|
* The default implementation returns YES.
|
||||||
|
*/
|
||||||
|
- (BOOL)shouldCreateDirectoryAtPath:(NSString*)path;
|
||||||
|
|
||||||
@end
|
@end
|
||||||
|
|
||||||
|
NS_ASSUME_NONNULL_END
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
Copyright (c) 2012-2014, Pierre-Olivier Latour
|
Copyright (c) 2012-2019, Pierre-Olivier Latour
|
||||||
All rights reserved.
|
All rights reserved.
|
||||||
|
|
||||||
Redistribution and use in source and binary forms, with or without
|
Redistribution and use in source and binary forms, with or without
|
||||||
@@ -25,6 +25,10 @@
|
|||||||
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
#if !__has_feature(objc_arc)
|
||||||
|
#error GCDWebUploader requires ARC
|
||||||
|
#endif
|
||||||
|
|
||||||
#import <TargetConditionals.h>
|
#import <TargetConditionals.h>
|
||||||
#if TARGET_OS_IPHONE
|
#if TARGET_OS_IPHONE
|
||||||
#import <UIKit/UIKit.h>
|
#import <UIKit/UIKit.h>
|
||||||
@@ -33,6 +37,7 @@
|
|||||||
#endif
|
#endif
|
||||||
|
|
||||||
#import "GCDWebUploader.h"
|
#import "GCDWebUploader.h"
|
||||||
|
#import "GCDWebServerFunctions.h"
|
||||||
|
|
||||||
#import "GCDWebServerDataRequest.h"
|
#import "GCDWebServerDataRequest.h"
|
||||||
#import "GCDWebServerMultiPartFormRequest.h"
|
#import "GCDWebServerMultiPartFormRequest.h"
|
||||||
@@ -42,29 +47,163 @@
|
|||||||
#import "GCDWebServerErrorResponse.h"
|
#import "GCDWebServerErrorResponse.h"
|
||||||
#import "GCDWebServerFileResponse.h"
|
#import "GCDWebServerFileResponse.h"
|
||||||
|
|
||||||
@interface GCDWebUploader () {
|
NS_ASSUME_NONNULL_BEGIN
|
||||||
@private
|
|
||||||
NSString* _uploadDirectory;
|
@interface GCDWebUploader (Methods)
|
||||||
NSArray* _allowedExtensions;
|
- (nullable GCDWebServerResponse*)listDirectory:(GCDWebServerRequest*)request;
|
||||||
BOOL _showHidden;
|
- (nullable GCDWebServerResponse*)downloadFile:(GCDWebServerRequest*)request;
|
||||||
NSString* _title;
|
- (nullable GCDWebServerResponse*)uploadFile:(GCDWebServerMultiPartFormRequest*)request;
|
||||||
NSString* _header;
|
- (nullable GCDWebServerResponse*)moveItem:(GCDWebServerURLEncodedFormRequest*)request;
|
||||||
NSString* _prologue;
|
- (nullable GCDWebServerResponse*)deleteItem:(GCDWebServerURLEncodedFormRequest*)request;
|
||||||
NSString* _epilogue;
|
- (nullable GCDWebServerResponse*)createDirectory:(GCDWebServerURLEncodedFormRequest*)request;
|
||||||
NSString* _footer;
|
@end
|
||||||
|
|
||||||
|
NS_ASSUME_NONNULL_END
|
||||||
|
|
||||||
|
@implementation GCDWebUploader
|
||||||
|
|
||||||
|
@dynamic delegate;
|
||||||
|
|
||||||
|
- (instancetype)initWithUploadDirectory:(NSString*)path {
|
||||||
|
if ((self = [super init])) {
|
||||||
|
NSString* bundlePath = [[NSBundle bundleForClass:[GCDWebUploader class]] pathForResource:@"GCDWebUploader" ofType:@"bundle"];
|
||||||
|
if (bundlePath == nil) {
|
||||||
|
return nil;
|
||||||
|
}
|
||||||
|
NSBundle* siteBundle = [NSBundle bundleWithPath:bundlePath];
|
||||||
|
if (siteBundle == nil) {
|
||||||
|
return nil;
|
||||||
|
}
|
||||||
|
_uploadDirectory = [path copy];
|
||||||
|
GCDWebUploader* __unsafe_unretained server = self;
|
||||||
|
|
||||||
|
// Resource files
|
||||||
|
[self addGETHandlerForBasePath:@"/" directoryPath:(NSString*)[siteBundle resourcePath] indexFilename:nil cacheAge:3600 allowRangeRequests:NO];
|
||||||
|
|
||||||
|
// Web page
|
||||||
|
[self addHandlerForMethod:@"GET"
|
||||||
|
path:@"/"
|
||||||
|
requestClass:[GCDWebServerRequest class]
|
||||||
|
processBlock:^GCDWebServerResponse*(GCDWebServerRequest* request) {
|
||||||
|
|
||||||
|
#if TARGET_OS_IPHONE
|
||||||
|
NSString* device = [[UIDevice currentDevice] name];
|
||||||
|
#else
|
||||||
|
NSString* device = CFBridgingRelease(SCDynamicStoreCopyComputerName(NULL, NULL));
|
||||||
|
#endif
|
||||||
|
NSString* title = server.title;
|
||||||
|
if (title == nil) {
|
||||||
|
title = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleDisplayName"];
|
||||||
|
if (title == nil) {
|
||||||
|
title = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleName"];
|
||||||
|
}
|
||||||
|
#if !TARGET_OS_IPHONE
|
||||||
|
if (title == nil) {
|
||||||
|
title = [[NSProcessInfo processInfo] processName];
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
NSString* header = server.header;
|
||||||
|
if (header == nil) {
|
||||||
|
header = title;
|
||||||
|
}
|
||||||
|
NSString* prologue = server.prologue;
|
||||||
|
if (prologue == nil) {
|
||||||
|
prologue = [siteBundle localizedStringForKey:@"PROLOGUE" value:@"" table:nil];
|
||||||
|
}
|
||||||
|
NSString* epilogue = server.epilogue;
|
||||||
|
if (epilogue == nil) {
|
||||||
|
epilogue = [siteBundle localizedStringForKey:@"EPILOGUE" value:@"" table:nil];
|
||||||
|
}
|
||||||
|
NSString* footer = server.footer;
|
||||||
|
if (footer == nil) {
|
||||||
|
NSString* name = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleDisplayName"];
|
||||||
|
if (name == nil) {
|
||||||
|
name = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleName"];
|
||||||
|
}
|
||||||
|
NSString* version = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleShortVersionString"];
|
||||||
|
#if !TARGET_OS_IPHONE
|
||||||
|
if (!name && !version) {
|
||||||
|
name = @"OS X";
|
||||||
|
version = [[NSProcessInfo processInfo] operatingSystemVersionString];
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
footer = [NSString stringWithFormat:[siteBundle localizedStringForKey:@"FOOTER_FORMAT" value:@"" table:nil], name, version];
|
||||||
|
}
|
||||||
|
return [GCDWebServerDataResponse responseWithHTMLTemplate:(NSString*)[siteBundle pathForResource:@"index" ofType:@"html"]
|
||||||
|
variables:@{
|
||||||
|
@"device" : device,
|
||||||
|
@"title" : title,
|
||||||
|
@"header" : header,
|
||||||
|
@"prologue" : prologue,
|
||||||
|
@"epilogue" : epilogue,
|
||||||
|
@"footer" : footer
|
||||||
|
}];
|
||||||
|
}];
|
||||||
|
|
||||||
|
// File listing
|
||||||
|
[self addHandlerForMethod:@"GET"
|
||||||
|
path:@"/list"
|
||||||
|
requestClass:[GCDWebServerRequest class]
|
||||||
|
processBlock:^GCDWebServerResponse*(GCDWebServerRequest* request) {
|
||||||
|
return [server listDirectory:request];
|
||||||
|
}];
|
||||||
|
|
||||||
|
// File download
|
||||||
|
[self addHandlerForMethod:@"GET"
|
||||||
|
path:@"/download"
|
||||||
|
requestClass:[GCDWebServerRequest class]
|
||||||
|
processBlock:^GCDWebServerResponse*(GCDWebServerRequest* request) {
|
||||||
|
return [server downloadFile:request];
|
||||||
|
}];
|
||||||
|
|
||||||
|
// File upload
|
||||||
|
[self addHandlerForMethod:@"POST"
|
||||||
|
path:@"/upload"
|
||||||
|
requestClass:[GCDWebServerMultiPartFormRequest class]
|
||||||
|
processBlock:^GCDWebServerResponse*(GCDWebServerRequest* request) {
|
||||||
|
return [server uploadFile:(GCDWebServerMultiPartFormRequest*)request];
|
||||||
|
}];
|
||||||
|
|
||||||
|
// File and folder moving
|
||||||
|
[self addHandlerForMethod:@"POST"
|
||||||
|
path:@"/move"
|
||||||
|
requestClass:[GCDWebServerURLEncodedFormRequest class]
|
||||||
|
processBlock:^GCDWebServerResponse*(GCDWebServerRequest* request) {
|
||||||
|
return [server moveItem:(GCDWebServerURLEncodedFormRequest*)request];
|
||||||
|
}];
|
||||||
|
|
||||||
|
// File and folder deletion
|
||||||
|
[self addHandlerForMethod:@"POST"
|
||||||
|
path:@"/delete"
|
||||||
|
requestClass:[GCDWebServerURLEncodedFormRequest class]
|
||||||
|
processBlock:^GCDWebServerResponse*(GCDWebServerRequest* request) {
|
||||||
|
return [server deleteItem:(GCDWebServerURLEncodedFormRequest*)request];
|
||||||
|
}];
|
||||||
|
|
||||||
|
// Directory creation
|
||||||
|
[self addHandlerForMethod:@"POST"
|
||||||
|
path:@"/create"
|
||||||
|
requestClass:[GCDWebServerURLEncodedFormRequest class]
|
||||||
|
processBlock:^GCDWebServerResponse*(GCDWebServerRequest* request) {
|
||||||
|
return [server createDirectory:(GCDWebServerURLEncodedFormRequest*)request];
|
||||||
|
}];
|
||||||
|
}
|
||||||
|
return self;
|
||||||
}
|
}
|
||||||
|
|
||||||
@end
|
@end
|
||||||
|
|
||||||
@implementation GCDWebUploader (Methods)
|
@implementation GCDWebUploader (Methods)
|
||||||
|
|
||||||
- (BOOL)_checkFileExtension:(NSString*)fileName {
|
- (BOOL)_checkFileExtension:(NSString*)fileName {
|
||||||
if (_allowedExtensions && ![_allowedExtensions containsObject:[[fileName pathExtension] lowercaseString]]) {
|
if (_allowedFileExtensions && ![_allowedFileExtensions containsObject:[[fileName pathExtension] lowercaseString]]) {
|
||||||
return NO;
|
return NO;
|
||||||
}
|
}
|
||||||
return YES;
|
return YES;
|
||||||
}
|
}
|
||||||
|
|
||||||
- (NSString*) _uniquePathForPath:(NSString*)path {
|
- (NSString*)_uniquePathForPath:(NSString*)path {
|
||||||
if ([[NSFileManager defaultManager] fileExistsAtPath:path]) {
|
if ([[NSFileManager defaultManager] fileExistsAtPath:path]) {
|
||||||
NSString* directory = [path stringByDeletingLastPathComponent];
|
NSString* directory = [path stringByDeletingLastPathComponent];
|
||||||
NSString* file = [path lastPathComponent];
|
NSString* file = [path lastPathComponent];
|
||||||
@@ -73,7 +212,7 @@
|
|||||||
int retries = 0;
|
int retries = 0;
|
||||||
do {
|
do {
|
||||||
if (extension.length) {
|
if (extension.length) {
|
||||||
path = [directory stringByAppendingPathComponent:[[base stringByAppendingFormat:@" (%i)", ++retries] stringByAppendingPathExtension:extension]];
|
path = [directory stringByAppendingPathComponent:(NSString*)[[base stringByAppendingFormat:@" (%i)", ++retries] stringByAppendingPathExtension:extension]];
|
||||||
} else {
|
} else {
|
||||||
path = [directory stringByAppendingPathComponent:[base stringByAppendingFormat:@" (%i)", ++retries]];
|
path = [directory stringByAppendingPathComponent:[base stringByAppendingFormat:@" (%i)", ++retries]];
|
||||||
}
|
}
|
||||||
@@ -84,42 +223,42 @@
|
|||||||
|
|
||||||
- (GCDWebServerResponse*)listDirectory:(GCDWebServerRequest*)request {
|
- (GCDWebServerResponse*)listDirectory:(GCDWebServerRequest*)request {
|
||||||
NSString* relativePath = [[request query] objectForKey:@"path"];
|
NSString* relativePath = [[request query] objectForKey:@"path"];
|
||||||
NSString* absolutePath = [_uploadDirectory stringByAppendingPathComponent:relativePath];
|
NSString* absolutePath = [_uploadDirectory stringByAppendingPathComponent:GCDWebServerNormalizePath(relativePath)];
|
||||||
BOOL isDirectory;
|
BOOL isDirectory = NO;
|
||||||
if (![[NSFileManager defaultManager] fileExistsAtPath:absolutePath isDirectory:&isDirectory]) {
|
if (!absolutePath || ![[NSFileManager defaultManager] fileExistsAtPath:absolutePath isDirectory:&isDirectory]) {
|
||||||
return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_NotFound message:@"\"%@\" does not exist", relativePath];
|
return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_NotFound message:@"\"%@\" does not exist", relativePath];
|
||||||
}
|
}
|
||||||
if (!isDirectory) {
|
if (!isDirectory) {
|
||||||
return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_BadRequest message:@"\"%@\" is not a directory", relativePath];
|
return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_BadRequest message:@"\"%@\" is not a directory", relativePath];
|
||||||
}
|
}
|
||||||
|
|
||||||
NSString* directoryName = [absolutePath lastPathComponent];
|
NSString* directoryName = [absolutePath lastPathComponent];
|
||||||
if (!_showHidden && [directoryName hasPrefix:@"."]) {
|
if (!_allowHiddenItems && [directoryName hasPrefix:@"."]) {
|
||||||
return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_Forbidden message:@"Listing directory name \"%@\" is not allowed", directoryName];
|
return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_Forbidden message:@"Listing directory name \"%@\" is not allowed", directoryName];
|
||||||
}
|
}
|
||||||
|
|
||||||
NSError* error = nil;
|
NSError* error = nil;
|
||||||
NSArray* contents = [[NSFileManager defaultManager] contentsOfDirectoryAtPath:absolutePath error:&error];
|
NSArray* contents = [[[NSFileManager defaultManager] contentsOfDirectoryAtPath:absolutePath error:&error] sortedArrayUsingSelector:@selector(localizedStandardCompare:)];
|
||||||
if (contents == nil) {
|
if (contents == nil) {
|
||||||
return [GCDWebServerErrorResponse responseWithServerError:kGCDWebServerHTTPStatusCode_InternalServerError underlyingError:error message:@"Failed listing directory \"%@\"", relativePath];
|
return [GCDWebServerErrorResponse responseWithServerError:kGCDWebServerHTTPStatusCode_InternalServerError underlyingError:error message:@"Failed listing directory \"%@\"", relativePath];
|
||||||
}
|
}
|
||||||
|
|
||||||
NSMutableArray* array = [NSMutableArray array];
|
NSMutableArray* array = [NSMutableArray array];
|
||||||
for (NSString* item in [contents sortedArrayUsingSelector:@selector(localizedStandardCompare:)]) {
|
for (NSString* item in [contents sortedArrayUsingSelector:@selector(localizedStandardCompare:)]) {
|
||||||
if (_showHidden || ![item hasPrefix:@"."]) {
|
if (_allowHiddenItems || ![item hasPrefix:@"."]) {
|
||||||
NSDictionary* attributes = [[NSFileManager defaultManager] attributesOfItemAtPath:[absolutePath stringByAppendingPathComponent:item] error:NULL];
|
NSDictionary* attributes = [[NSFileManager defaultManager] attributesOfItemAtPath:[absolutePath stringByAppendingPathComponent:item] error:NULL];
|
||||||
NSString* type = [attributes objectForKey:NSFileType];
|
NSString* type = [attributes objectForKey:NSFileType];
|
||||||
if ([type isEqualToString:NSFileTypeRegular] && [self _checkFileExtension:item]) {
|
if ([type isEqualToString:NSFileTypeRegular] && [self _checkFileExtension:item]) {
|
||||||
[array addObject:@{
|
[array addObject:@{
|
||||||
@"path": [relativePath stringByAppendingPathComponent:item],
|
@"path" : [relativePath stringByAppendingPathComponent:item],
|
||||||
@"name": item,
|
@"name" : item,
|
||||||
@"size": [attributes objectForKey:NSFileSize]
|
@"size" : (NSNumber*)[attributes objectForKey:NSFileSize]
|
||||||
}];
|
}];
|
||||||
} else if ([type isEqualToString:NSFileTypeDirectory]) {
|
} else if ([type isEqualToString:NSFileTypeDirectory]) {
|
||||||
[array addObject:@{
|
[array addObject:@{
|
||||||
@"path": [[relativePath stringByAppendingPathComponent:item] stringByAppendingString:@"/"],
|
@"path" : [[relativePath stringByAppendingPathComponent:item] stringByAppendingString:@"/"],
|
||||||
@"name": item
|
@"name" : item
|
||||||
}];
|
}];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -128,21 +267,21 @@
|
|||||||
|
|
||||||
- (GCDWebServerResponse*)downloadFile:(GCDWebServerRequest*)request {
|
- (GCDWebServerResponse*)downloadFile:(GCDWebServerRequest*)request {
|
||||||
NSString* relativePath = [[request query] objectForKey:@"path"];
|
NSString* relativePath = [[request query] objectForKey:@"path"];
|
||||||
NSString* absolutePath = [_uploadDirectory stringByAppendingPathComponent:relativePath];
|
NSString* absolutePath = [_uploadDirectory stringByAppendingPathComponent:GCDWebServerNormalizePath(relativePath)];
|
||||||
BOOL isDirectory;
|
BOOL isDirectory = NO;
|
||||||
if (![[NSFileManager defaultManager] fileExistsAtPath:absolutePath isDirectory:&isDirectory]) {
|
if (![[NSFileManager defaultManager] fileExistsAtPath:absolutePath isDirectory:&isDirectory]) {
|
||||||
return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_NotFound message:@"\"%@\" does not exist", relativePath];
|
return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_NotFound message:@"\"%@\" does not exist", relativePath];
|
||||||
}
|
}
|
||||||
if (isDirectory) {
|
if (isDirectory) {
|
||||||
return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_BadRequest message:@"\"%@\" is a directory", relativePath];
|
return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_BadRequest message:@"\"%@\" is a directory", relativePath];
|
||||||
}
|
}
|
||||||
|
|
||||||
NSString* fileName = [absolutePath lastPathComponent];
|
NSString* fileName = [absolutePath lastPathComponent];
|
||||||
if (([fileName hasPrefix:@"."] && !_showHidden) || ![self _checkFileExtension:fileName]) {
|
if (([fileName hasPrefix:@"."] && !_allowHiddenItems) || ![self _checkFileExtension:fileName]) {
|
||||||
return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_Forbidden message:@"Downlading file name \"%@\" is not allowed", fileName];
|
return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_Forbidden message:@"Downlading file name \"%@\" is not allowed", fileName];
|
||||||
}
|
}
|
||||||
|
|
||||||
if ([self.delegate respondsToSelector:@selector(webUploader:didDownloadFileAtPath: )]) {
|
if ([self.delegate respondsToSelector:@selector(webUploader:didDownloadFileAtPath:)]) {
|
||||||
dispatch_async(dispatch_get_main_queue(), ^{
|
dispatch_async(dispatch_get_main_queue(), ^{
|
||||||
[self.delegate webUploader:self didDownloadFileAtPath:absolutePath];
|
[self.delegate webUploader:self didDownloadFileAtPath:absolutePath];
|
||||||
});
|
});
|
||||||
@@ -153,23 +292,23 @@
|
|||||||
- (GCDWebServerResponse*)uploadFile:(GCDWebServerMultiPartFormRequest*)request {
|
- (GCDWebServerResponse*)uploadFile:(GCDWebServerMultiPartFormRequest*)request {
|
||||||
NSRange range = [[request.headers objectForKey:@"Accept"] rangeOfString:@"application/json" options:NSCaseInsensitiveSearch];
|
NSRange range = [[request.headers objectForKey:@"Accept"] rangeOfString:@"application/json" options:NSCaseInsensitiveSearch];
|
||||||
NSString* contentType = (range.location != NSNotFound ? @"application/json" : @"text/plain; charset=utf-8"); // Required when using iFrame transport (see https://github.com/blueimp/jQuery-File-Upload/wiki/Setup)
|
NSString* contentType = (range.location != NSNotFound ? @"application/json" : @"text/plain; charset=utf-8"); // Required when using iFrame transport (see https://github.com/blueimp/jQuery-File-Upload/wiki/Setup)
|
||||||
|
|
||||||
GCDWebServerMultiPartFile* file = [request.files objectForKey:@"files[]"];
|
GCDWebServerMultiPartFile* file = [request firstFileForControlName:@"files[]"];
|
||||||
if ((!_showHidden && [file.fileName hasPrefix:@"."]) || ![self _checkFileExtension:file.fileName]) {
|
if ((!_allowHiddenItems && [file.fileName hasPrefix:@"."]) || ![self _checkFileExtension:file.fileName]) {
|
||||||
return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_Forbidden message:@"Uploaded file name \"%@\" is not allowed", file.fileName];
|
return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_Forbidden message:@"Uploaded file name \"%@\" is not allowed", file.fileName];
|
||||||
}
|
}
|
||||||
NSString* relativePath = [(GCDWebServerMultiPartArgument*)[request.arguments objectForKey:@"path"] string];
|
NSString* relativePath = [[request firstArgumentForControlName:@"path"] string];
|
||||||
NSString* absolutePath = [self _uniquePathForPath:[[_uploadDirectory stringByAppendingPathComponent:relativePath] stringByAppendingPathComponent:file.fileName]];
|
NSString* absolutePath = [self _uniquePathForPath:[[_uploadDirectory stringByAppendingPathComponent:GCDWebServerNormalizePath(relativePath)] stringByAppendingPathComponent:file.fileName]];
|
||||||
|
|
||||||
if (![self shouldUploadFileAtPath:absolutePath withTemporaryFile:file.temporaryPath]) {
|
if (![self shouldUploadFileAtPath:absolutePath withTemporaryFile:file.temporaryPath]) {
|
||||||
return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_Forbidden message:@"Uploading file \"%@\" to \"%@\" is not permitted", file.fileName, relativePath];
|
return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_Forbidden message:@"Uploading file \"%@\" to \"%@\" is not permitted", file.fileName, relativePath];
|
||||||
}
|
}
|
||||||
|
|
||||||
NSError* error = nil;
|
NSError* error = nil;
|
||||||
if (![[NSFileManager defaultManager] moveItemAtPath:file.temporaryPath toPath:absolutePath error:&error]) {
|
if (![[NSFileManager defaultManager] moveItemAtPath:file.temporaryPath toPath:absolutePath error:&error]) {
|
||||||
return [GCDWebServerErrorResponse responseWithServerError:kGCDWebServerHTTPStatusCode_InternalServerError underlyingError:error message:@"Failed moving uploaded file to \"%@\"", relativePath];
|
return [GCDWebServerErrorResponse responseWithServerError:kGCDWebServerHTTPStatusCode_InternalServerError underlyingError:error message:@"Failed moving uploaded file to \"%@\"", relativePath];
|
||||||
}
|
}
|
||||||
|
|
||||||
if ([self.delegate respondsToSelector:@selector(webUploader:didUploadFileAtPath:)]) {
|
if ([self.delegate respondsToSelector:@selector(webUploader:didUploadFileAtPath:)]) {
|
||||||
dispatch_async(dispatch_get_main_queue(), ^{
|
dispatch_async(dispatch_get_main_queue(), ^{
|
||||||
[self.delegate webUploader:self didUploadFileAtPath:absolutePath];
|
[self.delegate webUploader:self didUploadFileAtPath:absolutePath];
|
||||||
@@ -180,29 +319,34 @@
|
|||||||
|
|
||||||
- (GCDWebServerResponse*)moveItem:(GCDWebServerURLEncodedFormRequest*)request {
|
- (GCDWebServerResponse*)moveItem:(GCDWebServerURLEncodedFormRequest*)request {
|
||||||
NSString* oldRelativePath = [request.arguments objectForKey:@"oldPath"];
|
NSString* oldRelativePath = [request.arguments objectForKey:@"oldPath"];
|
||||||
NSString* oldAbsolutePath = [_uploadDirectory stringByAppendingPathComponent:oldRelativePath];
|
NSString* oldAbsolutePath = [_uploadDirectory stringByAppendingPathComponent:GCDWebServerNormalizePath(oldRelativePath)];
|
||||||
BOOL isDirectory;
|
BOOL isDirectory = NO;
|
||||||
if (![[NSFileManager defaultManager] fileExistsAtPath:oldAbsolutePath isDirectory:&isDirectory]) {
|
if (![[NSFileManager defaultManager] fileExistsAtPath:oldAbsolutePath isDirectory:&isDirectory]) {
|
||||||
return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_NotFound message:@"\"%@\" does not exist", oldRelativePath];
|
return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_NotFound message:@"\"%@\" does not exist", oldRelativePath];
|
||||||
}
|
}
|
||||||
|
|
||||||
NSString* newRelativePath = [request.arguments objectForKey:@"newPath"];
|
NSString* oldItemName = [oldAbsolutePath lastPathComponent];
|
||||||
NSString* newAbsolutePath = [self _uniquePathForPath:[_uploadDirectory stringByAppendingPathComponent:newRelativePath]];
|
if ((!_allowHiddenItems && [oldItemName hasPrefix:@"."]) || (!isDirectory && ![self _checkFileExtension:oldItemName])) {
|
||||||
|
return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_Forbidden message:@"Moving from item name \"%@\" is not allowed", oldItemName];
|
||||||
NSString* itemName = [newAbsolutePath lastPathComponent];
|
|
||||||
if ((!_showHidden && [itemName hasPrefix:@"."]) || (!isDirectory && ![self _checkFileExtension:itemName])) {
|
|
||||||
return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_Forbidden message:@"Moving to item name \"%@\" is not allowed", itemName];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
NSString* newRelativePath = [request.arguments objectForKey:@"newPath"];
|
||||||
|
NSString* newAbsolutePath = [self _uniquePathForPath:[_uploadDirectory stringByAppendingPathComponent:GCDWebServerNormalizePath(newRelativePath)]];
|
||||||
|
|
||||||
|
NSString* newItemName = [newAbsolutePath lastPathComponent];
|
||||||
|
if ((!_allowHiddenItems && [newItemName hasPrefix:@"."]) || (!isDirectory && ![self _checkFileExtension:newItemName])) {
|
||||||
|
return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_Forbidden message:@"Moving to item name \"%@\" is not allowed", newItemName];
|
||||||
|
}
|
||||||
|
|
||||||
if (![self shouldMoveItemFromPath:oldAbsolutePath toPath:newAbsolutePath]) {
|
if (![self shouldMoveItemFromPath:oldAbsolutePath toPath:newAbsolutePath]) {
|
||||||
return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_Forbidden message:@"Moving \"%@\" to \"%@\" is not permitted", oldRelativePath, newRelativePath];
|
return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_Forbidden message:@"Moving \"%@\" to \"%@\" is not permitted", oldRelativePath, newRelativePath];
|
||||||
}
|
}
|
||||||
|
|
||||||
NSError* error = nil;
|
NSError* error = nil;
|
||||||
if (![[NSFileManager defaultManager] moveItemAtPath:oldAbsolutePath toPath:newAbsolutePath error:&error]) {
|
if (![[NSFileManager defaultManager] moveItemAtPath:oldAbsolutePath toPath:newAbsolutePath error:&error]) {
|
||||||
return [GCDWebServerErrorResponse responseWithServerError:kGCDWebServerHTTPStatusCode_InternalServerError underlyingError:error message:@"Failed moving \"%@\" to \"%@\"", oldRelativePath, newRelativePath];
|
return [GCDWebServerErrorResponse responseWithServerError:kGCDWebServerHTTPStatusCode_InternalServerError underlyingError:error message:@"Failed moving \"%@\" to \"%@\"", oldRelativePath, newRelativePath];
|
||||||
}
|
}
|
||||||
|
|
||||||
if ([self.delegate respondsToSelector:@selector(webUploader:didMoveItemFromPath:toPath:)]) {
|
if ([self.delegate respondsToSelector:@selector(webUploader:didMoveItemFromPath:toPath:)]) {
|
||||||
dispatch_async(dispatch_get_main_queue(), ^{
|
dispatch_async(dispatch_get_main_queue(), ^{
|
||||||
[self.delegate webUploader:self didMoveItemFromPath:oldAbsolutePath toPath:newAbsolutePath];
|
[self.delegate webUploader:self didMoveItemFromPath:oldAbsolutePath toPath:newAbsolutePath];
|
||||||
@@ -213,26 +357,26 @@
|
|||||||
|
|
||||||
- (GCDWebServerResponse*)deleteItem:(GCDWebServerURLEncodedFormRequest*)request {
|
- (GCDWebServerResponse*)deleteItem:(GCDWebServerURLEncodedFormRequest*)request {
|
||||||
NSString* relativePath = [request.arguments objectForKey:@"path"];
|
NSString* relativePath = [request.arguments objectForKey:@"path"];
|
||||||
NSString* absolutePath = [_uploadDirectory stringByAppendingPathComponent:relativePath];
|
NSString* absolutePath = [_uploadDirectory stringByAppendingPathComponent:GCDWebServerNormalizePath(relativePath)];
|
||||||
BOOL isDirectory = NO;
|
BOOL isDirectory = NO;
|
||||||
if (![[NSFileManager defaultManager] fileExistsAtPath:absolutePath isDirectory:&isDirectory]) {
|
if (![[NSFileManager defaultManager] fileExistsAtPath:absolutePath isDirectory:&isDirectory]) {
|
||||||
return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_NotFound message:@"\"%@\" does not exist", relativePath];
|
return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_NotFound message:@"\"%@\" does not exist", relativePath];
|
||||||
}
|
}
|
||||||
|
|
||||||
NSString* itemName = [absolutePath lastPathComponent];
|
NSString* itemName = [absolutePath lastPathComponent];
|
||||||
if (([itemName hasPrefix:@"."] && !_showHidden) || (!isDirectory && ![self _checkFileExtension:itemName])) {
|
if (([itemName hasPrefix:@"."] && !_allowHiddenItems) || (!isDirectory && ![self _checkFileExtension:itemName])) {
|
||||||
return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_Forbidden message:@"Deleting item name \"%@\" is not allowed", itemName];
|
return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_Forbidden message:@"Deleting item name \"%@\" is not allowed", itemName];
|
||||||
}
|
}
|
||||||
|
|
||||||
if (![self shouldDeleteItemAtPath:absolutePath]) {
|
if (![self shouldDeleteItemAtPath:absolutePath]) {
|
||||||
return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_Forbidden message:@"Deleting \"%@\" is not permitted", relativePath];
|
return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_Forbidden message:@"Deleting \"%@\" is not permitted", relativePath];
|
||||||
}
|
}
|
||||||
|
|
||||||
NSError* error = nil;
|
NSError* error = nil;
|
||||||
if (![[NSFileManager defaultManager] removeItemAtPath:absolutePath error:&error]) {
|
if (![[NSFileManager defaultManager] removeItemAtPath:absolutePath error:&error]) {
|
||||||
return [GCDWebServerErrorResponse responseWithServerError:kGCDWebServerHTTPStatusCode_InternalServerError underlyingError:error message:@"Failed deleting \"%@\"", relativePath];
|
return [GCDWebServerErrorResponse responseWithServerError:kGCDWebServerHTTPStatusCode_InternalServerError underlyingError:error message:@"Failed deleting \"%@\"", relativePath];
|
||||||
}
|
}
|
||||||
|
|
||||||
if ([self.delegate respondsToSelector:@selector(webUploader:didDeleteItemAtPath:)]) {
|
if ([self.delegate respondsToSelector:@selector(webUploader:didDeleteItemAtPath:)]) {
|
||||||
dispatch_async(dispatch_get_main_queue(), ^{
|
dispatch_async(dispatch_get_main_queue(), ^{
|
||||||
[self.delegate webUploader:self didDeleteItemAtPath:absolutePath];
|
[self.delegate webUploader:self didDeleteItemAtPath:absolutePath];
|
||||||
@@ -243,22 +387,22 @@
|
|||||||
|
|
||||||
- (GCDWebServerResponse*)createDirectory:(GCDWebServerURLEncodedFormRequest*)request {
|
- (GCDWebServerResponse*)createDirectory:(GCDWebServerURLEncodedFormRequest*)request {
|
||||||
NSString* relativePath = [request.arguments objectForKey:@"path"];
|
NSString* relativePath = [request.arguments objectForKey:@"path"];
|
||||||
NSString* absolutePath = [self _uniquePathForPath:[_uploadDirectory stringByAppendingPathComponent:relativePath]];
|
NSString* absolutePath = [self _uniquePathForPath:[_uploadDirectory stringByAppendingPathComponent:GCDWebServerNormalizePath(relativePath)]];
|
||||||
|
|
||||||
NSString* directoryName = [absolutePath lastPathComponent];
|
NSString* directoryName = [absolutePath lastPathComponent];
|
||||||
if (!_showHidden && [directoryName hasPrefix:@"."]) {
|
if (!_allowHiddenItems && [directoryName hasPrefix:@"."]) {
|
||||||
return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_Forbidden message:@"Creating directory name \"%@\" is not allowed", directoryName];
|
return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_Forbidden message:@"Creating directory name \"%@\" is not allowed", directoryName];
|
||||||
}
|
}
|
||||||
|
|
||||||
if (![self shouldCreateDirectoryAtPath:absolutePath]) {
|
if (![self shouldCreateDirectoryAtPath:absolutePath]) {
|
||||||
return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_Forbidden message:@"Creating directory \"%@\" is not permitted", relativePath];
|
return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_Forbidden message:@"Creating directory \"%@\" is not permitted", relativePath];
|
||||||
}
|
}
|
||||||
|
|
||||||
NSError* error = nil;
|
NSError* error = nil;
|
||||||
if (![[NSFileManager defaultManager] createDirectoryAtPath:absolutePath withIntermediateDirectories:NO attributes:nil error:&error]) {
|
if (![[NSFileManager defaultManager] createDirectoryAtPath:absolutePath withIntermediateDirectories:NO attributes:nil error:&error]) {
|
||||||
return [GCDWebServerErrorResponse responseWithServerError:kGCDWebServerHTTPStatusCode_InternalServerError underlyingError:error message:@"Failed creating directory \"%@\"", relativePath];
|
return [GCDWebServerErrorResponse responseWithServerError:kGCDWebServerHTTPStatusCode_InternalServerError underlyingError:error message:@"Failed creating directory \"%@\"", relativePath];
|
||||||
}
|
}
|
||||||
|
|
||||||
if ([self.delegate respondsToSelector:@selector(webUploader:didCreateDirectoryAtPath:)]) {
|
if ([self.delegate respondsToSelector:@selector(webUploader:didCreateDirectoryAtPath:)]) {
|
||||||
dispatch_async(dispatch_get_main_queue(), ^{
|
dispatch_async(dispatch_get_main_queue(), ^{
|
||||||
[self.delegate webUploader:self didCreateDirectoryAtPath:absolutePath];
|
[self.delegate webUploader:self didCreateDirectoryAtPath:absolutePath];
|
||||||
@@ -269,139 +413,6 @@
|
|||||||
|
|
||||||
@end
|
@end
|
||||||
|
|
||||||
@implementation GCDWebUploader
|
|
||||||
|
|
||||||
@synthesize uploadDirectory=_uploadDirectory, allowedFileExtensions=_allowedExtensions, showHiddenFiles=_showHidden,
|
|
||||||
title=_title, header=_header, prologue=_prologue, epilogue=_epilogue, footer=_footer;
|
|
||||||
|
|
||||||
- (instancetype)initWithUploadDirectory:(NSString*)path {
|
|
||||||
if ((self = [super init])) {
|
|
||||||
NSBundle* siteBundle = [NSBundle bundleWithPath:[[NSBundle mainBundle] pathForResource:@"GCDWebUploader" ofType:@"bundle"]];
|
|
||||||
if (siteBundle == nil) {
|
|
||||||
#if !__has_feature(objc_arc)
|
|
||||||
[self release];
|
|
||||||
#endif
|
|
||||||
return nil;
|
|
||||||
}
|
|
||||||
_uploadDirectory = [[path stringByStandardizingPath] copy];
|
|
||||||
#if __has_feature(objc_arc)
|
|
||||||
GCDWebUploader* __unsafe_unretained server = self;
|
|
||||||
#else
|
|
||||||
__block GCDWebUploader* server = self;
|
|
||||||
#endif
|
|
||||||
|
|
||||||
// Resource files
|
|
||||||
[self addGETHandlerForBasePath:@"/" directoryPath:[siteBundle resourcePath] indexFilename:nil cacheAge:3600 allowRangeRequests:NO];
|
|
||||||
|
|
||||||
// Web page
|
|
||||||
[self addHandlerForMethod:@"GET" path:@"/" requestClass:[GCDWebServerRequest class] processBlock:^GCDWebServerResponse *(GCDWebServerRequest* request) {
|
|
||||||
|
|
||||||
#if TARGET_OS_IPHONE
|
|
||||||
NSString* device = [[UIDevice currentDevice] name];
|
|
||||||
#else
|
|
||||||
#if __has_feature(objc_arc)
|
|
||||||
NSString* device = CFBridgingRelease(SCDynamicStoreCopyComputerName(NULL, NULL));
|
|
||||||
#else
|
|
||||||
NSString* device = [(id)SCDynamicStoreCopyComputerName(NULL, NULL) autorelease];
|
|
||||||
#endif
|
|
||||||
#endif
|
|
||||||
NSString* title = server.title;
|
|
||||||
if (title == nil) {
|
|
||||||
title = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleDisplayName"];
|
|
||||||
#if !TARGET_OS_IPHONE
|
|
||||||
if (title == nil) {
|
|
||||||
title = [[NSProcessInfo processInfo] processName];
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
}
|
|
||||||
NSString* header = server.header;
|
|
||||||
if (header == nil) {
|
|
||||||
header = title;
|
|
||||||
}
|
|
||||||
NSString* prologue = server.prologue;
|
|
||||||
if (prologue == nil) {
|
|
||||||
prologue = [siteBundle localizedStringForKey:@"PROLOGUE" value:@"" table:nil];
|
|
||||||
}
|
|
||||||
NSString* epilogue = server.epilogue;
|
|
||||||
if (epilogue == nil) {
|
|
||||||
epilogue = [siteBundle localizedStringForKey:@"EPILOGUE" value:@"" table:nil];
|
|
||||||
}
|
|
||||||
NSString* footer = server.footer;
|
|
||||||
if (footer == nil) {
|
|
||||||
NSString* name = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleDisplayName"];
|
|
||||||
NSString* version = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleShortVersionString"];
|
|
||||||
#if !TARGET_OS_IPHONE
|
|
||||||
if (!name && !version) {
|
|
||||||
name = @"OS X";
|
|
||||||
version = [[NSProcessInfo processInfo] operatingSystemVersionString];
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
footer = [NSString stringWithFormat:[siteBundle localizedStringForKey:@"FOOTER_FORMAT" value:@"" table:nil], name, version];
|
|
||||||
}
|
|
||||||
return [GCDWebServerDataResponse responseWithHTMLTemplate:[siteBundle pathForResource:@"index" ofType:@"html"]
|
|
||||||
variables:@{
|
|
||||||
@"device": device,
|
|
||||||
@"title": title,
|
|
||||||
@"header": header,
|
|
||||||
@"prologue": prologue,
|
|
||||||
@"epilogue": epilogue,
|
|
||||||
@"footer": footer
|
|
||||||
}];
|
|
||||||
|
|
||||||
}];
|
|
||||||
|
|
||||||
// File listing
|
|
||||||
[self addHandlerForMethod:@"GET" path:@"/list" requestClass:[GCDWebServerRequest class] processBlock:^GCDWebServerResponse *(GCDWebServerRequest* request) {
|
|
||||||
return [server listDirectory:request];
|
|
||||||
}];
|
|
||||||
|
|
||||||
// File download
|
|
||||||
[self addHandlerForMethod:@"GET" path:@"/download" requestClass:[GCDWebServerRequest class] processBlock:^GCDWebServerResponse *(GCDWebServerRequest* request) {
|
|
||||||
return [server downloadFile:request];
|
|
||||||
}];
|
|
||||||
|
|
||||||
// File upload
|
|
||||||
[self addHandlerForMethod:@"POST" path:@"/upload" requestClass:[GCDWebServerMultiPartFormRequest class] processBlock:^GCDWebServerResponse *(GCDWebServerRequest* request) {
|
|
||||||
return [server uploadFile:(GCDWebServerMultiPartFormRequest*)request];
|
|
||||||
}];
|
|
||||||
|
|
||||||
// File and folder moving
|
|
||||||
[self addHandlerForMethod:@"POST" path:@"/move" requestClass:[GCDWebServerURLEncodedFormRequest class] processBlock:^GCDWebServerResponse *(GCDWebServerRequest* request) {
|
|
||||||
return [server moveItem:(GCDWebServerURLEncodedFormRequest*)request];
|
|
||||||
}];
|
|
||||||
|
|
||||||
// File and folder deletion
|
|
||||||
[self addHandlerForMethod:@"POST" path:@"/delete" requestClass:[GCDWebServerURLEncodedFormRequest class] processBlock:^GCDWebServerResponse *(GCDWebServerRequest* request) {
|
|
||||||
return [server deleteItem:(GCDWebServerURLEncodedFormRequest*)request];
|
|
||||||
}];
|
|
||||||
|
|
||||||
// Directory creation
|
|
||||||
[self addHandlerForMethod:@"POST" path:@"/create" requestClass:[GCDWebServerURLEncodedFormRequest class] processBlock:^GCDWebServerResponse *(GCDWebServerRequest* request) {
|
|
||||||
return [server createDirectory:(GCDWebServerURLEncodedFormRequest*)request];
|
|
||||||
}];
|
|
||||||
|
|
||||||
}
|
|
||||||
return self;
|
|
||||||
}
|
|
||||||
|
|
||||||
#if !__has_feature(objc_arc)
|
|
||||||
|
|
||||||
- (void)dealloc {
|
|
||||||
[_uploadDirectory release];
|
|
||||||
[_allowedExtensions release];
|
|
||||||
[_title release];
|
|
||||||
[_header release];
|
|
||||||
[_prologue release];
|
|
||||||
[_epilogue release];
|
|
||||||
[_footer release];
|
|
||||||
|
|
||||||
[super dealloc];
|
|
||||||
}
|
|
||||||
|
|
||||||
#endif
|
|
||||||
|
|
||||||
@end
|
|
||||||
|
|
||||||
@implementation GCDWebUploader (Subclassing)
|
@implementation GCDWebUploader (Subclassing)
|
||||||
|
|
||||||
- (BOOL)shouldUploadFileAtPath:(NSString*)path withTemporaryFile:(NSString*)tempPath {
|
- (BOOL)shouldUploadFileAtPath:(NSString*)path withTemporaryFile:(NSString*)tempPath {
|
||||||
|
|||||||
276
Mac/main.m
276
Mac/main.m
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
Copyright (c) 2012-2014, Pierre-Olivier Latour
|
Copyright (c) 2012-2019, Pierre-Olivier Latour
|
||||||
All rights reserved.
|
All rights reserved.
|
||||||
|
|
||||||
Redistribution and use in source and binary forms, with or without
|
Redistribution and use in source and binary forms, with or without
|
||||||
@@ -31,9 +31,10 @@
|
|||||||
|
|
||||||
#import "GCDWebServerDataRequest.h"
|
#import "GCDWebServerDataRequest.h"
|
||||||
#import "GCDWebServerURLEncodedFormRequest.h"
|
#import "GCDWebServerURLEncodedFormRequest.h"
|
||||||
|
#import "GCDWebServerMultiPartFormRequest.h"
|
||||||
|
|
||||||
#import "GCDWebServerDataResponse.h"
|
#import "GCDWebServerDataResponse.h"
|
||||||
#import "GCDWebServerStreamingResponse.h"
|
#import "GCDWebServerStreamedResponse.h"
|
||||||
|
|
||||||
#import "GCDWebDAVServer.h"
|
#import "GCDWebDAVServer.h"
|
||||||
|
|
||||||
@@ -47,9 +48,11 @@ typedef enum {
|
|||||||
kMode_WebServer = 0,
|
kMode_WebServer = 0,
|
||||||
kMode_HTMLPage,
|
kMode_HTMLPage,
|
||||||
kMode_HTMLForm,
|
kMode_HTMLForm,
|
||||||
|
kMode_HTMLFileUpload,
|
||||||
kMode_WebDAV,
|
kMode_WebDAV,
|
||||||
kMode_WebUploader,
|
kMode_WebUploader,
|
||||||
kMode_StreamingResponse
|
kMode_StreamingResponse,
|
||||||
|
kMode_AsyncResponse
|
||||||
} Mode;
|
} Mode;
|
||||||
|
|
||||||
@interface Delegate : NSObject <GCDWebServerDelegate, GCDWebDAVServerDelegate, GCDWebUploaderDelegate>
|
@interface Delegate : NSObject <GCDWebServerDelegate, GCDWebDAVServerDelegate, GCDWebUploaderDelegate>
|
||||||
@@ -65,6 +68,14 @@ typedef enum {
|
|||||||
[self _logDelegateCall:_cmd];
|
[self _logDelegateCall:_cmd];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
- (void)webServerDidCompleteBonjourRegistration:(GCDWebServer*)server {
|
||||||
|
[self _logDelegateCall:_cmd];
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)webServerDidUpdateNATPortMapping:(GCDWebServer*)server {
|
||||||
|
[self _logDelegateCall:_cmd];
|
||||||
|
}
|
||||||
|
|
||||||
- (void)webServerDidConnect:(GCDWebServer*)server {
|
- (void)webServerDidConnect:(GCDWebServer*)server {
|
||||||
[self _logDelegateCall:_cmd];
|
[self _logDelegateCall:_cmd];
|
||||||
}
|
}
|
||||||
@@ -130,9 +141,15 @@ int main(int argc, const char* argv[]) {
|
|||||||
BOOL recording = NO;
|
BOOL recording = NO;
|
||||||
NSString* rootDirectory = NSHomeDirectory();
|
NSString* rootDirectory = NSHomeDirectory();
|
||||||
NSString* testDirectory = nil;
|
NSString* testDirectory = nil;
|
||||||
|
NSString* authenticationMethod = nil;
|
||||||
|
NSString* authenticationRealm = nil;
|
||||||
|
NSString* authenticationUser = nil;
|
||||||
|
NSString* authenticationPassword = nil;
|
||||||
|
BOOL bindToLocalhost = NO;
|
||||||
|
BOOL requestNATPortMapping = NO;
|
||||||
|
|
||||||
if (argc == 1) {
|
if (argc == 1) {
|
||||||
fprintf(stdout, "Usage: %s [-mode webServer | htmlPage | htmlForm | webDAV | webUploader | streamingResponse] [-record] [-root directory] [-tests directory]\n\n", basename((char*)argv[0]));
|
fprintf(stdout, "Usage: %s [-mode webServer | htmlPage | htmlForm | htmlFileUpload | webDAV | webUploader | streamingResponse | asyncResponse] [-record] [-root directory] [-tests directory] [-authenticationMethod Basic | Digest] [-authenticationRealm realm] [-authenticationUser user] [-authenticationPassword password] [--localhost]\n\n", basename((char*)argv[0]));
|
||||||
} else {
|
} else {
|
||||||
for (int i = 1; i < argc; ++i) {
|
for (int i = 1; i < argc; ++i) {
|
||||||
if (argv[i][0] != '-') {
|
if (argv[i][0] != '-') {
|
||||||
@@ -146,60 +163,76 @@ int main(int argc, const char* argv[]) {
|
|||||||
mode = kMode_HTMLPage;
|
mode = kMode_HTMLPage;
|
||||||
} else if (!strcmp(argv[i], "htmlForm")) {
|
} else if (!strcmp(argv[i], "htmlForm")) {
|
||||||
mode = kMode_HTMLForm;
|
mode = kMode_HTMLForm;
|
||||||
|
} else if (!strcmp(argv[i], "htmlFileUpload")) {
|
||||||
|
mode = kMode_HTMLFileUpload;
|
||||||
} else if (!strcmp(argv[i], "webDAV")) {
|
} else if (!strcmp(argv[i], "webDAV")) {
|
||||||
mode = kMode_WebDAV;
|
mode = kMode_WebDAV;
|
||||||
} else if (!strcmp(argv[i], "webUploader")) {
|
} else if (!strcmp(argv[i], "webUploader")) {
|
||||||
mode = kMode_WebUploader;
|
mode = kMode_WebUploader;
|
||||||
} else if (!strcmp(argv[i], "streamingResponse")) {
|
} else if (!strcmp(argv[i], "streamingResponse")) {
|
||||||
mode = kMode_StreamingResponse;
|
mode = kMode_StreamingResponse;
|
||||||
|
} else if (!strcmp(argv[i], "asyncResponse")) {
|
||||||
|
mode = kMode_AsyncResponse;
|
||||||
}
|
}
|
||||||
} else if (!strcmp(argv[i], "-record")) {
|
} else if (!strcmp(argv[i], "-record")) {
|
||||||
recording = YES;
|
recording = YES;
|
||||||
} else if (!strcmp(argv[i], "-root") && (i + 1 < argc)) {
|
} else if (!strcmp(argv[i], "-root") && (i + 1 < argc)) {
|
||||||
++i;
|
++i;
|
||||||
rootDirectory = [[[NSFileManager defaultManager] stringWithFileSystemRepresentation:argv[i] length:strlen(argv[i])] stringByStandardizingPath];
|
rootDirectory = [[NSFileManager defaultManager] stringWithFileSystemRepresentation:argv[i] length:strlen(argv[i])];
|
||||||
} else if (!strcmp(argv[i], "-tests") && (i + 1 < argc)) {
|
} else if (!strcmp(argv[i], "-tests") && (i + 1 < argc)) {
|
||||||
++i;
|
++i;
|
||||||
testDirectory = [[[NSFileManager defaultManager] stringWithFileSystemRepresentation:argv[i] length:strlen(argv[i])] stringByStandardizingPath];
|
testDirectory = [[NSFileManager defaultManager] stringWithFileSystemRepresentation:argv[i] length:strlen(argv[i])];
|
||||||
|
} else if (!strcmp(argv[i], "-authenticationMethod") && (i + 1 < argc)) {
|
||||||
|
++i;
|
||||||
|
authenticationMethod = [NSString stringWithUTF8String:argv[i]];
|
||||||
|
} else if (!strcmp(argv[i], "-authenticationRealm") && (i + 1 < argc)) {
|
||||||
|
++i;
|
||||||
|
authenticationRealm = [NSString stringWithUTF8String:argv[i]];
|
||||||
|
} else if (!strcmp(argv[i], "-authenticationUser") && (i + 1 < argc)) {
|
||||||
|
++i;
|
||||||
|
authenticationUser = [NSString stringWithUTF8String:argv[i]];
|
||||||
|
} else if (!strcmp(argv[i], "-authenticationPassword") && (i + 1 < argc)) {
|
||||||
|
++i;
|
||||||
|
authenticationPassword = [NSString stringWithUTF8String:argv[i]];
|
||||||
|
} else if (!strcmp(argv[i], "--localhost")) {
|
||||||
|
bindToLocalhost = YES;
|
||||||
|
} else if (!strcmp(argv[i], "--nat")) {
|
||||||
|
requestNATPortMapping = YES;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
GCDWebServer* webServer = nil;
|
GCDWebServer* webServer = nil;
|
||||||
switch (mode) {
|
switch (mode) {
|
||||||
|
|
||||||
// Simply serve contents of home directory
|
// Simply serve contents of home directory
|
||||||
case kMode_WebServer: {
|
case kMode_WebServer: {
|
||||||
fprintf(stdout, "Running in Web Server mode from \"%s\"", [rootDirectory UTF8String]);
|
fprintf(stdout, "Running in Web Server mode from \"%s\"\n", [rootDirectory UTF8String]);
|
||||||
webServer = [[GCDWebServer alloc] init];
|
webServer = [[GCDWebServer alloc] init];
|
||||||
[webServer addGETHandlerForBasePath:@"/" directoryPath:rootDirectory indexFilename:nil cacheAge:0 allowRangeRequests:YES];
|
[webServer addGETHandlerForBasePath:@"/" directoryPath:rootDirectory indexFilename:nil cacheAge:0 allowRangeRequests:YES];
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Renders a HTML page
|
// Renders a HTML page
|
||||||
case kMode_HTMLPage: {
|
case kMode_HTMLPage: {
|
||||||
fprintf(stdout, "Running in HTML Page mode");
|
fprintf(stdout, "Running in HTML Page mode\n");
|
||||||
webServer = [[GCDWebServer alloc] init];
|
webServer = [[GCDWebServer alloc] init];
|
||||||
[webServer addDefaultHandlerForMethod:@"GET"
|
[webServer addDefaultHandlerForMethod:@"GET"
|
||||||
requestClass:[GCDWebServerRequest class]
|
requestClass:[GCDWebServerRequest class]
|
||||||
processBlock:^GCDWebServerResponse *(GCDWebServerRequest* request) {
|
processBlock:^GCDWebServerResponse*(GCDWebServerRequest* request) {
|
||||||
|
return [GCDWebServerDataResponse responseWithHTML:@"<html><body><p>Hello World</p></body></html>"];
|
||||||
return [GCDWebServerDataResponse responseWithHTML:@"<html><body><p>Hello World</p></body></html>"];
|
}];
|
||||||
|
|
||||||
}];
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Implements an HTML form
|
// Implements an HTML form
|
||||||
case kMode_HTMLForm: {
|
case kMode_HTMLForm: {
|
||||||
fprintf(stdout, "Running in HTML Form mode");
|
fprintf(stdout, "Running in HTML Form mode\n");
|
||||||
webServer = [[GCDWebServer alloc] init];
|
webServer = [[GCDWebServer alloc] init];
|
||||||
[webServer addHandlerForMethod:@"GET"
|
[webServer addHandlerForMethod:@"GET"
|
||||||
path:@"/"
|
path:@"/"
|
||||||
requestClass:[GCDWebServerRequest class]
|
requestClass:[GCDWebServerRequest class]
|
||||||
processBlock:^GCDWebServerResponse *(GCDWebServerRequest* request) {
|
processBlock:^GCDWebServerResponse*(GCDWebServerRequest* request) {
|
||||||
|
NSString* html = @" \
|
||||||
NSString* html = @" \
|
|
||||||
<html><body> \
|
<html><body> \
|
||||||
<form name=\"input\" action=\"/\" method=\"post\" enctype=\"application/x-www-form-urlencoded\"> \
|
<form name=\"input\" action=\"/\" method=\"post\" enctype=\"application/x-www-form-urlencoded\"> \
|
||||||
Value: <input type=\"text\" name=\"value\"> \
|
Value: <input type=\"text\" name=\"value\"> \
|
||||||
@@ -207,88 +240,173 @@ int main(int argc, const char* argv[]) {
|
|||||||
</form> \
|
</form> \
|
||||||
</body></html> \
|
</body></html> \
|
||||||
";
|
";
|
||||||
return [GCDWebServerDataResponse responseWithHTML:html];
|
return [GCDWebServerDataResponse responseWithHTML:html];
|
||||||
|
}];
|
||||||
}];
|
|
||||||
[webServer addHandlerForMethod:@"POST"
|
[webServer addHandlerForMethod:@"POST"
|
||||||
path:@"/"
|
path:@"/"
|
||||||
requestClass:[GCDWebServerURLEncodedFormRequest class]
|
requestClass:[GCDWebServerURLEncodedFormRequest class]
|
||||||
processBlock:^GCDWebServerResponse *(GCDWebServerRequest* request) {
|
processBlock:^GCDWebServerResponse*(GCDWebServerRequest* request) {
|
||||||
|
NSString* value = [[(GCDWebServerURLEncodedFormRequest*)request arguments] objectForKey:@"value"];
|
||||||
NSString* value = [[(GCDWebServerURLEncodedFormRequest*)request arguments] objectForKey:@"value"];
|
NSString* html = [NSString stringWithFormat:@"<html><body><p>%@</p></body></html>", value];
|
||||||
NSString* html = [NSString stringWithFormat:@"<html><body><p>%@</p></body></html>", value];
|
return [GCDWebServerDataResponse responseWithHTML:html];
|
||||||
return [GCDWebServerDataResponse responseWithHTML:html];
|
}];
|
||||||
|
|
||||||
}];
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Serve home directory through WebDAV
|
// Implements HTML file upload
|
||||||
case kMode_WebDAV: {
|
case kMode_HTMLFileUpload: {
|
||||||
fprintf(stdout, "Running in WebDAV mode from \"%s\"", [rootDirectory UTF8String]);
|
fprintf(stdout, "Running in HTML File Upload mode\n");
|
||||||
webServer = [[GCDWebDAVServer alloc] initWithUploadDirectory:rootDirectory];
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Serve home directory through web uploader
|
|
||||||
case kMode_WebUploader: {
|
|
||||||
fprintf(stdout, "Running in Web Uploader mode from \"%s\"", [rootDirectory UTF8String]);
|
|
||||||
webServer = [[GCDWebUploader alloc] initWithUploadDirectory:rootDirectory];
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test streaming responses
|
|
||||||
case kMode_StreamingResponse: {
|
|
||||||
fprintf(stdout, "Running in Streaming Response mode");
|
|
||||||
webServer = [[GCDWebServer alloc] init];
|
webServer = [[GCDWebServer alloc] init];
|
||||||
|
NSString* formHTML = @" \
|
||||||
|
<form name=\"input\" action=\"/\" method=\"post\" enctype=\"multipart/form-data\"> \
|
||||||
|
<input type=\"hidden\" name=\"secret\" value=\"42\"> \
|
||||||
|
<input type=\"file\" name=\"files\" multiple><br/> \
|
||||||
|
<input type=\"submit\" value=\"Submit\"> \
|
||||||
|
</form> \
|
||||||
|
";
|
||||||
[webServer addHandlerForMethod:@"GET"
|
[webServer addHandlerForMethod:@"GET"
|
||||||
path:@"/"
|
path:@"/"
|
||||||
requestClass:[GCDWebServerRequest class]
|
requestClass:[GCDWebServerRequest class]
|
||||||
processBlock:^GCDWebServerResponse *(GCDWebServerRequest* request) {
|
processBlock:^GCDWebServerResponse*(GCDWebServerRequest* request) {
|
||||||
|
NSString* html = [NSString stringWithFormat:@"<html><body>%@</body></html>", formHTML];
|
||||||
__block int countDown = 10;
|
return [GCDWebServerDataResponse responseWithHTML:html];
|
||||||
return [GCDWebServerStreamingResponse responseWithContentType:@"text/plain" streamBlock:^NSData *(NSError** error) {
|
}];
|
||||||
|
[webServer addHandlerForMethod:@"POST"
|
||||||
usleep(100 * 1000);
|
path:@"/"
|
||||||
if (countDown) {
|
requestClass:[GCDWebServerMultiPartFormRequest class]
|
||||||
return [[NSString stringWithFormat:@"%i\n", countDown--] dataUsingEncoding:NSUTF8StringEncoding];
|
processBlock:^GCDWebServerResponse*(GCDWebServerRequest* request) {
|
||||||
} else {
|
NSMutableString* string = [NSMutableString string];
|
||||||
return [NSData data];
|
for (GCDWebServerMultiPartArgument* argument in [(GCDWebServerMultiPartFormRequest*)request arguments]) {
|
||||||
}
|
[string appendFormat:@"%@ = %@<br>", argument.controlName, argument.string];
|
||||||
|
}
|
||||||
}];
|
for (GCDWebServerMultiPartFile* file in [(GCDWebServerMultiPartFormRequest*)request files]) {
|
||||||
|
NSDictionary* attributes = [[NSFileManager defaultManager] attributesOfItemAtPath:file.temporaryPath error:NULL];
|
||||||
}];
|
[string appendFormat:@"%@ = "%@" (%@ | %llu %@)<br>", file.controlName, file.fileName, file.mimeType,
|
||||||
|
attributes.fileSize >= 1000 ? attributes.fileSize / 1000 : attributes.fileSize,
|
||||||
|
attributes.fileSize >= 1000 ? @"KB" : @"Bytes"];
|
||||||
|
};
|
||||||
|
NSString* html = [NSString stringWithFormat:@"<html><body><p>%@</p><hr>%@</body></html>", string, formHTML];
|
||||||
|
return [GCDWebServerDataResponse responseWithHTML:html];
|
||||||
|
}];
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Serve home directory through WebDAV
|
||||||
|
case kMode_WebDAV: {
|
||||||
|
fprintf(stdout, "Running in WebDAV mode from \"%s\"\n", [rootDirectory UTF8String]);
|
||||||
|
webServer = [[GCDWebDAVServer alloc] initWithUploadDirectory:rootDirectory];
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Serve home directory through web uploader
|
||||||
|
case kMode_WebUploader: {
|
||||||
|
fprintf(stdout, "Running in Web Uploader mode from \"%s\"\n", [rootDirectory UTF8String]);
|
||||||
|
webServer = [[GCDWebUploader alloc] initWithUploadDirectory:rootDirectory];
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test streaming responses
|
||||||
|
case kMode_StreamingResponse: {
|
||||||
|
fprintf(stdout, "Running in Streaming Response mode\n");
|
||||||
|
webServer = [[GCDWebServer alloc] init];
|
||||||
|
[webServer addHandlerForMethod:@"GET"
|
||||||
|
path:@"/sync"
|
||||||
|
requestClass:[GCDWebServerRequest class]
|
||||||
|
processBlock:^GCDWebServerResponse*(GCDWebServerRequest* request) {
|
||||||
|
__block int countDown = 10;
|
||||||
|
return [GCDWebServerStreamedResponse responseWithContentType:@"text/plain"
|
||||||
|
streamBlock:^NSData*(NSError** error) {
|
||||||
|
usleep(100 * 1000);
|
||||||
|
if (countDown) {
|
||||||
|
return [[NSString stringWithFormat:@"%i\n", countDown--] dataUsingEncoding:NSUTF8StringEncoding];
|
||||||
|
} else {
|
||||||
|
return [NSData data];
|
||||||
|
}
|
||||||
|
}];
|
||||||
|
}];
|
||||||
|
[webServer addHandlerForMethod:@"GET"
|
||||||
|
path:@"/async"
|
||||||
|
requestClass:[GCDWebServerRequest class]
|
||||||
|
processBlock:^GCDWebServerResponse*(GCDWebServerRequest* request) {
|
||||||
|
__block int countDown = 10;
|
||||||
|
return [GCDWebServerStreamedResponse responseWithContentType:@"text/plain"
|
||||||
|
asyncStreamBlock:^(GCDWebServerBodyReaderCompletionBlock completionBlock) {
|
||||||
|
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
|
||||||
|
NSData* data = countDown ? [[NSString stringWithFormat:@"%i\n", countDown--] dataUsingEncoding:NSUTF8StringEncoding] : [NSData data];
|
||||||
|
completionBlock(data, nil);
|
||||||
|
});
|
||||||
|
}];
|
||||||
|
}];
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test async responses
|
||||||
|
case kMode_AsyncResponse: {
|
||||||
|
fprintf(stdout, "Running in Async Response mode\n");
|
||||||
|
webServer = [[GCDWebServer alloc] init];
|
||||||
|
[webServer addHandlerForMethod:@"GET"
|
||||||
|
path:@"/async"
|
||||||
|
requestClass:[GCDWebServerRequest class]
|
||||||
|
asyncProcessBlock:^(GCDWebServerRequest* request, GCDWebServerCompletionBlock completionBlock) {
|
||||||
|
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1 * NSEC_PER_SEC)), dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
|
||||||
|
GCDWebServerDataResponse* response = [GCDWebServerDataResponse responseWithData:(NSData*)[@"Hello World!" dataUsingEncoding:NSUTF8StringEncoding] contentType:@"text/plain"];
|
||||||
|
completionBlock(response);
|
||||||
|
});
|
||||||
|
}];
|
||||||
|
[webServer addHandlerForMethod:@"GET"
|
||||||
|
path:@"/async2"
|
||||||
|
requestClass:[GCDWebServerRequest class]
|
||||||
|
asyncProcessBlock:^(GCDWebServerRequest* request, GCDWebServerCompletionBlock handlerCompletionBlock) {
|
||||||
|
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1 * NSEC_PER_SEC)), dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
|
||||||
|
__block int countDown = 10;
|
||||||
|
GCDWebServerStreamedResponse* response = [GCDWebServerStreamedResponse responseWithContentType:@"text/plain"
|
||||||
|
asyncStreamBlock:^(GCDWebServerBodyReaderCompletionBlock readerCompletionBlock) {
|
||||||
|
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
|
||||||
|
NSData* data = countDown ? [[NSString stringWithFormat:@"%i\n", countDown--] dataUsingEncoding:NSUTF8StringEncoding] : [NSData data];
|
||||||
|
readerCompletionBlock(data, nil);
|
||||||
|
});
|
||||||
|
}];
|
||||||
|
handlerCompletionBlock(response);
|
||||||
|
});
|
||||||
|
}];
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
#if __has_feature(objc_arc)
|
|
||||||
fprintf(stdout, " (ARC is ON)\n");
|
|
||||||
#else
|
|
||||||
fprintf(stdout, " (ARC is OFF)\n");
|
|
||||||
#endif
|
|
||||||
|
|
||||||
if (webServer) {
|
if (webServer) {
|
||||||
Delegate* delegate = [[Delegate alloc] init];
|
Delegate* delegate = [[Delegate alloc] init];
|
||||||
webServer.delegate = delegate;
|
|
||||||
if (testDirectory) {
|
if (testDirectory) {
|
||||||
|
#if DEBUG
|
||||||
|
webServer.delegate = delegate;
|
||||||
|
#endif
|
||||||
fprintf(stdout, "<RUNNING TESTS FROM \"%s\">\n\n", [testDirectory UTF8String]);
|
fprintf(stdout, "<RUNNING TESTS FROM \"%s\">\n\n", [testDirectory UTF8String]);
|
||||||
result = (int)[webServer runTestsInDirectory:testDirectory withPort:8080];
|
result = (int)[webServer runTestsWithOptions:@{GCDWebServerOption_Port : @8080} inDirectory:testDirectory];
|
||||||
} else {
|
} else {
|
||||||
|
webServer.delegate = delegate;
|
||||||
if (recording) {
|
if (recording) {
|
||||||
fprintf(stdout, "<RECORDING ENABLED>\n");
|
fprintf(stdout, "<RECORDING ENABLED>\n");
|
||||||
webServer.recordingEnabled = YES;
|
webServer.recordingEnabled = YES;
|
||||||
}
|
}
|
||||||
fprintf(stdout, "\n");
|
fprintf(stdout, "\n");
|
||||||
if ([webServer runWithPort:8080]) {
|
NSMutableDictionary* options = [NSMutableDictionary dictionary];
|
||||||
|
[options setObject:@8080 forKey:GCDWebServerOption_Port];
|
||||||
|
[options setObject:@(requestNATPortMapping) forKey:GCDWebServerOption_RequestNATPortMapping];
|
||||||
|
[options setObject:@(bindToLocalhost) forKey:GCDWebServerOption_BindToLocalhost];
|
||||||
|
[options setObject:@"" forKey:GCDWebServerOption_BonjourName];
|
||||||
|
if (authenticationUser && authenticationPassword) {
|
||||||
|
[options setValue:authenticationRealm forKey:GCDWebServerOption_AuthenticationRealm];
|
||||||
|
[options setObject:@{authenticationUser : authenticationPassword} forKey:GCDWebServerOption_AuthenticationAccounts];
|
||||||
|
if ([authenticationMethod isEqualToString:@"Basic"]) {
|
||||||
|
[options setObject:GCDWebServerAuthenticationMethod_Basic forKey:GCDWebServerOption_AuthenticationMethod];
|
||||||
|
} else if ([authenticationMethod isEqualToString:@"Digest"]) {
|
||||||
|
[options setObject:GCDWebServerAuthenticationMethod_DigestAccess forKey:GCDWebServerOption_AuthenticationMethod];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ([webServer runWithOptions:options error:NULL]) {
|
||||||
result = 0;
|
result = 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
#if !__has_feature(objc_arc)
|
webServer.delegate = nil;
|
||||||
[webServer release];
|
|
||||||
[delegate release];
|
|
||||||
#endif
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
|
|||||||
224
README.md
224
README.md
@@ -2,64 +2,89 @@ Overview
|
|||||||
========
|
========
|
||||||
|
|
||||||
[](https://travis-ci.org/swisspol/GCDWebServer)
|
[](https://travis-ci.org/swisspol/GCDWebServer)
|
||||||
|
[](https://cocoapods.org/pods/GCDWebServer)
|
||||||
|
[](https://github.com/swisspol/GCDWebServer)
|
||||||
|
[](LICENSE)
|
||||||
|
|
||||||
GCDWebServer is a modern and lightweight GCD based HTTP 1.1 server designed to be embedded in OS X & iOS apps. It was written from scratch with the following goals in mind:
|
GCDWebServer is a modern and lightweight GCD based HTTP 1.1 server designed to be embedded in iOS, macOS & tvOS apps. It was written from scratch with the following goals in mind:
|
||||||
* Easy to use and understand architecture with only 4 core classes: server, connection, request and response (see "Understanding GCDWebServer's Architecture" below)
|
* Elegant and easy to use architecture with only 4 core classes: server, connection, request and response (see "Understanding GCDWebServer's Architecture" below)
|
||||||
* Well designed API for easy integration and customization
|
* Well designed API with fully documented headers for easy integration and customization
|
||||||
* Entirely built with an event-driven design using [Grand Central Dispatch](http://en.wikipedia.org/wiki/Grand_Central_Dispatch) for maximal performance and concurrency
|
* Entirely built with an event-driven design using [Grand Central Dispatch](http://en.wikipedia.org/wiki/Grand_Central_Dispatch) for best performance and concurrency
|
||||||
* No dependencies on third-party source code
|
* No dependencies on third-party source code
|
||||||
* Available under a friendly [New BSD License](LICENSE)
|
* Available under a friendly [New BSD License](LICENSE)
|
||||||
|
|
||||||
Extra built-in features:
|
Extra built-in features:
|
||||||
|
* Allow implementation of fully asynchronous handlers of incoming HTTP requests
|
||||||
* Minimize memory usage with disk streaming of large HTTP request or response bodies
|
* Minimize memory usage with disk streaming of large HTTP request or response bodies
|
||||||
* Parser for [web forms](http://www.w3.org/TR/html401/interact/forms.html#h-17.13.4) submitted using "application/x-www-form-urlencoded" or "multipart/form-data" encodings (including file uploads)
|
* Parser for [web forms](http://www.w3.org/TR/html401/interact/forms.html#h-17.13.4) submitted using "application/x-www-form-urlencoded" or "multipart/form-data" encodings (including file uploads)
|
||||||
* [JSON](http://www.json.org/) parsing and serialization for request and response HTTP bodies
|
* [JSON](http://www.json.org/) parsing and serialization for request and response HTTP bodies
|
||||||
* [Chunked transfer encoding](https://en.wikipedia.org/wiki/Chunked_transfer_encoding) for request and response HTTP bodies
|
* [Chunked transfer encoding](https://en.wikipedia.org/wiki/Chunked_transfer_encoding) for request and response HTTP bodies
|
||||||
* [HTTP compression](https://en.wikipedia.org/wiki/HTTP_compression) with gzip for request and response HTTP bodies
|
* [HTTP compression](https://en.wikipedia.org/wiki/HTTP_compression) with gzip for request and response HTTP bodies
|
||||||
* [HTTP range](https://en.wikipedia.org/wiki/Byte_serving) support for requests of local files
|
* [HTTP range](https://en.wikipedia.org/wiki/Byte_serving) support for requests of local files
|
||||||
|
* [Basic](https://en.wikipedia.org/wiki/Basic_access_authentication) and [Digest Access](https://en.wikipedia.org/wiki/Digest_access_authentication) authentications for password protection
|
||||||
|
* Automatically handle transitions between foreground, background and suspended modes in iOS apps
|
||||||
|
* Full support for both IPv4 and IPv6
|
||||||
|
* NAT port mapping (IPv4 only)
|
||||||
|
|
||||||
Included extensions:
|
Included extensions:
|
||||||
* [GCDWebUploader](GCDWebUploader/GCDWebUploader.h): subclass of ```GCDWebServer``` that implements an interface for uploading and downloading files from an iOS app's sandbox using a web browser
|
* [GCDWebUploader](GCDWebUploader/GCDWebUploader.h): subclass of ```GCDWebServer``` that implements an interface for uploading and downloading files using a web browser
|
||||||
* [GCDWebDAVServer](GCDWebDAVServer/GCDWebDAVServer.h): subclass of ```GCDWebServer``` that implements a class 1 [WebDAV](https://en.wikipedia.org/wiki/WebDAV) server (with partial class 2 support for OS X Finder)
|
* [GCDWebDAVServer](GCDWebDAVServer/GCDWebDAVServer.h): subclass of ```GCDWebServer``` that implements a class 1 [WebDAV](https://en.wikipedia.org/wiki/WebDAV) server (with partial class 2 support for macOS Finder)
|
||||||
|
|
||||||
What's not available out of the box but can be implemented on top of the API:
|
|
||||||
* Authentication like [Basic Authentication](https://en.wikipedia.org/wiki/Basic_access_authentication)
|
|
||||||
* Web forms submitted using "multipart/mixed"
|
|
||||||
|
|
||||||
What's not supported (but not really required from an embedded HTTP server):
|
What's not supported (but not really required from an embedded HTTP server):
|
||||||
* Keep-alive connections
|
* Keep-alive connections
|
||||||
* HTTPS
|
* HTTPS
|
||||||
|
|
||||||
Requirements:
|
Requirements:
|
||||||
* OS X 10.7 or later (x86_64)
|
* macOS 10.7 or later (x86_64)
|
||||||
* iOS 5.0 or later (armv7, armv7s or arm64)
|
* iOS 8.0 or later (armv7, armv7s or arm64)
|
||||||
|
* tvOS 9.0 or later (arm64)
|
||||||
|
* ARC memory management only (if you need MRC support use GCDWebServer 3.1 or earlier)
|
||||||
|
|
||||||
Getting Started
|
Getting Started
|
||||||
===============
|
===============
|
||||||
|
|
||||||
Download or checkout the source for GCDWebServer then add the entire "GCDWebServer" subfolder to your Xcode project. If you intend to use one of the extensions like GCDWebDAVServer or GCDWebUploader, add these subfolders as well.
|
Download or check out the [latest release](https://github.com/swisspol/GCDWebServer/releases) of GCDWebServer then add the entire "GCDWebServer" subfolder to your Xcode project. If you intend to use one of the extensions like GCDWebDAVServer or GCDWebUploader, add these subfolders as well. Finally link to `libz` (via Target > Build Phases > Link Binary With Libraries) and add `$(SDKROOT)/usr/include/libxml2` to your header search paths (via Target > Build Settings > HEADER_SEARCH_PATHS).
|
||||||
|
|
||||||
Alternatively, you can install GCDWebServer using [CocoaPods](http://cocoapods.org/) by simply adding this line to your Xcode project's Podfile:
|
Alternatively, you can install GCDWebServer using [CocoaPods](http://cocoapods.org/) by simply adding this line to your Podfile:
|
||||||
```
|
```
|
||||||
pod "GCDWebServer", "~> 2.0"
|
pod "GCDWebServer", "~> 3.0"
|
||||||
```
|
```
|
||||||
If you want to use GCDWebUploader, use this line instead:
|
If you want to use GCDWebUploader, use this line instead:
|
||||||
```
|
```
|
||||||
pod "GCDWebServer/WebUploader", "~> 2.0"
|
pod "GCDWebServer/WebUploader", "~> 3.0"
|
||||||
```
|
```
|
||||||
Or this line for GCDWebDAVServer:
|
Or this line for GCDWebDAVServer:
|
||||||
```
|
```
|
||||||
pod "GCDWebServer/WebDAV", "~> 2.0"
|
pod "GCDWebServer/WebDAV", "~> 3.0"
|
||||||
```
|
```
|
||||||
|
|
||||||
|
And finally run `$ pod install`.
|
||||||
|
|
||||||
|
You can also use [Carthage](https://github.com/Carthage/Carthage) by adding this line to your Cartfile (3.2.5 is the first release with Carthage support):
|
||||||
|
```
|
||||||
|
github "swisspol/GCDWebServer" ~> 3.2.5
|
||||||
|
```
|
||||||
|
|
||||||
|
Then run `$ carthage update` and add the generated frameworks to your Xcode projects (see [Carthage instructions](https://github.com/Carthage/Carthage#adding-frameworks-to-an-application)).
|
||||||
|
|
||||||
|
Help & Support
|
||||||
|
==============
|
||||||
|
|
||||||
|
For help with using GCDWebServer, it's best to ask your question on Stack Overflow with the [`gcdwebserver`](http://stackoverflow.com/questions/tagged/gcdwebserver) tag. For bug reports and enhancement requests you can use [issues](https://github.com/swisspol/GCDWebServer/issues) in this project.
|
||||||
|
|
||||||
|
Be sure to read this entire README first though!
|
||||||
|
|
||||||
Hello World
|
Hello World
|
||||||
===========
|
===========
|
||||||
|
|
||||||
These codes snippets show how to implement a custom HTTP server that runs on port 8080 and returns a "Hello World" HTML page to any request. Since GCDWebServer uses GCD blocks to handle requests, no subclassing or delegates are needed, which results in very clean code.
|
These code snippets show how to implement a custom HTTP server that runs on port 8080 and returns a "Hello World" HTML page to any request. Since GCDWebServer uses GCD blocks to handle requests, no subclassing or delegates are needed, which results in very clean code.
|
||||||
|
|
||||||
**OS X version (command line tool):**
|
**IMPORTANT:** If not using CocoaPods, be sure to add the `libz` shared system library to the Xcode target for your app.
|
||||||
|
|
||||||
|
**macOS version (command line tool):**
|
||||||
```objectivec
|
```objectivec
|
||||||
#import "GCDWebServer.h"
|
#import "GCDWebServer.h"
|
||||||
|
#import "GCDWebServerDataResponse.h"
|
||||||
|
|
||||||
int main(int argc, const char* argv[]) {
|
int main(int argc, const char* argv[]) {
|
||||||
@autoreleasepool {
|
@autoreleasepool {
|
||||||
@@ -76,22 +101,27 @@ int main(int argc, const char* argv[]) {
|
|||||||
|
|
||||||
}];
|
}];
|
||||||
|
|
||||||
// Use convenience method that runs server on port 8080 until SIGINT received (i.e. Ctrl-C in Terminal)
|
// Use convenience method that runs server on port 8080
|
||||||
[webServer runWithPort:8080];
|
// until SIGINT (Ctrl-C in Terminal) or SIGTERM is received
|
||||||
|
[webServer runWithPort:8080 bonjourName:nil];
|
||||||
// Destroy server (unnecessary if using ARC)
|
NSLog(@"Visit %@ in your web browser", webServer.serverURL);
|
||||||
[webServer release];
|
|
||||||
|
|
||||||
}
|
}
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
**iOS Version:**
|
**iOS version:**
|
||||||
```objectivec
|
```objectivec
|
||||||
#import "GCDWebServer.h"
|
#import "GCDWebServer.h"
|
||||||
|
#import "GCDWebServerDataResponse.h"
|
||||||
|
|
||||||
static GCDWebServer* _webServer = nil; // This should really be an ivar of your application delegate class
|
@interface AppDelegate : NSObject <UIApplicationDelegate> {
|
||||||
|
GCDWebServer* _webServer;
|
||||||
|
}
|
||||||
|
@end
|
||||||
|
|
||||||
|
@implementation AppDelegate
|
||||||
|
|
||||||
- (BOOL)application:(UIApplication*)application didFinishLaunchingWithOptions:(NSDictionary*)launchOptions {
|
- (BOOL)application:(UIApplication*)application didFinishLaunchingWithOptions:(NSDictionary*)launchOptions {
|
||||||
|
|
||||||
@@ -109,9 +139,40 @@ static GCDWebServer* _webServer = nil; // This should really be an ivar of your
|
|||||||
|
|
||||||
// Start server on port 8080
|
// Start server on port 8080
|
||||||
[_webServer startWithPort:8080 bonjourName:nil];
|
[_webServer startWithPort:8080 bonjourName:nil];
|
||||||
|
NSLog(@"Visit %@ in your web browser", _webServer.serverURL);
|
||||||
|
|
||||||
return YES;
|
return YES;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@end
|
||||||
|
```
|
||||||
|
|
||||||
|
**macOS Swift version (command line tool):**
|
||||||
|
|
||||||
|
***webServer.swift***
|
||||||
|
```swift
|
||||||
|
import Foundation
|
||||||
|
import GCDWebServer
|
||||||
|
|
||||||
|
func initWebServer() {
|
||||||
|
|
||||||
|
let webServer = GCDWebServer()
|
||||||
|
|
||||||
|
webServer.addDefaultHandler(forMethod: "GET", request: GCDWebServerRequest.self, processBlock: {request in
|
||||||
|
return GCDWebServerDataResponse(html:"<html><body><p>Hello World</p></body></html>")
|
||||||
|
|
||||||
|
})
|
||||||
|
|
||||||
|
webServer.start(withPort: 8080, bonjourName: "GCD Web Server")
|
||||||
|
|
||||||
|
print("Visit \(webServer.serverURL) in your web browser")
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
***WebServer-Bridging-Header.h***
|
||||||
|
```objectivec
|
||||||
|
#import <GCDWebServer/GCDWebServer.h>
|
||||||
|
#import <GCDWebServer/GCDWebServerDataResponse.h>
|
||||||
```
|
```
|
||||||
|
|
||||||
Web Based Uploads in iOS Apps
|
Web Based Uploads in iOS Apps
|
||||||
@@ -119,12 +180,17 @@ Web Based Uploads in iOS Apps
|
|||||||
|
|
||||||
GCDWebUploader is a subclass of ```GCDWebServer``` that provides a ready-to-use HTML 5 file uploader & downloader. This lets users upload, download, delete files and create directories from a directory inside your iOS app's sandbox using a clean user interface in their web browser.
|
GCDWebUploader is a subclass of ```GCDWebServer``` that provides a ready-to-use HTML 5 file uploader & downloader. This lets users upload, download, delete files and create directories from a directory inside your iOS app's sandbox using a clean user interface in their web browser.
|
||||||
|
|
||||||
Simply instantiate and run a GCDWebUploader instance then visit http://{YOUR-IOS-DEVICE-IP-ADDRESS}/ from your web browser:
|
Simply instantiate and run a ```GCDWebUploader``` instance then visit ```http://{YOUR-IOS-DEVICE-IP-ADDRESS}/``` from your web browser:
|
||||||
|
|
||||||
```objectivec
|
```objectivec
|
||||||
#import "GCDWebUploader.h"
|
#import "GCDWebUploader.h"
|
||||||
|
|
||||||
static GCDWebUploader* _webUploader = nil; // This should really be an ivar of your application delegate class
|
@interface AppDelegate : NSObject <UIApplicationDelegate> {
|
||||||
|
GCDWebUploader* _webUploader;
|
||||||
|
}
|
||||||
|
@end
|
||||||
|
|
||||||
|
@implementation AppDelegate
|
||||||
|
|
||||||
- (BOOL)application:(UIApplication*)application didFinishLaunchingWithOptions:(NSDictionary*)launchOptions {
|
- (BOOL)application:(UIApplication*)application didFinishLaunchingWithOptions:(NSDictionary*)launchOptions {
|
||||||
NSString* documentsPath = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) firstObject];
|
NSString* documentsPath = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) firstObject];
|
||||||
@@ -133,6 +199,8 @@ static GCDWebUploader* _webUploader = nil; // This should really be an ivar of
|
|||||||
NSLog(@"Visit %@ in your web browser", _webUploader.serverURL);
|
NSLog(@"Visit %@ in your web browser", _webUploader.serverURL);
|
||||||
return YES;
|
return YES;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@end
|
||||||
```
|
```
|
||||||
|
|
||||||
WebDAV Server in iOS Apps
|
WebDAV Server in iOS Apps
|
||||||
@@ -140,14 +208,19 @@ WebDAV Server in iOS Apps
|
|||||||
|
|
||||||
GCDWebDAVServer is a subclass of ```GCDWebServer``` that provides a class 1 compliant [WebDAV](https://en.wikipedia.org/wiki/WebDAV) server. This lets users upload, download, delete files and create directories from a directory inside your iOS app's sandbox using any WebDAV client like [Transmit](https://panic.com/transmit/) (Mac), [ForkLift](http://binarynights.com/forklift/) (Mac) or [CyberDuck](http://cyberduck.io/) (Mac / Windows).
|
GCDWebDAVServer is a subclass of ```GCDWebServer``` that provides a class 1 compliant [WebDAV](https://en.wikipedia.org/wiki/WebDAV) server. This lets users upload, download, delete files and create directories from a directory inside your iOS app's sandbox using any WebDAV client like [Transmit](https://panic.com/transmit/) (Mac), [ForkLift](http://binarynights.com/forklift/) (Mac) or [CyberDuck](http://cyberduck.io/) (Mac / Windows).
|
||||||
|
|
||||||
GCDWebDAVServer should also work with the [OS X Finder](http://support.apple.com/kb/PH13859) as it is partially class 2 compliant (but only when the client is the OS X WebDAV implementation).
|
GCDWebDAVServer should also work with the [macOS Finder](http://support.apple.com/kb/PH13859) as it is partially class 2 compliant (but only when the client is the macOS WebDAV implementation).
|
||||||
|
|
||||||
Simply instantiate and run a GCDWebDAVServer instance then connect to http://{YOUR-IOS-DEVICE-IP-ADDRESS}/ using a WebDAV client:
|
Simply instantiate and run a ```GCDWebDAVServer``` instance then connect to ```http://{YOUR-IOS-DEVICE-IP-ADDRESS}/``` using a WebDAV client:
|
||||||
|
|
||||||
```objectivec
|
```objectivec
|
||||||
#import "GCDWebDAVServer.h"
|
#import "GCDWebDAVServer.h"
|
||||||
|
|
||||||
static GCDWebDAVServer* _davServer = nil; // This should really be an ivar of your application delegate class
|
@interface AppDelegate : NSObject <UIApplicationDelegate> {
|
||||||
|
GCDWebDAVServer* _davServer;
|
||||||
|
}
|
||||||
|
@end
|
||||||
|
|
||||||
|
@implementation AppDelegate
|
||||||
|
|
||||||
- (BOOL)application:(UIApplication*)application didFinishLaunchingWithOptions:(NSDictionary*)launchOptions {
|
- (BOOL)application:(UIApplication*)application didFinishLaunchingWithOptions:(NSDictionary*)launchOptions {
|
||||||
NSString* documentsPath = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) firstObject];
|
NSString* documentsPath = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) firstObject];
|
||||||
@@ -156,6 +229,8 @@ static GCDWebDAVServer* _davServer = nil; // This should really be an ivar of y
|
|||||||
NSLog(@"Visit %@ in your WebDAV client", _davServer.serverURL);
|
NSLog(@"Visit %@ in your WebDAV client", _davServer.serverURL);
|
||||||
return YES;
|
return YES;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@end
|
||||||
```
|
```
|
||||||
|
|
||||||
Serving a Static Website
|
Serving a Static Website
|
||||||
@@ -163,6 +238,7 @@ Serving a Static Website
|
|||||||
|
|
||||||
GCDWebServer includes a built-in handler that can recursively serve a directory (it also lets you control how the ["Cache-Control"](http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.9) header should be set):
|
GCDWebServer includes a built-in handler that can recursively serve a directory (it also lets you control how the ["Cache-Control"](http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.9) header should be set):
|
||||||
|
|
||||||
|
**macOS version (command line tool):**
|
||||||
```objectivec
|
```objectivec
|
||||||
#import "GCDWebServer.h"
|
#import "GCDWebServer.h"
|
||||||
|
|
||||||
@@ -172,7 +248,6 @@ int main(int argc, const char* argv[]) {
|
|||||||
GCDWebServer* webServer = [[GCDWebServer alloc] init];
|
GCDWebServer* webServer = [[GCDWebServer alloc] init];
|
||||||
[webServer addGETHandlerForBasePath:@"/" directoryPath:NSHomeDirectory() indexFilename:nil cacheAge:3600 allowRangeRequests:YES];
|
[webServer addGETHandlerForBasePath:@"/" directoryPath:NSHomeDirectory() indexFilename:nil cacheAge:3600 allowRangeRequests:YES];
|
||||||
[webServer runWithPort:8080];
|
[webServer runWithPort:8080];
|
||||||
[webServer release]; // Remove if using ARC
|
|
||||||
|
|
||||||
}
|
}
|
||||||
return 0;
|
return 0;
|
||||||
@@ -200,28 +275,99 @@ GCDWebServer's architecture consists of only 4 core classes:
|
|||||||
Implementing Handlers
|
Implementing Handlers
|
||||||
=====================
|
=====================
|
||||||
|
|
||||||
GCDWebServer relies on "handlers" to process incoming web requests and generating responses. Handlers are implemented with GCD blocks which makes it very easy to provide your owns. However, they are executed on arbitrary threads within GCD so __special attention must be paid to thread-safety and re-entrancy__.
|
GCDWebServer relies on "handlers" to process incoming web requests and generating responses. Handlers are implemented with GCD blocks which makes it very easy to provide your own. However, they are executed on arbitrary threads within GCD so __special attention must be paid to thread-safety and re-entrancy__.
|
||||||
|
|
||||||
Handlers require 2 GCD blocks:
|
Handlers require 2 GCD blocks:
|
||||||
* The ```GCDWebServerMatchBlock``` is called on every handler added to the ```GCDWebServer``` instance whenever a web request has started (i.e. HTTP headers have been received). It is passed the basic info for the web request (HTTP method, URL, headers...) and must decide if it wants to handle it or not. If yes, it must return a new ```GCDWebServerRequest``` instance (see above) created with this info. Otherwise, it simply returns nil.
|
* The ```GCDWebServerMatchBlock``` is called on every handler added to the ```GCDWebServer``` instance whenever a web request has started (i.e. HTTP headers have been received). It is passed the basic info for the web request (HTTP method, URL, headers...) and must decide if it wants to handle it or not. If yes, it must return a new ```GCDWebServerRequest``` instance (see above) created with this info. Otherwise, it simply returns nil.
|
||||||
* The ```GCDWebServerProcessBlock``` is called after the web request has been fully received and is passed the ```GCDWebServerRequest``` instance created at the previous step. It must return a ```GCDWebServerResponse``` instance (see above) or nil on error, which will result in a 500 HTTP status code returned to the client. It's however recommended to return an instance of [GCDWebServerErrorResponse](GCDWebServer/Responses/GCDWebServerErrorResponse.h) on error so more useful information can be returned to the client.
|
* The ```GCDWebServerProcessBlock``` or ```GCDWebServerAsyncProcessBlock``` is called after the web request has been fully received and is passed the ```GCDWebServerRequest``` instance created at the previous step. It must return synchronously (if using ```GCDWebServerProcessBlock```) or asynchronously (if using ```GCDWebServerAsyncProcessBlock```) a ```GCDWebServerResponse``` instance (see above) or nil on error, which will result in a 500 HTTP status code returned to the client. It's however recommended to return an instance of [GCDWebServerErrorResponse](GCDWebServer/Responses/GCDWebServerErrorResponse.h) on error so more useful information can be returned to the client.
|
||||||
|
|
||||||
Note that most methods on ```GCDWebServer``` to add handlers only require the ```GCDWebServerProcessBlock``` as they already provide a built-in ```GCDWebServerMatchBlock``` e.g. to match a URL path with a Regex.
|
Note that most methods on ```GCDWebServer``` to add handlers only require the ```GCDWebServerProcessBlock``` or ```GCDWebServerAsyncProcessBlock``` as they already provide a built-in ```GCDWebServerMatchBlock``` e.g. to match a URL path with a Regex.
|
||||||
|
|
||||||
|
Asynchronous HTTP Responses
|
||||||
|
===========================
|
||||||
|
|
||||||
|
New in GCDWebServer 3.0 is the ability to process HTTP requests asynchronously i.e. add handlers to the server which generate their ```GCDWebServerResponse``` asynchronously. This is achieved by adding handlers that use a ```GCDWebServerAsyncProcessBlock``` instead of a ```GCDWebServerProcessBlock```. Here's an example:
|
||||||
|
|
||||||
|
**(Synchronous version)** The handler blocks while generating the HTTP response:
|
||||||
|
```objectivec
|
||||||
|
[webServer addDefaultHandlerForMethod:@"GET"
|
||||||
|
requestClass:[GCDWebServerRequest class]
|
||||||
|
processBlock:^GCDWebServerResponse *(GCDWebServerRequest* request) {
|
||||||
|
|
||||||
|
GCDWebServerDataResponse* response = [GCDWebServerDataResponse responseWithHTML:@"<html><body><p>Hello World</p></body></html>"];
|
||||||
|
return response;
|
||||||
|
|
||||||
|
}];
|
||||||
|
```
|
||||||
|
|
||||||
|
**(Asynchronous version)** The handler returns immediately and calls back GCDWebServer later with the generated HTTP response:
|
||||||
|
```objectivec
|
||||||
|
[webServer addDefaultHandlerForMethod:@"GET"
|
||||||
|
requestClass:[GCDWebServerRequest class]
|
||||||
|
asyncProcessBlock:^(GCDWebServerRequest* request, GCDWebServerCompletionBlock completionBlock) {
|
||||||
|
|
||||||
|
// Do some async operation like network access or file I/O (simulated here using dispatch_after())
|
||||||
|
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3 * NSEC_PER_SEC)), dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
|
||||||
|
GCDWebServerDataResponse* response = [GCDWebServerDataResponse responseWithHTML:@"<html><body><p>Hello World</p></body></html>"];
|
||||||
|
completionBlock(response);
|
||||||
|
});
|
||||||
|
|
||||||
|
}];
|
||||||
|
```
|
||||||
|
|
||||||
|
**(Advanced asynchronous version)** The handler returns immediately a streamed HTTP response which itself generates its contents asynchronously:
|
||||||
|
```objectivec
|
||||||
|
[webServer addDefaultHandlerForMethod:@"GET"
|
||||||
|
requestClass:[GCDWebServerRequest class]
|
||||||
|
processBlock:^GCDWebServerResponse *(GCDWebServerRequest* request) {
|
||||||
|
|
||||||
|
NSMutableArray* contents = [NSMutableArray arrayWithObjects:@"<html><body><p>\n", @"Hello World!\n", @"</p></body></html>\n", nil]; // Fake data source we are reading from
|
||||||
|
GCDWebServerStreamedResponse* response = [GCDWebServerStreamedResponse responseWithContentType:@"text/html" asyncStreamBlock:^(GCDWebServerBodyReaderCompletionBlock completionBlock) {
|
||||||
|
|
||||||
|
// Simulate a delay reading from the fake data source
|
||||||
|
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1 * NSEC_PER_SEC)), dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
|
||||||
|
NSString* string = contents.firstObject;
|
||||||
|
if (string) {
|
||||||
|
[contents removeObjectAtIndex:0];
|
||||||
|
completionBlock([string dataUsingEncoding:NSUTF8StringEncoding], nil); // Generate the 2nd part of the stream data
|
||||||
|
} else {
|
||||||
|
completionBlock([NSData data], nil); // Must pass an empty NSData to signal the end of the stream
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
}];
|
||||||
|
return response;
|
||||||
|
|
||||||
|
}];
|
||||||
|
```
|
||||||
|
|
||||||
|
*Note that you can even combine both the asynchronous and advanced asynchronous versions to return asynchronously an asynchronous HTTP response!*
|
||||||
|
|
||||||
GCDWebServer & Background Mode for iOS Apps
|
GCDWebServer & Background Mode for iOS Apps
|
||||||
===========================================
|
===========================================
|
||||||
|
|
||||||
When doing networking operations in iOS apps, you must handle carefully [what happens when iOS puts the app in the background](https://developer.apple.com/library/ios/technotes/tn2277/_index.html). Typically you must stop any server while the app is in the background and restart them when it comes back to the foreground. This can become quite complex considering the server might have ongoing connections when it needs to be stopped.
|
When doing networking operations in iOS apps, you must handle carefully [what happens when iOS puts the app in the background](https://developer.apple.com/library/ios/technotes/tn2277/_index.html). Typically you must stop any network servers while the app is in the background and restart them when the app comes back to the foreground. This can become quite complex considering servers might have ongoing connections when they need to be stopped.
|
||||||
|
|
||||||
Fortunately, GCDWebServer does all of this automatically for you:
|
Fortunately, GCDWebServer does all of this automatically for you:
|
||||||
- GCDWebServer begins a [background task](https://developer.apple.com/library/ios/documentation/iphone/conceptual/iphoneosprogrammingguide/ManagingYourApplicationsFlow/ManagingYourApplicationsFlow.html) whenever the first HTTP connection is opened and ends it only when the last one is closed. This prevents iOS from suspending the app after it goes in the background, which would immediately kill HTTP connections to the client.
|
- GCDWebServer begins a [background task](https://developer.apple.com/library/archive/documentation/iPhone/Conceptual/iPhoneOSProgrammingGuide/BackgroundExecution/BackgroundExecution.html) whenever the first HTTP connection is opened and ends it only when the last one is closed. This prevents iOS from suspending the app after it goes in the background, which would immediately kill HTTP connections to the client.
|
||||||
- While the app is in the background, as long as new HTTP connections keep being initiated, the background task will continue to exist and iOS will not suspend the app (unless under sudden and unexpected memory pressure).
|
- While the app is in the background, as long as new HTTP connections keep being initiated, the background task will continue to exist and iOS will not suspend the app **for up to 10 minutes** (unless under sudden and unexpected memory pressure).
|
||||||
- If the app is still in the background when the last HTTP connection is closed, GCDWebServer will suspend itself and stop accepting new connections as if you had called ```-stop``` (this behavior can be disabled with the ```GCDWebServerOption_AutomaticallySuspendInBackground``` option).
|
- If the app is still in the background when the last HTTP connection is closed, GCDWebServer will suspend itself and stop accepting new connections as if you had called ```-stop``` (this behavior can be disabled with the ```GCDWebServerOption_AutomaticallySuspendInBackground``` option).
|
||||||
- If the app goes in the background while no HTTP connections are opened, GCDWebServer will immediately suspend itself and stop accepting new connections as if you had called ```-stop``` (this behavior can be disabled with the ```GCDWebServerOption_AutomaticallySuspendInBackground``` option).
|
- If the app goes in the background while no HTTP connections are opened, GCDWebServer will immediately suspend itself and stop accepting new connections as if you had called ```-stop``` (this behavior can be disabled with the ```GCDWebServerOption_AutomaticallySuspendInBackground``` option).
|
||||||
- If the app comes back to the foreground and GCDWebServer had been suspended, it will automatically resume itself and start accepting again new HTTP connections as if you had called ```-start```.
|
- If the app comes back to the foreground and GCDWebServer had been suspended, it will automatically resume itself and start accepting again new HTTP connections as if you had called ```-start```.
|
||||||
|
|
||||||
HTTP connections are often initiated in batches (or bursts), for instance when loading a web page with multiple resources. This makes it difficult to accurately detect when the *very last* HTTP connection has been closed: it's possible 2 consecutive HTTP connections part of the same batch would be separated by a small delay instead of overlapping. It would be bad for the client if GCDWebServer suspended itself right in between. The ```GCDWebServerOption_ConnectedStateCoalescingInterval``` option solves this problem elegantly by forcing GCDWebServer to wait some extra delay before performing any action after the last HTTP connection has been closed, just in case a new HTTP connection is initiated within this delay.
|
HTTP connections are often initiated in batches (or bursts), for instance when loading a web page with multiple resources. This makes it difficult to accurately detect when the *very last* HTTP connection has been closed: it's possible 2 consecutive HTTP connections part of the same batch would be separated by a small delay instead of overlapping. It would be bad for the client if GCDWebServer suspended itself right in between. The ```GCDWebServerOption_ConnectedStateCoalescingInterval``` option solves this problem elegantly by forcing GCDWebServer to wait some extra delay before performing any action after the last HTTP connection has been closed, just in case a new HTTP connection is initiated within this delay.
|
||||||
|
|
||||||
|
Logging in GCDWebServer
|
||||||
|
=======================
|
||||||
|
|
||||||
|
Both for debugging and informational purpose, GCDWebServer logs messages extensively whenever something happens. Furthermore, when building GCDWebServer in "Debug" mode versus "Release" mode, it logs even more information but also performs a number of internal consistency checks. To enable this behavior, define the preprocessor constant ```DEBUG=1``` when compiling GCDWebServer. In Xcode target settings, this can be done by adding ```DEBUG=1``` to the build setting ```GCC_PREPROCESSOR_DEFINITIONS``` when building in "Debug" configuration. Finally, you can also control the logging verbosity at run time by calling ```+[GCDWebServer setLogLevel:]```.
|
||||||
|
|
||||||
|
By default, all messages logged by GCDWebServer are sent to its built-in logging facility, which simply outputs to ```stderr``` (assuming a terminal type device is connected). In order to better integrate with the rest of your app or because of the amount of information logged, you might want to use another logging facility.
|
||||||
|
|
||||||
|
GCDWebServer has automatic support for [XLFacility](https://github.com/swisspol/XLFacility) (by the same author as GCDWebServer and also open-source): if it is in the same Xcode project, GCDWebServer should use it automatically instead of the built-in logging facility (see [GCDWebServerPrivate.h](GCDWebServer/Core/GCDWebServerPrivate.h) for the implementation details).
|
||||||
|
|
||||||
|
It's also possible to use a custom logging facility - see [GCDWebServer.h](GCDWebServer/Core/GCDWebServer.h) for more information.
|
||||||
|
|
||||||
Advanced Example 1: Implementing HTTP Redirects
|
Advanced Example 1: Implementing HTTP Redirects
|
||||||
===============================================
|
===============================================
|
||||||
|
|
||||||
@@ -320,4 +466,4 @@ Final Example: File Downloads and Uploads From iOS App
|
|||||||
|
|
||||||
GCDWebServer was originally written for the [ComicFlow](http://itunes.apple.com/us/app/comicflow/id409290355?mt=8) comic reader app for iPad. It allow users to connect to their iPad with their web browser over WiFi and then upload, download and organize comic files inside the app.
|
GCDWebServer was originally written for the [ComicFlow](http://itunes.apple.com/us/app/comicflow/id409290355?mt=8) comic reader app for iPad. It allow users to connect to their iPad with their web browser over WiFi and then upload, download and organize comic files inside the app.
|
||||||
|
|
||||||
ComicFlow is [entirely open-source](https://github.com/swisspol/ComicFlow) and you can see how it uses GCDWebUploader in the [WebServer.m](https://github.com/swisspol/ComicFlow/blob/master/Classes/WebServer.m) file.
|
ComicFlow is [entirely open-source](https://github.com/swisspol/ComicFlow) and you can see how it uses GCDWebServer in the [WebServer.h](https://github.com/swisspol/ComicFlow/blob/master/Classes/WebServer.h) and [WebServer.m](https://github.com/swisspol/ComicFlow/blob/master/Classes/WebServer.m) files.
|
||||||
|
|||||||
97
Run-Tests.sh
97
Run-Tests.sh
@@ -1,58 +1,83 @@
|
|||||||
#!/bin/bash -ex
|
#!/bin/bash -exu -o pipefail
|
||||||
|
|
||||||
|
if [[ -f "/usr/local/bin/xcpretty" ]]; then
|
||||||
|
PRETTYFIER="xcpretty"
|
||||||
|
else
|
||||||
|
PRETTYFIER="tee" # Passthrough stdout
|
||||||
|
fi
|
||||||
|
|
||||||
OSX_SDK="macosx"
|
OSX_SDK="macosx"
|
||||||
if [ -z "$TRAVIS" ]; then
|
IOS_SDK="iphonesimulator"
|
||||||
IOS_SDK="iphoneos"
|
TVOS_SDK="appletvsimulator"
|
||||||
else
|
|
||||||
IOS_SDK="iphonesimulator"
|
OSX_SDK_VERSION=`xcodebuild -version -sdk | grep -A 1 '^MacOSX' | tail -n 1 | awk '{ print $2 }'`
|
||||||
fi
|
IOS_SDK_VERSION=`xcodebuild -version -sdk | grep -A 1 '^iPhoneOS' | tail -n 1 | awk '{ print $2 }'`
|
||||||
|
TVOS_SDK_VERSION=`xcodebuild -version -sdk | grep -A 1 '^AppleTVOS' | tail -n 1 | awk '{ print $2 }'`
|
||||||
|
|
||||||
OSX_TARGET="GCDWebServer (Mac)"
|
OSX_TARGET="GCDWebServer (Mac)"
|
||||||
IOS_TARGET="GCDWebServer (iOS)"
|
IOS_TARGET="GCDWebServer (iOS)"
|
||||||
|
TVOS_TARGET="GCDWebServer (tvOS)"
|
||||||
CONFIGURATION="Release"
|
CONFIGURATION="Release"
|
||||||
|
|
||||||
MRC_BUILD_DIR="/tmp/GCDWebServer-MRC"
|
OSX_TEST_SCHEME="GCDWebServers (Mac)"
|
||||||
MRC_PRODUCT="$MRC_BUILD_DIR/$CONFIGURATION/GCDWebServer"
|
|
||||||
ARC_BUILD_DIR="/tmp/GCDWebServer-ARC"
|
BUILD_DIR="`pwd`/build"
|
||||||
ARC_PRODUCT="$ARC_BUILD_DIR/$CONFIGURATION/GCDWebServer"
|
PRODUCT="$BUILD_DIR/$CONFIGURATION/GCDWebServer"
|
||||||
|
|
||||||
PAYLOAD_ZIP="Tests/Payload.zip"
|
PAYLOAD_ZIP="Tests/Payload.zip"
|
||||||
PAYLOAD_DIR="/tmp/GCDWebServer"
|
PAYLOAD_DIR="`pwd`/build/Payload"
|
||||||
|
|
||||||
function runTests {
|
function runTests {
|
||||||
|
EXECUTABLE="$1"
|
||||||
|
MODE="$2"
|
||||||
|
TESTS="$3"
|
||||||
|
FILE="${4:-}"
|
||||||
|
|
||||||
rm -rf "$PAYLOAD_DIR"
|
rm -rf "$PAYLOAD_DIR"
|
||||||
ditto -x -k "$PAYLOAD_ZIP" "$PAYLOAD_DIR"
|
ditto -x -k "$PAYLOAD_ZIP" "$PAYLOAD_DIR"
|
||||||
TZ=GMT find "$PAYLOAD_DIR" -type d -exec SetFile -d "1/1/2014 00:00:00" -m "1/1/2014 00:00:00" '{}' \; # ZIP archives do not preserve directories dates
|
TZ=GMT find "$PAYLOAD_DIR" -type d -exec SetFile -d "1/1/2014 00:00:00" -m "1/1/2014 00:00:00" '{}' \; # ZIP archives do not preserve directories dates
|
||||||
logLevel=2 $1 -mode "$2" -root "$PAYLOAD_DIR/Payload" -tests "$3"
|
if [ "$FILE" != "" ]; then
|
||||||
|
cp -f "$4" "$PAYLOAD_DIR/Payload"
|
||||||
|
pushd "$PAYLOAD_DIR/Payload"
|
||||||
|
TZ=GMT SetFile -d "1/1/2014 00:00:00" -m "1/1/2014 00:00:00" `basename "$FILE"`
|
||||||
|
popd
|
||||||
|
fi
|
||||||
|
logLevel=2 $EXECUTABLE -mode "$MODE" -root "$PAYLOAD_DIR/Payload" -tests "$TESTS"
|
||||||
}
|
}
|
||||||
|
|
||||||
# Build for iOS in manual memory management mode (TODO: run tests on iOS)
|
# Run built-in OS X tests
|
||||||
rm -rf "$MRC_BUILD_DIR"
|
rm -rf "$BUILD_DIR"
|
||||||
xcodebuild -sdk "$IOS_SDK" -target "$IOS_TARGET" -configuration "$CONFIGURATION" build "SYMROOT=$MRC_BUILD_DIR" "CLANG_ENABLE_OBJC_ARC=NO" > /dev/null
|
xcodebuild test -scheme "$OSX_TEST_SCHEME" "SYMROOT=$BUILD_DIR"
|
||||||
|
|
||||||
# Build for iOS in ARC mode (TODO: run tests on iOS)
|
# Build for OS X for oldest supported deployment target
|
||||||
rm -rf "$ARC_BUILD_DIR"
|
rm -rf "$BUILD_DIR"
|
||||||
xcodebuild -sdk "$IOS_SDK" -target "$IOS_TARGET" -configuration "$CONFIGURATION" build "SYMROOT=$ARC_BUILD_DIR" "CLANG_ENABLE_OBJC_ARC=YES" > /dev/null
|
xcodebuild build -sdk "$OSX_SDK" -target "$OSX_TARGET" -configuration "$CONFIGURATION" "SYMROOT=$BUILD_DIR" "MACOSX_DEPLOYMENT_TARGET=10.7" | $PRETTYFIER
|
||||||
|
|
||||||
# Build for OS X in manual memory management mode
|
|
||||||
rm -rf "$MRC_BUILD_DIR"
|
|
||||||
xcodebuild -sdk "$OSX_SDK" -target "$OSX_TARGET" -configuration "$CONFIGURATION" build "SYMROOT=$MRC_BUILD_DIR" "CLANG_ENABLE_OBJC_ARC=NO" > /dev/null
|
|
||||||
|
|
||||||
# Build for OS X in ARC mode
|
|
||||||
rm -rf "$ARC_BUILD_DIR"
|
|
||||||
xcodebuild -sdk "$OSX_SDK" -target "$OSX_TARGET" -configuration "$CONFIGURATION" build "SYMROOT=$ARC_BUILD_DIR" "CLANG_ENABLE_OBJC_ARC=YES" > /dev/null
|
|
||||||
|
|
||||||
# Run tests
|
# Run tests
|
||||||
runTests $MRC_PRODUCT "webServer" "Tests/WebServer"
|
runTests $PRODUCT "htmlForm" "Tests/HTMLForm"
|
||||||
runTests $ARC_PRODUCT "webServer" "Tests/WebServer"
|
runTests $PRODUCT "htmlFileUpload" "Tests/HTMLFileUpload"
|
||||||
runTests $MRC_PRODUCT "webDAV" "Tests/WebDAV-Transmit"
|
runTests $PRODUCT "webServer" "Tests/WebServer"
|
||||||
runTests $ARC_PRODUCT "webDAV" "Tests/WebDAV-Transmit"
|
runTests $PRODUCT "webDAV" "Tests/WebDAV-Transmit"
|
||||||
runTests $MRC_PRODUCT "webDAV" "Tests/WebDAV-Cyberduck"
|
runTests $PRODUCT "webDAV" "Tests/WebDAV-Cyberduck"
|
||||||
runTests $ARC_PRODUCT "webDAV" "Tests/WebDAV-Cyberduck"
|
runTests $PRODUCT "webDAV" "Tests/WebDAV-Finder"
|
||||||
runTests $MRC_PRODUCT "webDAV" "Tests/WebDAV-Finder"
|
runTests $PRODUCT "webUploader" "Tests/WebUploader"
|
||||||
runTests $ARC_PRODUCT "webDAV" "Tests/WebDAV-Finder"
|
runTests $PRODUCT "webServer" "Tests/WebServer-Sample-Movie" "Tests/Sample-Movie.mp4"
|
||||||
runTests $MRC_PRODUCT "webUploader" "Tests/WebUploader"
|
|
||||||
runTests $ARC_PRODUCT "webUploader" "Tests/WebUploader"
|
# Build for OS X for current deployment target
|
||||||
|
rm -rf "$BUILD_DIR"
|
||||||
|
xcodebuild build -sdk "$OSX_SDK" -target "$OSX_TARGET" -configuration "$CONFIGURATION" "SYMROOT=$BUILD_DIR" "MACOSX_DEPLOYMENT_TARGET=$OSX_SDK_VERSION" | $PRETTYFIER
|
||||||
|
|
||||||
|
# Build for iOS for oldest supported deployment target
|
||||||
|
rm -rf "$BUILD_DIR"
|
||||||
|
xcodebuild build -sdk "$IOS_SDK" -target "$IOS_TARGET" -configuration "$CONFIGURATION" "SYMROOT=$BUILD_DIR" "IPHONEOS_DEPLOYMENT_TARGET=8.0" | $PRETTYFIER
|
||||||
|
|
||||||
|
# Build for iOS for current deployment target
|
||||||
|
rm -rf "$BUILD_DIR"
|
||||||
|
xcodebuild build -sdk "$IOS_SDK" -target "$IOS_TARGET" -configuration "$CONFIGURATION" "SYMROOT=$BUILD_DIR" "IPHONEOS_DEPLOYMENT_TARGET=$IOS_SDK_VERSION" | $PRETTYFIER
|
||||||
|
|
||||||
|
# Build for tvOS for current deployment target
|
||||||
|
rm -rf "$BUILD_DIR"
|
||||||
|
xcodebuild build -sdk "$TVOS_SDK" -target "$TVOS_TARGET" -configuration "$CONFIGURATION" "SYMROOT=$BUILD_DIR" "TVOS_DEPLOYMENT_TARGET=$TVOS_SDK_VERSION" | $PRETTYFIER
|
||||||
|
|
||||||
# Done
|
# Done
|
||||||
echo "\nAll tests completed successfully!"
|
echo "\nAll tests completed successfully!"
|
||||||
|
|||||||
9
Tests/HTMLFileUpload/001-200.response
Normal file
9
Tests/HTMLFileUpload/001-200.response
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
HTTP/1.1 200 OK
|
||||||
|
Cache-Control: no-cache
|
||||||
|
Content-Length: 299
|
||||||
|
Content-Type: text/html; charset=utf-8
|
||||||
|
Connection: Close
|
||||||
|
Server: GCDWebServer
|
||||||
|
Date: Fri, 25 Apr 2014 14:15:11 GMT
|
||||||
|
|
||||||
|
<html><body> <form name="input" action="/" method="post" enctype="multipart/form-data"> <input type="hidden" name="secret" value="42"> <input type="file" name="files" multiple><br/> <input type="submit" value="Submit"> </form> </body></html>
|
||||||
10
Tests/HTMLFileUpload/001-GET.request
Normal file
10
Tests/HTMLFileUpload/001-GET.request
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
GET / HTTP/1.1
|
||||||
|
Host: localhost:8080
|
||||||
|
Connection: keep-alive
|
||||||
|
Cache-Control: max-age=0
|
||||||
|
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
|
||||||
|
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/34.0.1847.131 Safari/537.36
|
||||||
|
DNT: 1
|
||||||
|
Accept-Encoding: gzip,deflate,sdch
|
||||||
|
Accept-Language: en-US,en;q=0.8,fr;q=0.6
|
||||||
|
|
||||||
9
Tests/HTMLFileUpload/002-200.response
Normal file
9
Tests/HTMLFileUpload/002-200.response
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
HTTP/1.1 200 OK
|
||||||
|
Cache-Control: no-cache
|
||||||
|
Content-Length: 447
|
||||||
|
Content-Type: text/html; charset=utf-8
|
||||||
|
Connection: Close
|
||||||
|
Server: GCDWebServer
|
||||||
|
Date: Fri, 25 Apr 2014 14:15:21 GMT
|
||||||
|
|
||||||
|
<html><body><p>secret = 42<br>files = "hero_mba_11.jpg" (image/jpeg | 106 KB)<br>files = "Test File.txt" (text/plain | 21 Bytes)<br></p><hr> <form name="input" action="/" method="post" enctype="multipart/form-data"> <input type="hidden" name="secret" value="42"> <input type="file" name="files" multiple><br/> <input type="submit" value="Submit"> </form> </body></html>
|
||||||
BIN
Tests/WebDAV-Finder/059-200.response → Tests/HTMLFileUpload/002-POST.request
Executable file → Normal file
BIN
Tests/WebDAV-Finder/059-200.response → Tests/HTMLFileUpload/002-POST.request
Executable file → Normal file
Binary file not shown.
9
Tests/HTMLForm/001-200.response
Normal file
9
Tests/HTMLForm/001-200.response
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
HTTP/1.1 200 OK
|
||||||
|
Cache-Control: no-cache
|
||||||
|
Content-Length: 293
|
||||||
|
Content-Type: text/html; charset=utf-8
|
||||||
|
Connection: Close
|
||||||
|
Server: GCDWebServer
|
||||||
|
Date: Fri, 25 Apr 2014 14:12:09 GMT
|
||||||
|
|
||||||
|
<html><body> <form name="input" action="/" method="post" enctype="application/x-www-form-urlencoded"> Value: <input type="text" name="value"> <input type="submit" value="Submit"> </form> </body></html>
|
||||||
10
Tests/HTMLForm/001-GET.request
Normal file
10
Tests/HTMLForm/001-GET.request
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
GET / HTTP/1.1
|
||||||
|
Host: localhost:8080
|
||||||
|
Connection: keep-alive
|
||||||
|
Cache-Control: max-age=0
|
||||||
|
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
|
||||||
|
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/34.0.1847.131 Safari/537.36
|
||||||
|
DNT: 1
|
||||||
|
Accept-Encoding: gzip,deflate,sdch
|
||||||
|
Accept-Language: en-US,en;q=0.8,fr;q=0.6
|
||||||
|
|
||||||
9
Tests/HTMLForm/002-200.response
Normal file
9
Tests/HTMLForm/002-200.response
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
HTTP/1.1 200 OK
|
||||||
|
Cache-Control: no-cache
|
||||||
|
Content-Length: 47
|
||||||
|
Content-Type: text/html; charset=utf-8
|
||||||
|
Connection: Close
|
||||||
|
Server: GCDWebServer
|
||||||
|
Date: Fri, 25 Apr 2014 14:12:20 GMT
|
||||||
|
|
||||||
|
<html><body><p>Hellø Wörld!</p></body></html>
|
||||||
15
Tests/HTMLForm/002-POST.request
Normal file
15
Tests/HTMLForm/002-POST.request
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
POST / HTTP/1.1
|
||||||
|
Host: localhost:8080
|
||||||
|
Connection: keep-alive
|
||||||
|
Content-Length: 30
|
||||||
|
Cache-Control: max-age=0
|
||||||
|
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
|
||||||
|
Origin: http://localhost:8080
|
||||||
|
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/34.0.1847.131 Safari/537.36
|
||||||
|
Content-Type: application/x-www-form-urlencoded
|
||||||
|
DNT: 1
|
||||||
|
Referer: http://localhost:8080/
|
||||||
|
Accept-Encoding: gzip,deflate,sdch
|
||||||
|
Accept-Language: en-US,en;q=0.8,fr;q=0.6
|
||||||
|
|
||||||
|
value=Hell%C3%B8+W%C3%B6rld%21
|
||||||
BIN
Tests/Sample-Movie.mp4
Normal file
BIN
Tests/Sample-Movie.mp4
Normal file
Binary file not shown.
0
Tests/WebDAV-Cyberduck/001-200.response
Executable file → Normal file
0
Tests/WebDAV-Cyberduck/001-200.response
Executable file → Normal file
0
Tests/WebDAV-Cyberduck/001-HEAD.request
Executable file → Normal file
0
Tests/WebDAV-Cyberduck/001-HEAD.request
Executable file → Normal file
0
Tests/WebDAV-Cyberduck/002-207.response
Executable file → Normal file
0
Tests/WebDAV-Cyberduck/002-207.response
Executable file → Normal file
0
Tests/WebDAV-Cyberduck/002-PROPFIND.request
Executable file → Normal file
0
Tests/WebDAV-Cyberduck/002-PROPFIND.request
Executable file → Normal file
0
Tests/WebDAV-Cyberduck/003-207.response
Executable file → Normal file
0
Tests/WebDAV-Cyberduck/003-207.response
Executable file → Normal file
0
Tests/WebDAV-Cyberduck/003-PROPFIND.request
Executable file → Normal file
0
Tests/WebDAV-Cyberduck/003-PROPFIND.request
Executable file → Normal file
0
Tests/WebDAV-Cyberduck/004-207.response
Executable file → Normal file
0
Tests/WebDAV-Cyberduck/004-207.response
Executable file → Normal file
0
Tests/WebDAV-Cyberduck/004-PROPFIND.request
Executable file → Normal file
0
Tests/WebDAV-Cyberduck/004-PROPFIND.request
Executable file → Normal file
0
Tests/WebDAV-Cyberduck/005-200.response
Executable file → Normal file
0
Tests/WebDAV-Cyberduck/005-200.response
Executable file → Normal file
0
Tests/WebDAV-Cyberduck/005-HEAD.request
Executable file → Normal file
0
Tests/WebDAV-Cyberduck/005-HEAD.request
Executable file → Normal file
0
Tests/WebDAV-Cyberduck/006-404.response
Executable file → Normal file
0
Tests/WebDAV-Cyberduck/006-404.response
Executable file → Normal file
0
Tests/WebDAV-Cyberduck/006-HEAD.request
Executable file → Normal file
0
Tests/WebDAV-Cyberduck/006-HEAD.request
Executable file → Normal file
0
Tests/WebDAV-Cyberduck/007-201.response
Executable file → Normal file
0
Tests/WebDAV-Cyberduck/007-201.response
Executable file → Normal file
0
Tests/WebDAV-Cyberduck/007-COPY.request
Executable file → Normal file
0
Tests/WebDAV-Cyberduck/007-COPY.request
Executable file → Normal file
0
Tests/WebDAV-Cyberduck/008-207.response
Executable file → Normal file
0
Tests/WebDAV-Cyberduck/008-207.response
Executable file → Normal file
0
Tests/WebDAV-Cyberduck/008-PROPFIND.request
Executable file → Normal file
0
Tests/WebDAV-Cyberduck/008-PROPFIND.request
Executable file → Normal file
0
Tests/WebDAV-Cyberduck/009-200.response
Executable file → Normal file
0
Tests/WebDAV-Cyberduck/009-200.response
Executable file → Normal file
0
Tests/WebDAV-Cyberduck/009-HEAD.request
Executable file → Normal file
0
Tests/WebDAV-Cyberduck/009-HEAD.request
Executable file → Normal file
0
Tests/WebDAV-Cyberduck/010-200.response
Executable file → Normal file
0
Tests/WebDAV-Cyberduck/010-200.response
Executable file → Normal file
0
Tests/WebDAV-Cyberduck/010-GET.request
Executable file → Normal file
0
Tests/WebDAV-Cyberduck/010-GET.request
Executable file → Normal file
0
Tests/WebDAV-Cyberduck/011-207.response
Executable file → Normal file
0
Tests/WebDAV-Cyberduck/011-207.response
Executable file → Normal file
0
Tests/WebDAV-Cyberduck/011-PROPFIND.request
Executable file → Normal file
0
Tests/WebDAV-Cyberduck/011-PROPFIND.request
Executable file → Normal file
0
Tests/WebDAV-Cyberduck/012-204.response
Executable file → Normal file
0
Tests/WebDAV-Cyberduck/012-204.response
Executable file → Normal file
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user