282 Commits
1.4 ... 3.2

Author SHA1 Message Date
Pierre-Olivier Latour
9d9546bb6d Bumped version 2014-11-07 11:44:11 +09:00
Pierre-Olivier Latour
2ff05b1aa0 Bumped version 2014-11-07 11:43:10 +09:00
Pierre-Olivier Latour
bf2c9a170d Workaround Firefox and IE not showing file selection dialog 2014-11-07 11:42:36 +09:00
Pierre-Olivier Latour
15caa9cd20 Fix 2014-10-18 12:42:38 -07:00
Pierre-Olivier Latour
32ba49ae34 Removed MRC support entirely 2014-10-18 09:32:24 -07:00
Pierre-Olivier Latour
8b87924776 Bumped version 2014-10-16 08:27:05 -07:00
Pierre-Olivier Latour
5bda05c1f9 Update README.md 2014-10-16 08:19:28 -07:00
Pierre-Olivier Latour
a8481af765 Update README.md 2014-10-16 08:18:42 -07:00
Pierre-Olivier Latour
b5ad507a57 Update README.md 2014-10-16 08:17:36 -07:00
Pierre-Olivier Latour
8c8e4847a5 Fix 2014-10-16 08:13:31 -07:00
Pierre-Olivier Latour
514c09dc39 Added truly asynchronous support to GCDWebServerStreamedResponse 2014-10-14 12:40:51 -07:00
Pierre-Olivier Latour
c4bf7b11e2 Added support for asynchronous reading in GCDWebServerBodyReader 2014-10-14 12:40:19 -07:00
Pierre-Olivier Latour
a933b2126e Fix 2014-10-14 12:10:10 -07:00
Pierre-Olivier Latour
001df4ea39 Fix 2014-10-14 01:09:17 -07:00
Pierre-Olivier Latour
75d018a375 #85 Added support for IPv6 2014-10-14 00:45:08 -07:00
Pierre-Olivier Latour
4449e42601 Update README.md 2014-10-13 22:48:09 -07:00
Pierre-Olivier Latour
c45053bc11 Added support for third-party logging facilities 2014-10-13 22:45:07 -07:00
Pierre-Olivier Latour
5070e4fc33 Update README.md 2014-10-13 12:29:06 -07:00
Pierre-Olivier Latour
7c1e70a538 Added XLFacilityLogging.h 2014-10-13 12:23:42 -07:00
Pierre-Olivier Latour
7102c7922e Fix 2014-10-13 12:23:23 -07:00
Pierre-Olivier Latour
2de9418307 Enabled ENABLE_STRICT_OBJC_MSGSEND 2014-10-13 12:04:01 -07:00
Pierre-Olivier Latour
e59cf4b6df Bumped version 2014-10-12 23:31:42 -07:00
Pierre-Olivier Latour
9e8f0e00f3 Updated iOS app for iOS 8 SDK 2014-10-12 00:47:12 -07:00
Pierre-Olivier Latour
d7650a71e0 Lowered deployment targets 2014-10-11 01:02:01 -07:00
Pierre-Olivier Latour
420ddc3eac Replaced preprocessor constant "NDEBUG" by the more standard "DEBUG" one and flipped behavior accordingly 2014-10-10 10:27:07 -07:00
Pierre-Olivier Latour
143e38c968 Added README and podspec files to Xcode project for convenience 2014-10-10 10:18:20 -07:00
Pierre-Olivier Latour
8e9fe4c4c1 Update README.md 2014-10-09 12:31:05 -07:00
Pierre-Olivier Latour
95bccff2f7 Fix 2014-10-09 10:16:13 -07:00
Pierre-Olivier Latour
780a608d6c Update README.md 2014-10-09 09:57:08 -07:00
Pierre-Olivier Latour
18d93bbf47 Bumped version 2014-10-09 09:54:08 -07:00
Pierre-Olivier Latour
b35ebd7d58 #92 Added support for async handlers 2014-10-09 09:53:03 -07:00
Pierre-Olivier Latour
a11b047233 Renamed GCDWebServerStreamingBlock to GCDWebServerStreamBlock 2014-10-09 09:43:59 -07:00
Pierre-Olivier Latour
4eac9d4f8e Upgrade to Xcode 6.1 2014-10-09 09:09:34 -07:00
Pierre-Olivier Latour
d1e2a1a12f Bumped version 2014-10-07 13:27:04 -07:00
Pierre-Olivier Latour
54d5abd3a8 Fixed rare race-condition with disconnection timer 2014-10-07 13:26:55 -07:00
Pierre-Olivier Latour
a9fee8d7e2 Fixed rare exception in GCDWebServerParseURLEncodedForm() 2014-10-07 12:16:14 -07:00
Pierre-Olivier Latour
6b15bdaa4e Merge pull request #89 from nickgravelyn/capture-regex-parameters-in-request
Added attributes to GCDWebServerRequest with support to retrieve regex captured variables
2014-09-30 10:52:31 -07:00
Nick Gravelyn
3771cf4e92 Adding an attribute collection to GCDWebServerRequest and adding regular expression captures as an attribute. 2014-09-30 10:45:45 -07:00
Pierre-Olivier Latour
a5d83abdd0 Bumped version to 2.5.2 2014-09-21 10:35:46 -07:00
Pierre-Olivier Latour
f1e9f1a37c Improved handling of symbolic links in -addGETHandlerForBasePath:directoryPath:indexFilename:cacheAge:allowRangeRequests: 2014-09-19 08:19:49 -07:00
Pierre-Olivier Latour
00b5ec87ba #76 Fixed -bonjourServerURL to correctly return hostname instead of service name 2014-09-15 09:00:39 -07:00
Pierre-Olivier Latour
cf94e70a42 Fall back to CFBundleName if CFBundleDisplayName is not available 2014-09-15 08:05:07 -07:00
Pierre-Olivier Latour
d47409c776 Updated for Xcode6 2014-09-15 08:05:00 -07:00
Pierre-Olivier Latour
a9db13475b Bumped version 2014-08-24 12:28:51 -07:00
Pierre-Olivier Latour
17fad0f1b9 Improved automatic detection of when to use dispatch_retain() and dispatch_release() depending on compiler settings 2014-08-24 12:24:16 -07:00
Pierre-Olivier Latour
5493d9e803 Run test against default and oldest supported deployment targets 2014-08-24 12:20:55 -07:00
Pierre-Olivier Latour
12b1edb958 #70 Ensure -isRunning works as expected even if GCDWebServerOption_AutomaticallySuspendInBackground is enabled 2014-07-23 07:40:37 -07:00
Pierre-Olivier Latour
7544a6dc4e Update 2014-07-12 17:11:26 -07:00
Pierre-Olivier Latour
f9621c8aac Bumped version 2014-07-12 16:55:31 -07:00
Pierre-Olivier Latour
7a93b27478 Update README.md 2014-07-03 19:54:25 -07:00
Pierre-Olivier Latour
9d48f9ec12 #57 Validate paths passed to GCDWebDAVServer and GCDWebUploader to ensure they are within the upload directory 2014-06-22 15:53:03 -07:00
Pierre-Olivier Latour
7c6e85cf9a Merge pull request #60 from pvblivs/master
Adding instructions for Swift command line tool
2014-06-22 15:36:21 -07:00
pvblivs
24fbd161d8 Update README.md 2014-06-10 08:28:59 +02:00
pvblivs
0ae0d4175a Adding instructions for Swift command line tool 2014-06-06 16:34:26 +02:00
Pierre-Olivier Latour
6d550a02b7 Merge pull request #55 from mstarinteractive/fix-json-content-type
Fix content-types like "application/json; charset=utf-8"
2014-05-16 12:17:19 -07:00
Braden MacDonald
a7f46b762f Fix content-types like "application/json; charset=utf-8" 2014-05-16 11:55:20 -07:00
Pierre-Olivier Latour
d1c7f9a323 Merge pull request #56 from jaanus/customBonjourType
Can specify a custom Bonjour service type for the server.
2014-05-16 08:31:21 -07:00
jaanus
93bfe65211 Removed unneeded API. If custom Bonjour type is needed, startWithOptions should be used to specify an options dictionary with the custom type. 2014-05-16 17:12:52 +02:00
jaanus
8ab53f74d5 Can specify a custom Bonjour service type for the server. 2014-05-16 11:20:28 +02:00
Pierre-Olivier Latour
04a69787bf Update README.md 2014-05-05 13:43:38 -07:00
Pierre-Olivier Latour
dfd019de7d Update README.md 2014-05-05 13:18:04 -07:00
Pierre-Olivier Latour
4db631fa27 Fixed errno being corrupted by LOG_ERROR() 2014-05-02 22:17:25 -07:00
Pierre-Olivier Latour
ba03d756c6 Merge pull request #52 from tipbit/fix-empty-query-param
Fix GCDWebServerParseURLEncodedForm to allow empty values.
2014-04-30 17:22:56 -07:00
Ewan Mellor
04f59a9214 Fix GCDWebServerParseURLEncodedForm to allow empty values.
If the input string is something like foo=&bar= then both foo and bar
should be in the result, with the empty string as their corresponding value.

The [scanner scanUpToString:@"&" ...] call was returning failure, because
it was already positioned at the &.  In this situation, just set value to the
empty string.
2014-04-30 17:08:42 -07:00
Pierre-Olivier Latour
40ea252ad6 Ensure connected state is updated immediately after calling -stop 2014-04-30 14:04:46 -07:00
Pierre-Olivier Latour
c193860468 Fix 2014-04-30 13:59:26 -07:00
Pierre-Olivier Latour
94ad8c745e No need to call -stop from -dealloc 2014-04-30 13:36:06 -07:00
Pierre-Olivier Latour
56c096996f Fix 2014-04-30 13:06:02 -07:00
Pierre-Olivier Latour
8cbaf0f867 Added video streaming unit test 2014-04-30 11:31:20 -07:00
Pierre-Olivier Latour
295901c0b3 Reject files greater than 4 GiB in 32 bit mode 2014-04-30 09:57:12 -07:00
Pierre-Olivier Latour
2dda0c98ce #50 Use NSUIntegerMax instead of NSNotFound to indicate undefined length 2014-04-30 09:56:18 -07:00
Pierre-Olivier Latour
70a38c8b01 Fix 2014-04-30 09:15:30 -07:00
Pierre-Olivier Latour
1b12a7bd14 Ensure pending scheduled callbacks have executed in "run" APIs 2014-04-29 22:12:28 -07:00
Pierre-Olivier Latour
75e6332500 Don't delay disconnected state update if already stopped 2014-04-29 22:06:13 -07:00
Pierre-Olivier Latour
420ed719e8 Add SIGTERM support to -runWithOptions:error: 2014-04-29 16:20:03 -07:00
Pierre-Olivier Latour
3b75f9dd20 Added -rewriteRequestURL:withMethod:headers: hook 2014-04-28 14:09:15 -07:00
Pierre-Olivier Latour
f01307b2a7 Bumped version 2014-04-27 19:25:51 -07:00
Pierre-Olivier Latour
1f5e650423 Fix 2014-04-27 19:20:38 -07:00
Pierre-Olivier Latour
d404112a88 #46 Added "error" argument to -startWithOptions: 2014-04-27 19:19:55 -07:00
Pierre-Olivier Latour
dd3f539f74 #47 Ensure listening socket is closed when -stop returns 2014-04-27 19:05:34 -07:00
Pierre-Olivier Latour
0c51d09b69 Update README.md 2014-04-27 18:46:45 -07:00
Pierre-Olivier Latour
0c53c52dd4 Fix 2014-04-27 12:50:36 -07:00
Pierre-Olivier Latour
a687b52563 #39 Added support for "multipart/mixed" parts inside "multipart/form-data" 2014-04-26 19:31:11 -07:00
Pierre-Olivier Latour
c8c34aa61f Fix 2014-04-26 19:30:18 -07:00
Pierre-Olivier Latour
ed709d1476 Added HTMLFileUpload unit tests 2014-04-25 07:17:34 -07:00
Pierre-Olivier Latour
3dc7cb0ec4 Added HTMLForm unit tests 2014-04-25 07:14:40 -07:00
Pierre-Olivier Latour
142f007e58 Added "htmlFileUpload" mode 2014-04-24 19:18:13 -07:00
Pierre-Olivier Latour
c8d2b225ba Modified GCDWebServerMultiPart to allow duplicate control names 2014-04-24 19:01:46 -07:00
Pierre-Olivier Latour
f7d6da55cd Added GCDWebServerMIMEStreamParser class 2014-04-24 18:45:34 -07:00
Pierre-Olivier Latour
5a0c274807 Fix 2014-04-23 10:40:43 -07:00
Pierre-Olivier Latour
591da12aa3 Exclude GCDWebServerPrivate.h from Podspec 2014-04-23 10:39:42 -07:00
Pierre-Olivier Latour
72475429e4 Bumped version 2014-04-23 10:33:50 -07:00
Pierre-Olivier Latour
01da5969e4 Update README.md 2014-04-22 22:47:07 -07:00
Pierre-Olivier Latour
46890a0642 Merge pull request #43 from iosphere/pullrequest/unusedVar
Fixes a static analyzer warning about unused variables for disabled logging
2014-04-21 21:15:35 -03:00
Pierre-Olivier Latour
143ca5b99f Fix 2014-04-20 10:34:45 -03:00
Pierre-Olivier Latour
519866bc03 Fix 2014-04-19 23:05:13 -03:00
Pierre-Olivier Latour
a93cac5ea6 Fix 2014-04-19 22:56:45 -03:00
Pierre-Olivier Latour
43b578677f Fix 2014-04-19 22:19:47 -03:00
Pierre-Olivier Latour
10cbe27e50 Fixed open() calls 2014-04-19 22:17:17 -03:00
Pierre-Olivier Latour
b1169ce7d1 Bumped version 2014-04-19 17:48:53 -03:00
Pierre-Olivier Latour
766072eb89 Renamed GCDWebServerStreamingResponse to GCDWebServerStreamedResponse.h 2014-04-19 17:43:36 -03:00
Pierre-Olivier Latour
0807cf5fe6 #33 Documented Requests/ and Responses/ 2014-04-19 17:39:09 -03:00
Pierre-Olivier Latour
c6701cd474 Renamed "showHiddenFiles" property to "allowHiddenItems" 2014-04-19 16:31:00 -03:00
Pierre-Olivier Latour
b6866bee8e #33 Documented GCDWebDAVServer/ and GCDWebUploader/ 2014-04-19 16:27:49 -03:00
Pierre-Olivier Latour
075774b6c0 Added -webServerDidCompleteBonjourRegistration: 2014-04-19 12:57:01 -03:00
Pierre-Olivier Latour
7743d18006 Added -logException: 2014-04-19 12:52:34 -03:00
Pierre-Olivier Latour
633d40f155 Don't cache GCDWebServerDataResponse 2014-04-19 12:51:36 -03:00
Pierre-Olivier Latour
5d82a80a34 #33 Documented Core/ 2014-04-19 12:51:00 -03:00
Felix Lamouroux
3e5fe3f956 Fixes logging for non-arc builds 2014-04-19 14:09:55 +02:00
Pierre-Olivier Latour
5a26a09d8e Update README.md 2014-04-19 00:17:45 -03:00
Pierre-Olivier Latour
51e8dbe475 #41 Fixed linking issues with Podspec 2014-04-18 18:38:17 -03:00
Pierre-Olivier Latour
4a3d4537a3 Update README.md 2014-04-18 18:25:35 -03:00
Felix Lamouroux
a5208bd60f Fixes a static analyzer warning about unused variables when logging is disabled 2014-04-18 21:26:18 +02:00
Pierre-Olivier Latour
2597d6da00 Bumped version 2014-04-18 12:59:16 -03:00
Pierre-Olivier Latour
1ca5a56952 Allow multiple user accounts for authentication 2014-04-18 12:50:11 -03:00
Pierre-Olivier Latour
80cff01154 Update README.md 2014-04-18 12:33:59 -03:00
Pierre-Olivier Latour
1e17d5c455 #38 Added support for digest authentication 2014-04-18 12:32:55 -03:00
Pierre-Olivier Latour
ce1eb3c29a Fix 2014-04-18 12:31:41 -03:00
Pierre-Olivier Latour
f11647ee7f Fixed unit tests 2014-04-18 10:59:29 -03:00
Pierre-Olivier Latour
60f281fd30 #40 Allow non ISO Latin 1 file names when downloading files 2014-04-18 10:46:50 -03:00
Pierre-Olivier Latour
181984ba6d Update README.md 2014-04-17 23:10:10 -03:00
Pierre-Olivier Latour
4685fae29c Cleaned up authentication options 2014-04-17 23:08:16 -03:00
Pierre-Olivier Latour
8619799e62 Added -preflightRequest: and -overrideResponse:forRequest: APIs 2014-04-17 22:51:55 -03:00
Pierre-Olivier Latour
b1061378fb Fix 2014-04-17 22:16:25 -03:00
Pierre-Olivier Latour
9bc2bfa506 Update README.md 2014-04-17 18:06:15 -03:00
Pierre-Olivier Latour
078c5e7bc3 Bumped version 2014-04-17 11:50:17 -03:00
Pierre-Olivier Latour
3f304b3c14 Fixed parsing of 'multipart/form-data' with non-ASCII headers 2014-04-17 11:50:09 -03:00
Pierre-Olivier Latour
80348079a6 Added support for Basic Authentication 2014-04-17 11:14:17 -03:00
Pierre-Olivier Latour
099b2a03e6 Updated run APIs to use options 2014-04-17 11:12:18 -03:00
Pierre-Olivier Latour
f0c63f4776 Fixes 2014-04-17 10:41:33 -03:00
Pierre-Olivier Latour
4e3aa3bc5c Update README.md 2014-04-17 01:57:30 -03:00
Pierre-Olivier Latour
c5d82b85ed Update README.md 2014-04-17 01:55:15 -03:00
Pierre-Olivier Latour
b1181d2e40 Update README.md 2014-04-17 01:54:21 -03:00
Pierre-Olivier Latour
d931e04d47 Update README.md 2014-04-17 01:52:19 -03:00
Pierre-Olivier Latour
016c4da6d2 Update README.md 2014-04-17 01:50:07 -03:00
Pierre-Olivier Latour
38dd39a789 Update README.md 2014-04-17 01:49:03 -03:00
Pierre-Olivier Latour
748f6a8bc9 Update README.md 2014-04-17 01:48:22 -03:00
Pierre-Olivier Latour
0b3f825f1a Bumped version 2014-04-17 01:04:55 -03:00
Pierre-Olivier Latour
6b9142642e Update README.md 2014-04-17 00:47:34 -03:00
Pierre-Olivier Latour
0a21059d25 Added support for background mode on iOS 2014-04-16 23:49:35 -03:00
Pierre-Olivier Latour
14acb7b323 Fix 2014-04-16 23:30:22 -03:00
Pierre-Olivier Latour
2f7f7b7b50 Fix 2014-04-16 01:50:17 -03:00
Pierre-Olivier Latour
998a47b099 Replaced GCDWebServer subclassing with explicit options 2014-04-16 01:29:52 -03:00
Pierre-Olivier Latour
2d8996b91e Added connected state to GCDWebServer 2014-04-16 00:57:41 -03:00
Pierre-Olivier Latour
c5ca0f7cee Added GCDWebServerDelegate 2014-04-16 00:57:41 -03:00
Pierre-Olivier Latour
05a704bcef Fix 2014-04-16 00:57:41 -03:00
Pierre-Olivier Latour
c170fb4fd3 Use strerror() consistently 2014-04-16 00:57:41 -03:00
Pierre-Olivier Latour
55a9abd506 Fix 2014-04-16 00:57:39 -03:00
Pierre-Olivier Latour
489c8c6236 Added +[GCDWebServer maxPendingConnections] 2014-04-16 00:57:14 -03:00
Pierre-Olivier Latour
9c73736225 Fix 2014-04-15 21:53:02 -03:00
Pierre-Olivier Latour
1f503aa3b6 Fix 2014-04-15 18:02:38 -03:00
Pierre-Olivier Latour
4eeeab7dc2 Build iOS app using iOS Simulator SDK on Travis CI 2014-04-15 17:57:42 -03:00
Pierre-Olivier Latour
813f124f6a Fix 2014-04-15 17:57:15 -03:00
Pierre-Olivier Latour
8fe66444ae Cleaned up connections read / write implementation 2014-04-15 17:46:43 -03:00
Pierre-Olivier Latour
e3efc065df Build iOS app as part of unit tests 2014-04-15 16:50:57 -03:00
Pierre-Olivier Latour
016153f900 Isolated all testing code with __GCDWEBSERVER_ENABLE_TESTING__ 2014-04-15 16:50:57 -03:00
Pierre-Olivier Latour
a55781e2c1 Changed -[GCDWebServerConnection open] to return a BOOL 2014-04-15 16:50:57 -03:00
Pierre-Olivier Latour
4fb5d67e9b Fixed broken iOS build with __GCDWEBSERVER_ENABLE_TESTING__ 2014-04-15 16:50:57 -03:00
Pierre-Olivier Latour
14e04b445f Fixes 2014-04-15 16:50:57 -03:00
Pierre-Olivier Latour
ad01c15dcd Update README.md 2014-04-15 01:28:46 -03:00
Pierre-Olivier Latour
bb0f62416e Update README.md 2014-04-15 01:24:31 -03:00
Pierre-Olivier Latour
14228834d6 Update README.md 2014-04-15 01:09:10 -03:00
Pierre-Olivier Latour
eb16566605 Update README.md 2014-04-15 01:08:19 -03:00
Pierre-Olivier Latour
06e0fc531d Fixes 2014-04-15 01:01:55 -03:00
Pierre-Olivier Latour
ea777865f3 Bumped version 2014-04-15 00:33:17 -03:00
Pierre-Olivier Latour
5842149d4b Fixed unit tests to work in any time zone 2014-04-15 00:29:56 -03:00
Pierre-Olivier Latour
46ee1a48d6 Fixed source folder name typo! 2014-04-15 00:13:31 -03:00
Pierre-Olivier Latour
c1b2b79b06 Update README.md 2014-04-15 00:04:56 -03:00
Pierre-Olivier Latour
41c56334d0 Update README.md 2014-04-15 00:02:42 -03:00
Pierre-Olivier Latour
2810086816 Update README.md 2014-04-14 23:59:07 -03:00
Pierre-Olivier Latour
c8cd771697 Make header parsing more robust 2014-04-14 19:08:08 -03:00
Pierre-Olivier Latour
252c38c42a Force preserve scrolling position 2014-04-14 18:45:31 -03:00
Pierre-Olivier Latour
cedeb88cb6 Bumped version 2014-04-14 18:37:35 -03:00
Pierre-Olivier Latour
854bbdc6b2 Disable table reloads while renaming 2014-04-14 18:34:26 -03:00
Pierre-Olivier Latour
b949187770 Use jquery data() instead of element attributes 2014-04-14 18:28:22 -03:00
Pierre-Olivier Latour
dafcb0d895 Enable Enter key for dialogs 2014-04-14 10:34:43 -03:00
Pierre-Olivier Latour
7bceb8132a Force overwrite of entire GCDWebUploader.bundle on build 2014-04-14 10:34:14 -03:00
Pierre-Olivier Latour
4c2ac12f7b Fix 2014-04-13 19:20:55 -03:00
Pierre-Olivier Latour
1d2efbbbc7 Update README.md 2014-04-12 09:12:48 -07:00
Pierre-Olivier Latour
91b832715a Organized source code in subfolders 2014-04-12 09:11:24 -07:00
Pierre-Olivier Latour
0852bf2d05 Moved functions to GCDWebServerFunctions.[h/m] 2014-04-12 09:07:09 -07:00
Pierre-Olivier Latour
eb29232842 #34 Added Travis CI integration 2014-04-11 23:37:45 -07:00
Pierre-Olivier Latour
3b1fa05046 Update .travis.yml 2014-04-11 23:31:28 -07:00
Pierre-Olivier Latour
4535c5d61a Update Run-Tests.sh 2014-04-11 23:29:40 -07:00
Pierre-Olivier Latour
ccd1eaa880 Create .travis.yml 2014-04-11 23:27:35 -07:00
Pierre-Olivier Latour
1b805c3951 Update GCDWebServerRequest.h 2014-04-11 23:19:09 -07:00
Pierre-Olivier Latour
2172872787 Update README.md 2014-04-11 23:17:57 -07:00
Pierre-Olivier Latour
894eacd517 Bumped version 2014-04-11 22:39:51 -07:00
Pierre-Olivier Latour
7a54bcbae5 #35 Finalized unit tests 2014-04-11 22:39:50 -07:00
Pierre-Olivier Latour
a28ac82ba2 #35 More work on unit tests 2014-04-11 22:39:50 -07:00
Pierre-Olivier Latour
c062d9d6d3 Use internal functions for date formatting in WebDAV 2014-04-11 22:39:50 -07:00
Pierre-Olivier Latour
bb32a721b6 Update README.md 2014-04-11 08:22:42 -07:00
Pierre-Olivier Latour
1b6e4f6491 #35 First pass at unit tests 2014-04-10 20:22:44 -07:00
Pierre-Olivier Latour
7b51023373 Fixed memory corruption under non-ARC 2014-04-10 20:19:37 -07:00
Pierre-Olivier Latour
f21c6ab667 Fix 2014-04-10 19:49:24 -07:00
Pierre-Olivier Latour
0dd6d8c5fc Fix memory leak 2014-04-10 15:28:40 -07:00
Pierre-Olivier Latour
d58b2122ed Improved CocoaPods integration 2014-04-10 14:37:55 -07:00
Pierre-Olivier Latour
4fa91f6802 Don't use SO_REUSEPORT 2014-04-10 14:27:39 -07:00
Pierre-Olivier Latour
dfd37078ae Fix 2014-04-10 14:27:20 -07:00
Pierre-Olivier Latour
582c6da74f More cleaning of logging system 2014-04-10 14:27:07 -07:00
Pierre-Olivier Latour
1e1fd24b5d Optimized logging 2014-04-09 19:15:03 -07:00
Pierre-Olivier Latour
a3996f3fbf Merge branch 'v2' 2014-04-09 14:05:21 -07:00
Pierre-Olivier Latour
2ecbfea72f Fix 2014-04-09 14:02:58 -07:00
Pierre-Olivier Latour
d78aa3baae Allow HEAD requests on collections 2014-04-09 13:59:20 -07:00
Pierre-Olivier Latour
fcea9cad44 Moved +shouldAutomaticallyMapHEADToGET to GCDWebServer class 2014-04-09 13:53:46 -07:00
Pierre-Olivier Latour
d383845fcc Cleaned up file servers error handling 2014-04-09 13:47:26 -07:00
Pierre-Olivier Latour
97929f7d89 Added -description methods 2014-04-09 13:47:26 -07:00
Pierre-Olivier Latour
4008b5b476 Only set "Cache-Control" on successful responses 2014-04-09 13:47:26 -07:00
Pierre-Olivier Latour
e49b9219ea Renamed "filePath" to "temporaryPath" 2014-04-09 13:46:43 -07:00
Pierre-Olivier Latour
30eb01ca6f Fixed memory corruption 2014-04-09 13:38:30 -07:00
Pierre-Olivier Latour
6f90a3e6ce Log real request method 2014-04-09 12:20:37 -07:00
Pierre-Olivier Latour
efad06f506 __unsafe_unretained does not prevent self retain-cycles when not under ARC 2014-04-09 11:10:45 -07:00
Pierre-Olivier Latour
811e45ab26 Properly handle casing of header values 2014-04-09 10:34:33 -07:00
Pierre-Olivier Latour
f14dda522c Update README.md 2014-04-09 01:56:15 -07:00
Pierre-Olivier Latour
e5550bf290 Fix non-ARC build failure 2014-04-09 01:42:52 -07:00
Pierre-Olivier Latour
9f0544b449 Removed Mac Finder mode 2014-04-09 01:41:56 -07:00
Pierre-Olivier Latour
d2c0d6da2b Update README.md 2014-04-09 01:40:16 -07:00
Pierre-Olivier Latour
d5811fe6df Added compatibility with OS X Finder for WebDAV 2014-04-09 01:38:14 -07:00
Pierre-Olivier Latour
b494e40442 Fix 2014-04-09 01:38:14 -07:00
Pierre-Olivier Latour
157b683082 Automatically map HEAD requests to GET ones 2014-04-09 01:38:14 -07:00
Pierre-Olivier Latour
62ee560d51 Added -replaceResponse:forRequest: hook 2014-04-09 01:38:14 -07:00
Pierre-Olivier Latour
bda3d917ca Automatically handle ETag and Last-Modified-Date caching 2014-04-09 01:38:14 -07:00
Pierre-Olivier Latour
6210564bfc Added support for "ETag" and "If-None-Match" headers 2014-04-09 01:38:14 -07:00
Pierre-Olivier Latour
c454dc4e8e Simplified internal checks for requests and responses 2014-04-09 01:38:14 -07:00
Pierre-Olivier Latour
289059c875 Added support for "If-Modified-Since" and "Accept-Encoding" headers 2014-04-09 01:38:14 -07:00
Pierre-Olivier Latour
f1a79ffd11 Added support for "Last-Modified" response header 2014-04-09 01:38:13 -07:00
Pierre-Olivier Latour
7339a7a2a6 Factored out HTTP date parsing and formatting 2014-04-09 01:38:13 -07:00
Pierre-Olivier Latour
1be1966252 Fix 2014-04-09 01:37:05 -07:00
Pierre-Olivier Latour
30756fc8f9 Update README.md 2014-04-08 19:59:41 -07:00
Pierre-Olivier Latour
4c993ebfac Update README.md 2014-04-08 19:51:01 -07:00
Pierre-Olivier Latour
60f9ee975e Fix 2014-04-08 19:47:04 -07:00
Pierre-Olivier Latour
b3a700d38a Ensure Content-Type header is lowercased 2014-04-08 19:47:03 -07:00
Pierre-Olivier Latour
881cc3b00c Added JSON and text extensions to GCDWebServerDataRequest 2014-04-08 19:47:03 -07:00
Pierre-Olivier Latour
e26c9b76ea Updated to "instancetype" type 2014-04-08 19:47:03 -07:00
Pierre-Olivier Latour
3401206279 Fix 2014-04-08 19:47:03 -07:00
Pierre-Olivier Latour
715d985475 Update README.md 2014-04-08 19:47:03 -07:00
Pierre-Olivier Latour
efb1f9f4eb First pass at implementing class 1 WebDAV server 2014-04-08 19:47:03 -07:00
Pierre-Olivier Latour
93c6a9bece More refactoring of GCDWebUploader 2014-04-08 19:47:03 -07:00
Pierre-Olivier Latour
1d381c9fc6 Refactor GCDWebUploader 2014-04-08 19:47:03 -07:00
Pierre-Olivier Latour
31d51cf5c0 Updated GCDWebUploader to latest APIs 2014-04-08 19:47:03 -07:00
Pierre-Olivier Latour
35ce178323 Added GCDWebServerErrorResponse 2014-04-08 19:47:03 -07:00
Pierre-Olivier Latour
5ece52fa1b Fix 2014-04-08 19:47:03 -07:00
Pierre-Olivier Latour
131810229f Improved logging APIs 2014-04-08 19:47:03 -07:00
Pierre-Olivier Latour
b942a9d2b8 Fix 2014-04-08 19:47:02 -07:00
Pierre-Olivier Latour
047a0604bf Make default implementation for GCDWebServerRequest and GCDWebServerResponse ignore bodies 2014-04-08 19:47:02 -07:00
Pierre-Olivier Latour
4b46c95a78 Allow requests with body but no Content-Type header 2014-04-08 19:47:02 -07:00
Pierre-Olivier Latour
794ab5f293 Fixes 2014-04-08 19:47:02 -07:00
Pierre-Olivier Latour
fb08e77c0c Added -hasByteRange API 2014-04-08 19:47:02 -07:00
Pierre-Olivier Latour
c51f9ad7d9 Added GCDWebServerHTTPStatusCodes.h 2014-04-08 19:47:02 -07:00
Pierre-Olivier Latour
7ec8d5247a Added logging APIs 2014-04-08 19:47:02 -07:00
Pierre-Olivier Latour
dcbc0f96c5 Fixed addDefaultHandlerForMethod:requestClass:processBlock: ignoring method 2014-04-08 19:47:02 -07:00
Pierre-Olivier Latour
f61ff832ea Added -abortRequest:withStatusCode: API 2014-04-08 19:47:02 -07:00
Pierre-Olivier Latour
e85a0c9a61 Update README.md 2014-04-08 19:47:02 -07:00
Pierre-Olivier Latour
8f9c03991d Renamed GCDWebServerStreamResponse to GCDWebServerStreamingResponse 2014-04-08 19:47:02 -07:00
Pierre-Olivier Latour
c5d3764913 Moved response body chunked transfer encoding to GCDWebServerConnection 2014-04-08 19:47:02 -07:00
Pierre-Olivier Latour
7af258eb6b #17 Added support for chunked transfer encoding in request bodies 2014-04-08 19:47:02 -07:00
Pierre-Olivier Latour
c213e167b4 Fix 2014-04-08 19:47:02 -07:00
Pierre-Olivier Latour
06630d3245 Added support for gzip body encoding 2014-04-08 19:47:01 -07:00
Pierre-Olivier Latour
63a66ff331 Added GCDWebServerBodyWriter protocol 2014-04-08 19:47:01 -07:00
Pierre-Olivier Latour
1f9a0d38d0 Split class files 2014-04-08 19:47:01 -07:00
Pierre-Olivier Latour
81638ad086 First pass at adding body encoders 2014-04-08 19:47:01 -07:00
Pierre-Olivier Latour
7506f9c9a2 Fix 2014-04-06 10:10:25 -07:00
Pierre-Olivier Latour
1315c0e965 Enable -Wshadow 2014-04-04 10:48:12 -07:00
Pierre-Olivier Latour
2c98a5931d Merge pull request #32 from iosphere/pullrequest/shadowLocalVar
Fixes warnings regarding shadowing local variables
2014-04-04 10:46:16 -07:00
felixLam
9a4ec5b7e8 Fixes warning regarding shadowing local variables 2014-04-04 12:05:32 +02:00
Pierre-Olivier Latour
8bebee94ec Enforce Content-Type and Content-Length consistency on requests 2014-04-03 18:30:28 -07:00
Pierre-Olivier Latour
7cd68b3d96 Fix 2014-04-03 15:27:44 -07:00
Pierre-Olivier Latour
6a4f74c2e4 Added GCDWebServerChunkedResponse 2014-04-03 15:25:34 -07:00
Pierre-Olivier Latour
8116d88ec4 Make contentType and contentLength properties 2014-04-03 15:23:33 -07:00
Pierre-Olivier Latour
467830e4de Fix 2014-04-02 10:12:27 -07:00
Pierre-Olivier Latour
030c28eb93 Fix 2014-04-02 10:09:07 -07:00
Pierre-Olivier Latour
e4702059c8 #30 Add support for moving files 2014-04-02 10:05:38 -07:00
Pierre-Olivier Latour
877813ed0f Fix 2014-04-02 09:44:18 -07:00
Pierre-Olivier Latour
ccfb4f0b54 Use modal panel instead of prompt() when creating folders 2014-04-02 09:41:07 -07:00
Pierre-Olivier Latour
e27fd601a8 Fix 2014-04-02 09:40:43 -07:00
Pierre-Olivier Latour
7c7f561f90 Fixed default footer for CLTs 2014-04-02 09:02:43 -07:00
Pierre-Olivier Latour
689cb5f26a Fix 2014-04-02 08:50:18 -07:00
Pierre-Olivier Latour
1e4c508737 Update README.md 2014-04-02 08:49:51 -07:00
Pierre-Olivier Latour
ef08ab5516 Update README.md 2014-04-02 08:49:11 -07:00
Pierre-Olivier Latour
18ad23f503 Update README.md 2014-04-02 08:47:32 -07:00
Pierre-Olivier Latour
d68687af62 Fix 2014-04-02 08:34:19 -07:00
Pierre-Olivier Latour
0cdf441830 Update README.md 2014-04-01 16:59:43 -07:00
855 changed files with 18336 additions and 2912 deletions

2
.gitignore vendored
View File

@@ -1,3 +1,5 @@
.DS_Store
xcuserdata
project.xcworkspace
Tests/Payload

2
.travis.yml Normal file
View File

@@ -0,0 +1,2 @@
language: objective-c
script: ./Run-Tests.sh

View File

@@ -1,85 +0,0 @@
/*
Copyright (c) 2012-2014, 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 <TargetConditionals.h>
#import "GCDWebServerRequest.h"
#import "GCDWebServerResponse.h"
typedef GCDWebServerRequest* (^GCDWebServerMatchBlock)(NSString* requestMethod, NSURL* requestURL, NSDictionary* requestHeaders, NSString* urlPath, NSDictionary* urlQuery);
typedef GCDWebServerResponse* (^GCDWebServerProcessBlock)(GCDWebServerRequest* request);
#ifdef __cplusplus
extern "C" {
#endif
NSString* GCDWebServerGetMimeTypeForExtension(NSString* extension);
NSString* GCDWebServerEscapeURLString(NSString* string);
NSString* GCDWebServerUnescapeURLString(NSString* string);
NSDictionary* GCDWebServerParseURLEncodedForm(NSString* form);
NSString* GCDWebServerGetPrimaryIPv4Address(); // Returns IPv4 address of primary connected service on OS X or of WiFi interface on iOS if connected
#ifdef __cplusplus
}
#endif
@interface GCDWebServer : NSObject
@property(nonatomic, readonly, getter=isRunning) BOOL running;
@property(nonatomic, readonly) NSUInteger port;
@property(nonatomic, readonly) NSString* bonjourName; // Only non-nil if Bonjour registration is active
- (void)addHandlerWithMatchBlock:(GCDWebServerMatchBlock)matchBlock processBlock:(GCDWebServerProcessBlock)processBlock;
- (void)removeAllHandlers;
- (BOOL)start; // Default is port 8080 (OS X & iOS Simulator) or 80 (iOS) and computer name
- (BOOL)startWithPort:(NSUInteger)port bonjourName:(NSString*)name; // Pass nil name to disable Bonjour or empty string to use computer name
- (void)stop;
@end
@interface GCDWebServer (Subclassing)
+ (Class)connectionClass;
+ (NSString*)serverName; // Default is class name
@end
@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
#if !TARGET_OS_IPHONE
- (BOOL)runWithPort:(NSUInteger)port; // Starts then automatically stops on SIGINT i.e. Ctrl-C (use on main thread only)
#endif
@end
@interface GCDWebServer (Handlers)
- (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
@end
@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
@end

View File

@@ -1,639 +0,0 @@
/*
Copyright (c) 2012-2014, 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 <TargetConditionals.h>
#if TARGET_OS_IPHONE
#import <MobileCoreServices/MobileCoreServices.h>
#else
#import <SystemConfiguration/SystemConfiguration.h>
#endif
#import <netinet/in.h>
#import <ifaddrs.h>
#import <net/if.h>
#import <netdb.h>
#import "GCDWebServerPrivate.h"
#if TARGET_OS_IPHONE && !TARGET_IPHONE_SIMULATOR
#define kDefaultPort 80
#else
#define kDefaultPort 8080
#endif
#define kMaxPendingConnections 16
@interface GCDWebServer () {
@private
NSMutableArray* _handlers;
NSUInteger _port;
dispatch_source_t _source;
CFNetServiceRef _service;
}
@end
@interface GCDWebServerHandler () {
@private
GCDWebServerMatchBlock _matchBlock;
GCDWebServerProcessBlock _processBlock;
}
@end
#if !TARGET_OS_IPHONE
static BOOL _run;
#endif
#ifndef __GCDWEBSERVER_LOGGING_HEADER__
void GCDLogMessage(long level, NSString* format, ...) {
static const char* levelNames[] = {"DEBUG", "VERBOSE", "INFO", "WARNING", "ERROR", "EXCEPTION"};
static long minLevel = -1;
if (minLevel < 0) {
const char* logLevel = getenv("logLevel");
minLevel = logLevel ? atoi(logLevel) : 0;
}
if (level >= minLevel) {
va_list arguments;
va_start(arguments, format);
NSString* message = [[NSString alloc] initWithFormat:format arguments:arguments];
va_end(arguments);
fprintf(stderr, "[%s] %s\n", levelNames[level], [message UTF8String]);
ARC_RELEASE(message);
}
}
#endif
NSString* GCDWebServerGetMimeTypeForExtension(NSString* extension) {
static NSDictionary* _overrides = nil;
if (_overrides == nil) {
_overrides = [[NSDictionary alloc] initWithObjectsAndKeys:
@"text/css", @"css",
nil];
}
NSString* mimeType = nil;
extension = [extension lowercaseString];
if (extension.length) {
mimeType = [_overrides objectForKey:extension];
if (mimeType == nil) {
CFStringRef uti = UTTypeCreatePreferredIdentifierForTag(kUTTagClassFilenameExtension, (ARC_BRIDGE CFStringRef)extension, NULL);
if (uti) {
mimeType = ARC_BRIDGE_RELEASE(UTTypeCopyPreferredTagWithClass(uti, kUTTagClassMIMEType));
CFRelease(uti);
}
}
}
return mimeType ? mimeType : kGCDWebServerDefaultMimeType;
}
NSString* GCDWebServerEscapeURLString(NSString* string) {
return ARC_BRIDGE_RELEASE(CFURLCreateStringByAddingPercentEscapes(kCFAllocatorDefault, (CFStringRef)string, NULL, CFSTR(":@/?&=+"), kCFStringEncodingUTF8));
}
NSString* GCDWebServerUnescapeURLString(NSString* string) {
return ARC_BRIDGE_RELEASE(CFURLCreateStringByReplacingPercentEscapesUsingEncoding(kCFAllocatorDefault, (CFStringRef)string, CFSTR(""), kCFStringEncodingUTF8));
}
NSDictionary* GCDWebServerParseURLEncodedForm(NSString* form) {
NSMutableDictionary* parameters = [NSMutableDictionary dictionary];
NSScanner* scanner = [[NSScanner alloc] initWithString:form];
[scanner setCharactersToBeSkipped:nil];
while (1) {
NSString* key = nil;
if (![scanner scanUpToString:@"=" intoString:&key] || [scanner isAtEnd]) {
break;
}
[scanner setScanLocation:([scanner scanLocation] + 1)];
NSString* value = nil;
if (![scanner scanUpToString:@"&" intoString:&value]) {
break;
}
key = [key stringByReplacingOccurrencesOfString:@"+" withString:@" "];
value = [value stringByReplacingOccurrencesOfString:@"+" withString:@" "];
if (key && value) {
[parameters setObject:GCDWebServerUnescapeURLString(value) forKey:GCDWebServerUnescapeURLString(key)];
} else {
DNOT_REACHED();
}
if ([scanner isAtEnd]) {
break;
}
[scanner setScanLocation:([scanner scanLocation] + 1)];
}
ARC_RELEASE(scanner);
return parameters;
}
NSString* GCDWebServerGetPrimaryIPv4Address() {
NSString* address = nil;
#if TARGET_OS_IPHONE
#if !TARGET_IPHONE_SIMULATOR
const char* primaryInterface = "en0"; // WiFi interface on iOS
#endif
#else
const char* primaryInterface = NULL;
SCDynamicStoreRef store = SCDynamicStoreCreate(kCFAllocatorDefault, CFSTR("GCDWebServer"), NULL, NULL);
if (store) {
CFPropertyListRef info = SCDynamicStoreCopyValue(store, CFSTR("State:/Network/Global/IPv4"));
if (info) {
primaryInterface = [[(ARC_BRIDGE NSDictionary*)info objectForKey:@"PrimaryInterface"] UTF8String];
CFRelease(info);
}
CFRelease(store);
}
if (primaryInterface == NULL) {
primaryInterface = "lo0";
}
#endif
struct ifaddrs* list;
if (getifaddrs(&list) >= 0) {
for (struct ifaddrs* ifap = list; ifap; ifap = ifap->ifa_next) {
#if TARGET_IPHONE_SIMULATOR
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
#else
if (strcmp(ifap->ifa_name, primaryInterface))
#endif
{
continue;
}
if ((ifap->ifa_flags & IFF_UP) && (ifap->ifa_addr->sa_family == AF_INET)) {
char buffer[NI_MAXHOST];
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;
}
}
freeifaddrs(list);
}
return address;
}
#if !TARGET_OS_IPHONE
static void _SignalHandler(int signal) {
_run = NO;
printf("\n");
}
#endif
@implementation GCDWebServerHandler
@synthesize matchBlock=_matchBlock, processBlock=_processBlock;
- (id)initWithMatchBlock:(GCDWebServerMatchBlock)matchBlock processBlock:(GCDWebServerProcessBlock)processBlock {
if ((self = [super init])) {
_matchBlock = [matchBlock copy];
_processBlock = [processBlock copy];
}
return self;
}
- (void)dealloc {
ARC_RELEASE(_matchBlock);
ARC_RELEASE(_processBlock);
ARC_DEALLOC(super);
}
@end
@implementation GCDWebServer
@synthesize handlers=_handlers, port=_port;
+ (void)initialize {
[GCDWebServerConnection class]; // Initialize class immediately to make sure it happens on the main thread
}
- (id)init {
if ((self = [super init])) {
_handlers = [[NSMutableArray alloc] init];
}
return self;
}
- (void)dealloc {
if (_source) {
[self stop];
}
ARC_RELEASE(_handlers);
ARC_DEALLOC(super);
}
- (NSString*)bonjourName {
CFStringRef name = _service ? CFNetServiceGetName(_service) : NULL;
return name && CFStringGetLength(name) ? ARC_BRIDGE_RELEASE(CFStringCreateCopy(kCFAllocatorDefault, name)) : nil;
}
- (void)addHandlerWithMatchBlock:(GCDWebServerMatchBlock)matchBlock processBlock:(GCDWebServerProcessBlock)handlerBlock {
DCHECK(_source == NULL);
GCDWebServerHandler* handler = [[GCDWebServerHandler alloc] initWithMatchBlock:matchBlock processBlock:handlerBlock];
[_handlers insertObject:handler atIndex:0];
ARC_RELEASE(handler);
}
- (void)removeAllHandlers {
DCHECK(_source == NULL);
[_handlers removeAllObjects];
}
- (BOOL)start {
return [self startWithPort:kDefaultPort bonjourName:@""];
}
static void _NetServiceClientCallBack(CFNetServiceRef service, CFStreamError* error, void* info) {
@autoreleasepool {
if (error->error) {
LOG_ERROR(@"Bonjour error %i (domain %i)", (int)error->error, (int)error->domain);
} else {
GCDWebServer* server = (ARC_BRIDGE GCDWebServer*)info;
LOG_VERBOSE(@"%@ now reachable at %@", [server class], server.bonjourServerURL);
}
}
}
- (BOOL)startWithPort:(NSUInteger)port bonjourName:(NSString*)name {
DCHECK(_source == NULL);
int listeningSocket = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP);
if (listeningSocket > 0) {
int yes = 1;
setsockopt(listeningSocket, SOL_SOCKET, SO_REUSEADDR, &yes, sizeof(yes));
setsockopt(listeningSocket, SOL_SOCKET, SO_REUSEPORT, &yes, sizeof(yes));
struct sockaddr_in addr4;
bzero(&addr4, sizeof(addr4));
addr4.sin_len = sizeof(addr4);
addr4.sin_family = AF_INET;
addr4.sin_port = htons(port);
addr4.sin_addr.s_addr = htonl(INADDR_ANY);
if (bind(listeningSocket, (void*)&addr4, sizeof(addr4)) == 0) {
if (listen(listeningSocket, kMaxPendingConnections) == 0) {
_source = dispatch_source_create(DISPATCH_SOURCE_TYPE_READ, listeningSocket, 0, kGCDWebServerGCDQueue);
dispatch_source_set_cancel_handler(_source, ^{
@autoreleasepool {
int result = close(listeningSocket);
if (result != 0) {
LOG_ERROR(@"Failed closing socket (%i): %s", errno, strerror(errno));
} else {
LOG_DEBUG(@"Closed listening socket");
}
}
});
dispatch_source_set_event_handler(_source, ^{
@autoreleasepool {
struct sockaddr remoteSockAddr;
socklen_t remoteAddrLen = sizeof(remoteSockAddr);
int socket = accept(listeningSocket, &remoteSockAddr, &remoteAddrLen);
if (socket > 0) {
NSData* remoteAddress = [NSData dataWithBytes:&remoteSockAddr length:remoteAddrLen];
struct sockaddr localSockAddr;
socklen_t localAddrLen = sizeof(localSockAddr);
NSData* localAddress = nil;
if (getsockname(socket, &localSockAddr, &localAddrLen) == 0) {
localAddress = [NSData dataWithBytes:&localSockAddr length:localAddrLen];
} else {
DNOT_REACHED();
}
int yes = 1;
setsockopt(socket, SOL_SOCKET, SO_NOSIGPIPE, &yes, sizeof(yes)); // Make sure this socket cannot generate SIG_PIPE
Class connectionClass = [[self class] connectionClass];
GCDWebServerConnection* connection = [[connectionClass alloc] initWithServer:self localAddress:localAddress remoteAddress:remoteAddress socket:socket]; // Connection will automatically retain itself while opened
#if __has_feature(objc_arc)
[connection self]; // Prevent compiler from complaining about unused variable / useless statement
#else
[connection release];
#endif
} else {
LOG_ERROR(@"Failed accepting socket (%i): %s", errno, strerror(errno));
}
}
});
if (port == 0) { // Determine the actual port we are listening on
struct sockaddr addr;
socklen_t addrlen = sizeof(addr);
if (getsockname(listeningSocket, &addr, &addrlen) == 0) {
struct sockaddr_in* sockaddr = (struct sockaddr_in*)&addr;
_port = ntohs(sockaddr->sin_port);
} else {
LOG_ERROR(@"Failed retrieving socket address (%i): %s", errno, strerror(errno));
}
} else {
_port = port;
}
if (name) {
_service = CFNetServiceCreate(kCFAllocatorDefault, CFSTR("local."), CFSTR("_http._tcp"), (ARC_BRIDGE CFStringRef)name, (SInt32)_port);
if (_service) {
CFNetServiceClientContext context = {0, (ARC_BRIDGE void*)self, NULL, NULL, NULL};
CFNetServiceSetClient(_service, _NetServiceClientCallBack, &context);
CFNetServiceScheduleWithRunLoop(_service, CFRunLoopGetMain(), kCFRunLoopCommonModes);
CFStreamError error = {0};
CFNetServiceRegisterWithOptions(_service, 0, &error);
} else {
LOG_ERROR(@"Failed creating CFNetService");
}
}
dispatch_resume(_source);
LOG_VERBOSE(@"%@ started on port %i and reachable at %@", [self class], (int)_port, self.serverURL);
} else {
LOG_ERROR(@"Failed listening on socket (%i): %s", errno, strerror(errno));
close(listeningSocket);
}
} else {
LOG_ERROR(@"Failed binding socket (%i): %s", errno, strerror(errno));
close(listeningSocket);
}
} else {
LOG_ERROR(@"Failed creating socket (%i): %s", errno, strerror(errno));
}
return (_source ? YES : NO);
}
- (BOOL)isRunning {
return (_source ? YES : NO);
}
- (void)stop {
DCHECK(_source != NULL);
if (_source) {
if (_service) {
CFNetServiceUnscheduleFromRunLoop(_service, CFRunLoopGetMain(), kCFRunLoopCommonModes);
CFNetServiceSetClient(_service, NULL, NULL);
CFRelease(_service);
_service = NULL;
}
dispatch_source_cancel(_source); // This will close the socket
ARC_DISPATCH_RELEASE(_source);
_source = NULL;
LOG_VERBOSE(@"%@ stopped", [self class]);
}
_port = 0;
}
@end
@implementation GCDWebServer (Subclassing)
+ (Class)connectionClass {
return [GCDWebServerConnection class];
}
+ (NSString*)serverName {
return NSStringFromClass(self);
}
@end
@implementation GCDWebServer (Extensions)
- (NSURL*)serverURL {
if (_source) {
NSString* ipAddress = GCDWebServerGetPrimaryIPv4Address();
if (ipAddress) {
if (_port != 80) {
return [NSURL URLWithString:[NSString stringWithFormat:@"http://%@:%i/", ipAddress, (int)_port]];
} else {
return [NSURL URLWithString:[NSString stringWithFormat:@"http://%@/", ipAddress]];
}
}
}
return nil;
}
- (NSURL*)bonjourServerURL {
if (_source && _service) {
CFStringRef name = CFNetServiceGetName(_service);
if (name && CFStringGetLength(name)) {
if (_port != 80) {
return [NSURL URLWithString:[NSString stringWithFormat:@"http://%@.local:%i/", name, (int)_port]];
} else {
return [NSURL URLWithString:[NSString stringWithFormat:@"http://%@.local/", name]];
}
}
}
return nil;
}
#if !TARGET_OS_IPHONE
- (BOOL)runWithPort:(NSUInteger)port {
BOOL success = NO;
_run = YES;
void (*handler)(int) = signal(SIGINT, _SignalHandler);
if (handler != SIG_ERR) {
if ([self startWithPort:port bonjourName:@""]) {
while (_run) {
CFRunLoopRunInMode(kCFRunLoopDefaultMode, 1.0, true);
}
[self stop];
success = YES;
}
signal(SIGINT, handler);
}
return success;
}
#endif
@end
@implementation GCDWebServer (Handlers)
- (void)addDefaultHandlerForMethod:(NSString*)method requestClass:(Class)aClass processBlock:(GCDWebServerProcessBlock)block {
[self addHandlerWithMatchBlock:^GCDWebServerRequest *(NSString* requestMethod, NSURL* requestURL, NSDictionary* requestHeaders, NSString* urlPath, NSDictionary* urlQuery) {
return ARC_AUTORELEASE([[aClass alloc] initWithMethod:requestMethod url:requestURL headers:requestHeaders path:urlPath query:urlQuery]);
} processBlock:block];
}
- (void)addHandlerForMethod:(NSString*)method path:(NSString*)path requestClass:(Class)aClass processBlock:(GCDWebServerProcessBlock)block {
if ([path hasPrefix:@"/"] && [aClass isSubclassOfClass:[GCDWebServerRequest class]]) {
[self addHandlerWithMatchBlock:^GCDWebServerRequest *(NSString* requestMethod, NSURL* requestURL, NSDictionary* requestHeaders, NSString* urlPath, NSDictionary* urlQuery) {
if (![requestMethod isEqualToString:method]) {
return nil;
}
if ([urlPath caseInsensitiveCompare:path] != NSOrderedSame) {
return nil;
}
return ARC_AUTORELEASE([[aClass alloc] initWithMethod:requestMethod url:requestURL headers:requestHeaders path:urlPath query:urlQuery]);
} processBlock:block];
} else {
DNOT_REACHED();
}
}
- (void)addHandlerForMethod:(NSString*)method pathRegex:(NSString*)regex requestClass:(Class)aClass processBlock:(GCDWebServerProcessBlock)block {
NSRegularExpression* expression = [NSRegularExpression regularExpressionWithPattern:regex options:NSRegularExpressionCaseInsensitive error:NULL];
if (expression && [aClass isSubclassOfClass:[GCDWebServerRequest class]]) {
[self addHandlerWithMatchBlock:^GCDWebServerRequest *(NSString* requestMethod, NSURL* requestURL, NSDictionary* requestHeaders, NSString* urlPath, NSDictionary* urlQuery) {
if (![requestMethod isEqualToString:method]) {
return nil;
}
if ([expression firstMatchInString:urlPath options:0 range:NSMakeRange(0, urlPath.length)] == nil) {
return nil;
}
return ARC_AUTORELEASE([[aClass alloc] initWithMethod:requestMethod url:requestURL headers:requestHeaders path:urlPath query:urlQuery]);
} processBlock:block];
} else {
DNOT_REACHED();
}
}
@end
@implementation GCDWebServer (GETHandlers)
- (void)addGETHandlerForPath:(NSString*)path staticData:(NSData*)staticData contentType:(NSString*)contentType cacheAge:(NSUInteger)cacheAge {
GCDWebServerResponse* response = [GCDWebServerDataResponse responseWithData:staticData contentType:contentType];
response.cacheControlMaxAge = cacheAge;
[self addHandlerForMethod:@"GET" path:path requestClass:[GCDWebServerRequest class] processBlock:^GCDWebServerResponse *(GCDWebServerRequest* request) {
return response;
}];
}
- (void)addGETHandlerForPath:(NSString*)path filePath:(NSString*)filePath isAttachment:(BOOL)isAttachment cacheAge:(NSUInteger)cacheAge allowRangeRequests:(BOOL)allowRangeRequests {
[self addHandlerForMethod:@"GET" path:path requestClass:[GCDWebServerRequest class] processBlock:^GCDWebServerResponse *(GCDWebServerRequest* request) {
GCDWebServerResponse* response = nil;
if (allowRangeRequests) {
response = [GCDWebServerFileResponse responseWithFile:filePath byteRange:request.byteRange isAttachment:isAttachment];
[response setValue:@"bytes" forAdditionalHeader:@"Accept-Ranges"];
} else {
response = [GCDWebServerFileResponse responseWithFile:filePath isAttachment:isAttachment];
}
response.cacheControlMaxAge = cacheAge;
return response;
}];
}
- (GCDWebServerResponse*)_responseWithContentsOfDirectory:(NSString*)path {
NSDirectoryEnumerator* enumerator = [[NSFileManager defaultManager] enumeratorAtPath:path];
if (enumerator == nil) {
return nil;
}
NSMutableString* html = [NSMutableString string];
[html appendString:@"<!DOCTYPE html>\n"];
[html appendString:@"<html><head><meta charset=\"utf-8\"></head><body>\n"];
[html appendString:@"<ul>\n"];
for (NSString* file in enumerator) {
if (![file hasPrefix:@"."]) {
NSString* type = [[enumerator fileAttributes] objectForKey:NSFileType];
NSString* escapedFile = [file stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding];
DCHECK(escapedFile);
if ([type isEqualToString:NSFileTypeRegular]) {
[html appendFormat:@"<li><a href=\"%@\">%@</a></li>\n", escapedFile, file];
} else if ([type isEqualToString:NSFileTypeDirectory]) {
[html appendFormat:@"<li><a href=\"%@/\">%@/</a></li>\n", escapedFile, file];
}
}
[enumerator skipDescendents];
}
[html appendString:@"</ul>\n"];
[html appendString:@"</body></html>\n"];
return [GCDWebServerDataResponse responseWithHTML:html];
}
- (void)addGETHandlerForBasePath:(NSString*)basePath directoryPath:(NSString*)directoryPath indexFilename:(NSString*)indexFilename cacheAge:(NSUInteger)cacheAge allowRangeRequests:(BOOL)allowRangeRequests {
if ([basePath hasPrefix:@"/"] && [basePath hasSuffix:@"/"]) {
#if __has_feature(objc_arc)
__unsafe_unretained GCDWebServer* server = self;
#else
__block GCDWebServer* server = self;
#endif
[self addHandlerWithMatchBlock:^GCDWebServerRequest *(NSString* requestMethod, NSURL* requestURL, NSDictionary* requestHeaders, NSString* urlPath, NSDictionary* urlQuery) {
if (![requestMethod isEqualToString:@"GET"]) {
return nil;
}
if (![urlPath hasPrefix:basePath]) {
return nil;
}
return ARC_AUTORELEASE([[GCDWebServerRequest alloc] initWithMethod:requestMethod url:requestURL headers:requestHeaders path:urlPath query:urlQuery]);
} processBlock:^GCDWebServerResponse *(GCDWebServerRequest* request) {
GCDWebServerResponse* response = nil;
NSString* filePath = [directoryPath stringByAppendingPathComponent:[request.path substringFromIndex:basePath.length]];
BOOL isDirectory;
if ([[NSFileManager defaultManager] fileExistsAtPath:filePath isDirectory:&isDirectory]) {
if (isDirectory) {
if (indexFilename) {
NSString* indexPath = [filePath stringByAppendingPathComponent:indexFilename];
if ([[NSFileManager defaultManager] fileExistsAtPath:indexPath isDirectory:&isDirectory] && !isDirectory) {
return [GCDWebServerFileResponse responseWithFile:indexPath];
}
}
response = [server _responseWithContentsOfDirectory:filePath];
} else {
if (allowRangeRequests) {
response = [GCDWebServerFileResponse responseWithFile:filePath byteRange:request.byteRange];
[response setValue:@"bytes" forAdditionalHeader:@"Accept-Ranges"];
} else {
response = [GCDWebServerFileResponse responseWithFile:filePath];
}
}
}
if (response) {
response.cacheControlMaxAge = cacheAge;
} else {
response = [GCDWebServerResponse responseWithStatusCode:404];
}
return response;
}];
} else {
DNOT_REACHED();
}
}
@end

View File

@@ -1,555 +0,0 @@
/*
Copyright (c) 2012-2014, 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 <netdb.h>
#import "GCDWebServerPrivate.h"
#define kHeadersReadBuffer 1024
#define kBodyWriteBufferSize (32 * 1024)
typedef void (^ReadBufferCompletionBlock)(dispatch_data_t buffer);
typedef void (^ReadDataCompletionBlock)(NSData* data);
typedef void (^ReadHeadersCompletionBlock)(NSData* extraData);
typedef void (^ReadBodyCompletionBlock)(BOOL success);
typedef void (^WriteBufferCompletionBlock)(BOOL success);
typedef void (^WriteDataCompletionBlock)(BOOL success);
typedef void (^WriteHeadersCompletionBlock)(BOOL success);
typedef void (^WriteBodyCompletionBlock)(BOOL success);
static NSData* _separatorData = nil;
static NSData* _continueData = nil;
static NSDateFormatter* _dateFormatter = nil;
static dispatch_queue_t _formatterQueue = NULL;
@interface GCDWebServerConnection () {
@private
GCDWebServer* _server;
NSData* _localAddress;
NSData* _remoteAddress;
CFSocketNativeHandle _socket;
NSUInteger _bytesRead;
NSUInteger _bytesWritten;
CFHTTPMessageRef _requestMessage;
GCDWebServerRequest* _request;
GCDWebServerHandler* _handler;
CFHTTPMessageRef _responseMessage;
GCDWebServerResponse* _response;
}
@end
@implementation GCDWebServerConnection (Read)
- (void)_readBufferWithLength:(NSUInteger)length completionBlock:(ReadBufferCompletionBlock)block {
dispatch_read(_socket, length, kGCDWebServerGCDQueue, ^(dispatch_data_t buffer, int error) {
@autoreleasepool {
if (error == 0) {
size_t size = dispatch_data_get_size(buffer);
if (size > 0) {
LOG_DEBUG(@"Connection received %zu bytes on socket %i", size, _socket);
_bytesRead += size;
[self didUpdateBytesRead];
block(buffer);
} else {
if (_bytesRead > 0) {
LOG_ERROR(@"No more data available on socket %i", _socket);
} else {
LOG_WARNING(@"No data received from socket %i", _socket);
}
block(NULL);
}
} else {
LOG_ERROR(@"Error while reading from socket %i: %s (%i)", _socket, strerror(error), error);
block(NULL);
}
}
});
}
- (void)_readDataWithCompletionBlock:(ReadDataCompletionBlock)block {
[self _readBufferWithLength:SIZE_T_MAX completionBlock:^(dispatch_data_t buffer) {
if (buffer) {
NSMutableData* data = [[NSMutableData alloc] initWithCapacity:dispatch_data_get_size(buffer)];
dispatch_data_apply(buffer, ^bool(dispatch_data_t region, size_t offset, const void* buffer, size_t size) {
[data appendBytes:buffer length:size];
return true;
});
block(data);
ARC_RELEASE(data);
} else {
block(nil);
}
}];
}
- (void)_readHeadersWithCompletionBlock:(ReadHeadersCompletionBlock)block {
DCHECK(_requestMessage);
[self _readBufferWithLength:SIZE_T_MAX completionBlock:^(dispatch_data_t buffer) {
if (buffer) {
NSMutableData* data = [NSMutableData dataWithCapacity:kHeadersReadBuffer];
dispatch_data_apply(buffer, ^bool(dispatch_data_t region, size_t offset, const void* buffer, size_t size) {
[data appendBytes:buffer length:size];
return true;
});
NSRange range = [data rangeOfData:_separatorData options:0 range:NSMakeRange(0, data.length)];
if (range.location == NSNotFound) {
if (CFHTTPMessageAppendBytes(_requestMessage, data.bytes, data.length)) {
[self _readHeadersWithCompletionBlock:block];
} else {
LOG_ERROR(@"Failed appending request headers data from socket %i", _socket);
block(nil);
}
} else {
NSUInteger length = range.location + range.length;
if (CFHTTPMessageAppendBytes(_requestMessage, data.bytes, length)) {
if (CFHTTPMessageIsHeaderComplete(_requestMessage)) {
block([data subdataWithRange:NSMakeRange(length, data.length - length)]);
} else {
LOG_ERROR(@"Failed parsing request headers from socket %i", _socket);
block(nil);
}
} else {
LOG_ERROR(@"Failed appending request headers data from socket %i", _socket);
block(nil);
}
}
} else {
block(nil);
}
}];
}
- (void)_readBodyWithRemainingLength:(NSUInteger)length completionBlock:(ReadBodyCompletionBlock)block {
DCHECK([_request hasBody]);
[self _readBufferWithLength:length completionBlock:^(dispatch_data_t buffer) {
if (buffer) {
NSInteger remainingLength = length - dispatch_data_get_size(buffer);
if (remainingLength >= 0) {
bool success = dispatch_data_apply(buffer, ^bool(dispatch_data_t region, size_t offset, const void* buffer, size_t size) {
NSInteger result = [_request write:buffer maxLength:size];
if (result != (NSInteger)size) {
LOG_ERROR(@"Failed writing request body on socket %i (error %i)", _socket, (int)result);
return false;
}
return true;
});
if (success) {
if (remainingLength > 0) {
[self _readBodyWithRemainingLength:remainingLength completionBlock:block];
} else {
block(YES);
}
} else {
block(NO);
}
} else {
block(NO);
DNOT_REACHED();
}
} else {
block(NO);
}
}];
}
@end
@implementation GCDWebServerConnection (Write)
- (void)_writeBuffer:(dispatch_data_t)buffer withCompletionBlock:(WriteBufferCompletionBlock)block {
size_t size = dispatch_data_get_size(buffer);
dispatch_write(_socket, buffer, kGCDWebServerGCDQueue, ^(dispatch_data_t data, int error) {
@autoreleasepool {
if (error == 0) {
DCHECK(data == NULL);
LOG_DEBUG(@"Connection sent %zu bytes on socket %i", size, _socket);
_bytesWritten += size;
[self didUpdateBytesWritten];
block(YES);
} else {
LOG_ERROR(@"Error while writing to socket %i: %s (%i)", _socket, strerror(error), error);
block(NO);
}
}
});
}
- (void)_writeData:(NSData*)data withCompletionBlock:(WriteDataCompletionBlock)block {
#if !__has_feature(objc_arc)
[data retain];
#endif
dispatch_data_t buffer = dispatch_data_create(data.bytes, data.length, dispatch_get_main_queue(), ^{
#if __has_feature(objc_arc)
[data self]; // Keeps ARC from releasing data too early
#else
[data release];
#endif
});
[self _writeBuffer:buffer withCompletionBlock:block];
ARC_DISPATCH_RELEASE(buffer);
}
- (void)_writeHeadersWithCompletionBlock:(WriteHeadersCompletionBlock)block {
DCHECK(_responseMessage);
CFDataRef message = CFHTTPMessageCopySerializedMessage(_responseMessage);
[self _writeData:(ARC_BRIDGE NSData*)message withCompletionBlock:block];
CFRelease(message);
}
- (void)_writeBodyWithCompletionBlock:(WriteBodyCompletionBlock)block {
DCHECK([_response hasBody]);
void* buffer = malloc(kBodyWriteBufferSize);
NSInteger result = [_response read:buffer maxLength:kBodyWriteBufferSize];
if (result > 0) {
dispatch_data_t wrapper = dispatch_data_create(buffer, result, NULL, DISPATCH_DATA_DESTRUCTOR_FREE);
[self _writeBuffer:wrapper withCompletionBlock:^(BOOL success) {
if (success) {
[self _writeBodyWithCompletionBlock:block];
} else {
block(NO);
}
}];
ARC_DISPATCH_RELEASE(wrapper);
} else if (result < 0) {
LOG_ERROR(@"Failed reading response body on socket %i (error %i)", _socket, (int)result);
block(NO);
free(buffer);
} else {
block(YES);
free(buffer);
}
}
@end
@implementation GCDWebServerConnection
@synthesize server=_server, localAddressData=_localAddress, remoteAddressData=_remoteAddress, totalBytesRead=_bytesRead, totalBytesWritten=_bytesWritten;
+ (void)initialize {
if (_separatorData == nil) {
_separatorData = [[NSData alloc] initWithBytes:"\r\n\r\n" length:4];
DCHECK(_separatorData);
}
if (_continueData == nil) {
CFHTTPMessageRef message = CFHTTPMessageCreateResponse(kCFAllocatorDefault, 100, NULL, kCFHTTPVersion1_1);
#if __has_feature(objc_arc)
_continueData = CFBridgingRelease(CFHTTPMessageCopySerializedMessage(message));
#else
_continueData = (NSData*)CFHTTPMessageCopySerializedMessage(message);
#endif
CFRelease(message);
DCHECK(_continueData);
}
if (_dateFormatter == nil) {
DCHECK([NSThread isMainThread]); // NSDateFormatter should be initialized on main thread
_dateFormatter = [[NSDateFormatter alloc] init];
_dateFormatter.timeZone = [NSTimeZone timeZoneWithAbbreviation:@"GMT"];
_dateFormatter.dateFormat = @"EEE',' dd MMM yyyy HH':'mm':'ss 'GMT'";
_dateFormatter.locale = ARC_AUTORELEASE([[NSLocale alloc] initWithLocaleIdentifier:@"en_US"]);
DCHECK(_dateFormatter);
}
if (_formatterQueue == NULL) {
_formatterQueue = dispatch_queue_create(NULL, DISPATCH_QUEUE_SERIAL);
DCHECK(_formatterQueue);
}
}
- (void)_initializeResponseHeadersWithStatusCode:(NSInteger)statusCode {
_responseMessage = CFHTTPMessageCreateResponse(kCFAllocatorDefault, statusCode, NULL, kCFHTTPVersion1_1);
CFHTTPMessageSetHeaderFieldValue(_responseMessage, CFSTR("Connection"), CFSTR("Close"));
CFHTTPMessageSetHeaderFieldValue(_responseMessage, CFSTR("Server"), (ARC_BRIDGE CFStringRef)[[_server class] serverName]);
dispatch_sync(_formatterQueue, ^{
NSString* date = [_dateFormatter stringFromDate:[NSDate date]];
CFHTTPMessageSetHeaderFieldValue(_responseMessage, CFSTR("Date"), (ARC_BRIDGE CFStringRef)date);
});
}
- (void)_abortWithStatusCode:(NSUInteger)statusCode {
DCHECK(_responseMessage == NULL);
DCHECK((statusCode >= 400) && (statusCode < 600));
[self _initializeResponseHeadersWithStatusCode:statusCode];
[self _writeHeadersWithCompletionBlock:^(BOOL success) {
; // Nothing more to do
}];
LOG_DEBUG(@"Connection aborted with status code %i on socket %i", (int)statusCode, _socket);
}
// http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html
- (void)_processRequest {
DCHECK(_responseMessage == NULL);
GCDWebServerResponse* response = [self processRequest:_request withBlock:_handler.processBlock];
if (![response hasBody] || [response open]) {
_response = ARC_RETAIN(response);
}
if (_response) {
[self _initializeResponseHeadersWithStatusCode:_response.statusCode];
NSUInteger maxAge = _response.cacheControlMaxAge;
if (maxAge > 0) {
CFHTTPMessageSetHeaderFieldValue(_responseMessage, CFSTR("Cache-Control"), (ARC_BRIDGE CFStringRef)[NSString stringWithFormat:@"max-age=%i, public", (int)maxAge]);
} else {
CFHTTPMessageSetHeaderFieldValue(_responseMessage, CFSTR("Cache-Control"), CFSTR("no-cache"));
}
[_response.additionalHeaders enumerateKeysAndObjectsUsingBlock:^(id key, id obj, BOOL* stop) {
CFHTTPMessageSetHeaderFieldValue(_responseMessage, (ARC_BRIDGE CFStringRef)key, (ARC_BRIDGE CFStringRef)obj);
}];
if ([_response hasBody]) {
CFHTTPMessageSetHeaderFieldValue(_responseMessage, CFSTR("Content-Type"), (ARC_BRIDGE CFStringRef)_response.contentType);
CFHTTPMessageSetHeaderFieldValue(_responseMessage, CFSTR("Content-Length"), (ARC_BRIDGE CFStringRef)[NSString stringWithFormat:@"%i", (int)_response.contentLength]);
}
[self _writeHeadersWithCompletionBlock:^(BOOL success) {
if (success) {
if ([_response hasBody]) {
[self _writeBodyWithCompletionBlock:^(BOOL success) {
[_response close]; // Can't do anything with result anyway
}];
}
} else if ([_response hasBody]) {
[_response close]; // Can't do anything with result anyway
}
}];
} else {
[self _abortWithStatusCode:500];
}
}
- (void)_readRequestBody:(NSData*)initialData {
if ([_request open]) {
NSInteger length = _request.contentLength;
if (initialData.length) {
NSInteger result = [_request write:initialData.bytes maxLength:initialData.length];
if (result == (NSInteger)initialData.length) {
length -= initialData.length;
DCHECK(length >= 0);
} else {
LOG_ERROR(@"Failed writing request body on socket %i (error %i)", _socket, (int)result);
length = -1;
}
}
if (length > 0) {
[self _readBodyWithRemainingLength:length completionBlock:^(BOOL success) {
if (![_request close]) {
success = NO;
}
if (success) {
[self _processRequest];
} else {
[self _abortWithStatusCode:500];
}
}];
} else if (length == 0) {
if ([_request close]) {
[self _processRequest];
} else {
[self _abortWithStatusCode:500];
}
} else {
[_request close]; // Can't do anything with result anyway
[self _abortWithStatusCode:500];
}
} else {
[self _abortWithStatusCode:500];
}
}
- (void)_readRequestHeaders {
_requestMessage = CFHTTPMessageCreateEmpty(kCFAllocatorDefault, true);
[self _readHeadersWithCompletionBlock:^(NSData* extraData) {
if (extraData) {
NSString* requestMethod = [ARC_BRIDGE_RELEASE(CFHTTPMessageCopyRequestMethod(_requestMessage)) uppercaseString];
DCHECK(requestMethod);
NSURL* requestURL = ARC_BRIDGE_RELEASE(CFHTTPMessageCopyRequestURL(_requestMessage));
DCHECK(requestURL);
NSString* requestPath = GCDWebServerUnescapeURLString(ARC_BRIDGE_RELEASE(CFURLCopyPath((CFURLRef)requestURL))); // Don't use -[NSURL path] which strips the ending slash
DCHECK(requestPath);
NSDictionary* requestQuery = nil;
NSString* queryString = ARC_BRIDGE_RELEASE(CFURLCopyQueryString((CFURLRef)requestURL, NULL)); // Don't use -[NSURL query] to make sure query is not unescaped;
if (queryString.length) {
requestQuery = GCDWebServerParseURLEncodedForm(queryString);
DCHECK(requestQuery);
}
NSDictionary* requestHeaders = ARC_BRIDGE_RELEASE(CFHTTPMessageCopyAllHeaderFields(_requestMessage));
DCHECK(requestHeaders);
for (_handler in _server.handlers) {
_request = ARC_RETAIN(_handler.matchBlock(requestMethod, requestURL, requestHeaders, requestPath, requestQuery));
if (_request) {
break;
}
}
if (_request) {
if (_request.hasBody) {
if (extraData.length <= _request.contentLength) {
NSString* expectHeader = ARC_BRIDGE_RELEASE(CFHTTPMessageCopyHeaderFieldValue(_requestMessage, CFSTR("Expect")));
if (expectHeader) {
if ([expectHeader caseInsensitiveCompare:@"100-continue"] == NSOrderedSame) {
[self _writeData:_continueData withCompletionBlock:^(BOOL success) {
if (success) {
[self _readRequestBody:extraData];
}
}];
} else {
LOG_ERROR(@"Unsupported 'Expect' / 'Content-Length' header combination on socket %i", _socket);
[self _abortWithStatusCode:417];
}
} else {
[self _readRequestBody:extraData];
}
} else {
LOG_ERROR(@"Unexpected 'Content-Length' header value on socket %i", _socket);
[self _abortWithStatusCode:400];
}
} else {
[self _processRequest];
}
} else {
[self _abortWithStatusCode:405];
}
} else {
[self _abortWithStatusCode:500];
}
}];
}
- (id)initWithServer:(GCDWebServer*)server localAddress:(NSData*)localAddress remoteAddress:(NSData*)remoteAddress socket:(CFSocketNativeHandle)socket {
if ((self = [super init])) {
_server = ARC_RETAIN(server);
_localAddress = ARC_RETAIN(localAddress);
_remoteAddress = ARC_RETAIN(remoteAddress);
_socket = socket;
[self open];
}
return self;
}
static NSString* _StringFromAddressData(NSData* data) {
NSString* string = nil;
const struct sockaddr* addr = data.bytes;
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) {
string = [NSString stringWithFormat:@"%s:%s", hostBuffer, serviceBuffer];
} else {
DNOT_REACHED();
}
return string;
}
- (NSString*)localAddressString {
return _StringFromAddressData(_localAddress);
}
- (NSString*)remoteAddressString {
return _StringFromAddressData(_remoteAddress);
}
- (void)dealloc {
[self close];
ARC_RELEASE(_server);
ARC_RELEASE(_localAddress);
ARC_RELEASE(_remoteAddress);
if (_requestMessage) {
CFRelease(_requestMessage);
}
ARC_RELEASE(_request);
if (_responseMessage) {
CFRelease(_responseMessage);
}
ARC_RELEASE(_response);
ARC_DEALLOC(super);
}
@end
@implementation GCDWebServerConnection (Subclassing)
- (void)open {
LOG_DEBUG(@"Did open connection on socket %i", _socket);
[self _readRequestHeaders];
}
- (void)didUpdateBytesRead {
;
}
- (void)didUpdateBytesWritten {
;
}
- (GCDWebServerResponse*)processRequest:(GCDWebServerRequest*)request withBlock:(GCDWebServerProcessBlock)block {
LOG_DEBUG(@"Connection on socket %i processing %@ request for \"%@\" (%lu bytes body)", _socket, _request.method, _request.path, (unsigned long)_request.contentLength);
GCDWebServerResponse* response = nil;
@try {
response = block(request);
LOG_VERBOSE(@"%@ | %@ \"%@ %@\" %i %lu", self.localAddressString, self.remoteAddressString, _request.method, _request.path, (int)response.statusCode, (unsigned long)response.contentLength);
}
@catch (NSException* exception) {
LOG_EXCEPTION(exception);
}
return response;
}
- (void)close {
int result = close(_socket);
if (result != 0) {
LOG_ERROR(@"Failed closing socket %i for connection (%i): %s", _socket, errno, strerror(errno));
}
LOG_DEBUG(@"Did close connection on socket %i", _socket);
}
@end

View File

@@ -1,106 +0,0 @@
/*
Copyright (c) 2012-2014, 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 <TargetConditionals.h>
#import <AvailabilityMacros.h>
#if __has_feature(objc_arc)
#define ARC_BRIDGE __bridge
#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_RELEASE(__OBJECT__)
#else
#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_RELEASE(__OBJECT__) dispatch_release(__OBJECT__)
#endif
#import "GCDWebServerConnection.h"
#ifdef __GCDWEBSERVER_LOGGING_HEADER__
// Define __GCDWEBSERVER_LOGGING_HEADER__ as a preprocessor constant to redirect GCDWebServer logging to your own system
#import __GCDWEBSERVER_LOGGING_HEADER__
#else
extern void GCDLogMessage(long level, NSString* format, ...) NS_FORMAT_FUNCTION(2, 3);
#define LOG_VERBOSE(...) GCDLogMessage(1, __VA_ARGS__)
#define LOG_INFO(...) GCDLogMessage(2, __VA_ARGS__)
#define LOG_WARNING(...) GCDLogMessage(3, __VA_ARGS__)
#define LOG_ERROR(...) GCDLogMessage(4, __VA_ARGS__)
#define LOG_EXCEPTION(__EXCEPTION__) GCDLogMessage(5, @"%@", __EXCEPTION__)
#ifdef NDEBUG
#define DCHECK(__CONDITION__)
#define DNOT_REACHED()
#define LOG_DEBUG(...)
#else
#define DCHECK(__CONDITION__) \
do { \
if (!(__CONDITION__)) { \
abort(); \
} \
} while (0)
#define DNOT_REACHED() abort()
#define LOG_DEBUG(...) GCDLogMessage(0, __VA_ARGS__)
#endif
#endif
#define kGCDWebServerDefaultMimeType @"application/octet-stream"
#define kGCDWebServerGCDQueue dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)
@interface GCDWebServerConnection ()
- (id)initWithServer:(GCDWebServer*)server localAddress:(NSData*)localAddress remoteAddress:(NSData*)remoteAddress socket:(CFSocketNativeHandle)socket;
@end
@interface GCDWebServer ()
@property(nonatomic, readonly) NSArray* handlers;
@end
@interface GCDWebServerHandler : NSObject
@property(nonatomic, readonly) GCDWebServerMatchBlock matchBlock;
@property(nonatomic, readonly) GCDWebServerProcessBlock processBlock;
- (id)initWithMatchBlock:(GCDWebServerMatchBlock)matchBlock processBlock:(GCDWebServerProcessBlock)processBlock;
@end

View File

@@ -1,81 +0,0 @@
/*
Copyright (c) 2012-2014, 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 <Foundation/Foundation.h>
@interface GCDWebServerRequest : NSObject
@property(nonatomic, readonly) NSString* method;
@property(nonatomic, readonly) NSURL* URL;
@property(nonatomic, readonly) NSDictionary* headers;
@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)
@property(nonatomic, readonly) NSUInteger contentLength; // Automatically parsed from headers
@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, -bytes] from end)
- (id)initWithMethod:(NSString*)method url:(NSURL*)url headers:(NSDictionary*)headers path:(NSString*)path query:(NSDictionary*)query;
- (BOOL)hasBody; // Convenience method
@end
@interface GCDWebServerRequest (Subclassing)
- (BOOL)open; // Implementation required
- (NSInteger)write:(const void*)buffer maxLength:(NSUInteger)length; // Implementation required
- (BOOL)close; // Implementation required
@end
@interface GCDWebServerDataRequest : GCDWebServerRequest
@property(nonatomic, readonly) NSData* data; // Only valid after open / write / close sequence
@end
@interface GCDWebServerFileRequest : GCDWebServerRequest
@property(nonatomic, readonly) NSString* filePath; // Only valid after open / write / close sequence
@end
@interface GCDWebServerURLEncodedFormRequest : GCDWebServerDataRequest
@property(nonatomic, readonly) NSDictionary* arguments; // Only valid after open / write / close sequence
+ (NSString*)mimeType;
@end
@interface GCDWebServerMultiPart : NSObject
@property(nonatomic, readonly) NSString* contentType; // May be nil
@property(nonatomic, readonly) NSString* mimeType; // Defaults to "text/plain" per specifications if undefined
@end
@interface GCDWebServerMultiPartArgument : GCDWebServerMultiPart
@property(nonatomic, readonly) NSData* data;
@property(nonatomic, readonly) NSString* string; // May be nil (only valid for text mime types)
@end
@interface GCDWebServerMultiPartFile : GCDWebServerMultiPart
@property(nonatomic, readonly) NSString* fileName; // May be nil
@property(nonatomic, readonly) NSString* temporaryPath;
@end
@interface GCDWebServerMultiPartFormRequest : GCDWebServerRequest
@property(nonatomic, readonly) NSDictionary* arguments; // Only valid after open / write / close sequence
@property(nonatomic, readonly) NSDictionary* files; // Only valid after open / write / close sequence
+ (NSString*)mimeType;
@end

View File

@@ -1,617 +0,0 @@
/*
Copyright (c) 2012-2014, 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 "GCDWebServerPrivate.h"
#define kMultiPartBufferSize (256 * 1024)
enum {
kParserState_Undefined = 0,
kParserState_Start,
kParserState_Headers,
kParserState_Content,
kParserState_End
};
@interface GCDWebServerRequest () {
@private
NSString* _method;
NSURL* _url;
NSDictionary* _headers;
NSString* _path;
NSDictionary* _query;
NSString* _type;
NSUInteger _length;
NSRange _range;
}
@end
@interface GCDWebServerDataRequest () {
@private
NSMutableData* _data;
}
@end
@interface GCDWebServerFileRequest () {
@private
NSString* _filePath;
int _file;
}
@end
@interface GCDWebServerURLEncodedFormRequest () {
@private
NSDictionary* _arguments;
}
@end
@interface GCDWebServerMultiPart () {
@private
NSString* _contentType;
NSString* _mimeType;
}
@end
@interface GCDWebServerMultiPartArgument () {
@private
NSData* _data;
NSString* _string;
}
@end
@interface GCDWebServerMultiPartFile () {
@private
NSString* _fileName;
NSString* _temporaryPath;
}
@end
@interface GCDWebServerMultiPartFormRequest () {
@private
NSData* _boundary;
NSUInteger _parserState;
NSMutableData* _parserData;
NSString* _controlName;
NSString* _fileName;
NSString* _contentType;
NSString* _tmpPath;
int _tmpFile;
NSMutableDictionary* _arguments;
NSMutableDictionary* _files;
}
@end
static NSData* _newlineData = nil;
static NSData* _newlinesData = nil;
static NSData* _dashNewlineData = nil;
static NSString* _ExtractHeaderParameter(NSString* header, NSString* attribute) {
NSString* value = nil;
if (header) {
NSScanner* scanner = [[NSScanner alloc] initWithString:header];
NSString* string = [NSString stringWithFormat:@"%@=", attribute];
if ([scanner scanUpToString:string intoString:NULL]) {
[scanner scanString:string intoString:NULL];
if ([scanner scanString:@"\"" intoString:NULL]) {
[scanner scanUpToString:@"\"" intoString:&value];
} else {
[scanner scanUpToCharactersFromSet:[NSCharacterSet whitespaceCharacterSet] intoString:&value];
}
}
ARC_RELEASE(scanner);
}
return value;
}
// http://www.w3schools.com/tags/ref_charactersets.asp
static NSStringEncoding _StringEncodingFromCharset(NSString* charset) {
NSStringEncoding encoding = kCFStringEncodingInvalidId;
if (charset) {
encoding = CFStringConvertEncodingToNSStringEncoding(CFStringConvertIANACharSetNameToEncoding((CFStringRef)charset));
}
return (encoding != kCFStringEncodingInvalidId ? encoding : NSUTF8StringEncoding);
}
@implementation GCDWebServerRequest : NSObject
@synthesize method=_method, URL=_url, headers=_headers, path=_path, query=_query, contentType=_type, contentLength=_length, byteRange=_range;
- (id)initWithMethod:(NSString*)method url:(NSURL*)url headers:(NSDictionary*)headers path:(NSString*)path query:(NSDictionary*)query {
if ((self = [super init])) {
_method = [method copy];
_url = ARC_RETAIN(url);
_headers = ARC_RETAIN(headers);
_path = [path copy];
_query = ARC_RETAIN(query);
_type = ARC_RETAIN([_headers objectForKey:@"Content-Type"]);
NSInteger length = [[_headers objectForKey:@"Content-Length"] integerValue];
if (length < 0) {
DNOT_REACHED();
ARC_RELEASE(self);
return nil;
}
_length = length;
if ((_length > 0) && (_type == nil)) {
_type = [kGCDWebServerDefaultMimeType copy];
}
_range = NSMakeRange(NSNotFound, 0);
NSString* rangeHeader = [[_headers objectForKey:@"Range"] lowercaseString];
if (rangeHeader) {
if ([rangeHeader hasPrefix:@"bytes="]) {
NSArray* components = [[rangeHeader substringFromIndex:6] componentsSeparatedByString:@","];
if (components.count == 1) {
components = [[components firstObject] componentsSeparatedByString:@"-"];
if (components.count == 2) {
NSString* startString = [components objectAtIndex:0];
NSInteger startValue = [startString integerValue];
NSString* endString = [components objectAtIndex:1];
NSInteger endValue = [endString integerValue];
if (startString.length && (startValue >= 0) && endString.length && (endValue >= startValue)) { // The second 500 bytes: "500-999"
_range.location = startValue;
_range.length = endValue - startValue + 1;
} else if (startString.length && (startValue >= 0)) { // The bytes after 9500 bytes: "9500-"
_range.location = startValue;
_range.length = NSUIntegerMax;
} else if (endString.length && (endValue > 0)) { // The final 500 bytes: "-500"
_range.location = NSNotFound;
_range.length = endValue;
}
}
}
}
if ((_range.location == NSNotFound) && (_range.length == 0)) { // Ignore "Range" header if syntactically invalid
LOG_WARNING(@"Failed to parse 'Range' header \"%@\" for url: %@", rangeHeader, url);
}
}
}
return self;
}
- (void)dealloc {
ARC_RELEASE(_method);
ARC_RELEASE(_url);
ARC_RELEASE(_headers);
ARC_RELEASE(_path);
ARC_RELEASE(_query);
ARC_RELEASE(_type);
ARC_DEALLOC(super);
}
- (BOOL)hasBody {
return _type ? YES : NO;
}
@end
@implementation GCDWebServerRequest (Subclassing)
- (BOOL)open {
[self doesNotRecognizeSelector:_cmd];
return NO;
}
- (NSInteger)write:(const void*)buffer maxLength:(NSUInteger)length {
[self doesNotRecognizeSelector:_cmd];
return -1;
}
- (BOOL)close {
[self doesNotRecognizeSelector:_cmd];
return NO;
}
@end
@implementation GCDWebServerDataRequest
@synthesize data=_data;
- (void)dealloc {
DCHECK(_data != nil);
ARC_RELEASE(_data);
ARC_DEALLOC(super);
}
- (BOOL)open {
DCHECK(_data == nil);
_data = [[NSMutableData alloc] initWithCapacity:self.contentLength];
return _data ? YES : NO;
}
- (NSInteger)write:(const void*)buffer maxLength:(NSUInteger)length {
DCHECK(_data != nil);
[_data appendBytes:buffer length:length];
return length;
}
- (BOOL)close {
DCHECK(_data != nil);
return YES;
}
@end
@implementation GCDWebServerFileRequest
@synthesize filePath=_filePath;
- (id)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])) {
_filePath = ARC_RETAIN([NSTemporaryDirectory() stringByAppendingPathComponent:[[NSProcessInfo processInfo] globallyUniqueString]]);
}
return self;
}
- (void)dealloc {
DCHECK(_file < 0);
unlink([_filePath fileSystemRepresentation]);
ARC_RELEASE(_filePath);
ARC_DEALLOC(super);
}
- (BOOL)open {
DCHECK(_file == 0);
_file = open([_filePath fileSystemRepresentation], O_CREAT | O_TRUNC | O_WRONLY, S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH);
return (_file > 0 ? YES : NO);
}
- (NSInteger)write:(const void*)buffer maxLength:(NSUInteger)length {
DCHECK(_file > 0);
return write(_file, buffer, length);
}
- (BOOL)close {
DCHECK(_file > 0);
int result = close(_file);
_file = -1;
return (result == 0 ? YES : NO);
}
@end
@implementation GCDWebServerURLEncodedFormRequest
@synthesize arguments=_arguments;
+ (NSString*)mimeType {
return @"application/x-www-form-urlencoded";
}
- (void)dealloc {
ARC_RELEASE(_arguments);
ARC_DEALLOC(super);
}
- (BOOL)close {
if (![super close]) {
return NO;
}
NSString* charset = _ExtractHeaderParameter(self.contentType, @"charset");
NSString* string = [[NSString alloc] initWithData:self.data encoding:_StringEncodingFromCharset(charset)];
_arguments = ARC_RETAIN(GCDWebServerParseURLEncodedForm(string));
ARC_RELEASE(string);
return (_arguments ? YES : NO);
}
@end
@implementation GCDWebServerMultiPart
@synthesize contentType=_contentType, mimeType=_mimeType;
- (id)initWithContentType:(NSString*)contentType {
if ((self = [super init])) {
_contentType = [contentType copy];
NSArray* components = [_contentType componentsSeparatedByString:@";"];
if (components.count) {
_mimeType = ARC_RETAIN([[components objectAtIndex:0] lowercaseString]);
}
if (_mimeType == nil) {
_mimeType = @"text/plain";
}
}
return self;
}
- (void)dealloc {
ARC_RELEASE(_contentType);
ARC_RELEASE(_mimeType);
ARC_DEALLOC(super);
}
@end
@implementation GCDWebServerMultiPartArgument
@synthesize data=_data, string=_string;
- (id)initWithContentType:(NSString*)contentType data:(NSData*)data {
if ((self = [super initWithContentType:contentType])) {
_data = ARC_RETAIN(data);
if ([self.mimeType hasPrefix:@"text/"]) {
NSString* charset = _ExtractHeaderParameter(self.contentType, @"charset");
_string = [[NSString alloc] initWithData:_data encoding:_StringEncodingFromCharset(charset)];
}
}
return self;
}
- (void)dealloc {
ARC_RELEASE(_data);
ARC_RELEASE(_string);
ARC_DEALLOC(super);
}
- (NSString*)description {
return [NSString stringWithFormat:@"<%@ | '%@' | %i bytes>", [self class], self.mimeType, (int)_data.length];
}
@end
@implementation GCDWebServerMultiPartFile
@synthesize fileName=_fileName, temporaryPath=_temporaryPath;
- (id)initWithContentType:(NSString*)contentType fileName:(NSString*)fileName temporaryPath:(NSString*)temporaryPath {
if ((self = [super initWithContentType:contentType])) {
_fileName = [fileName copy];
_temporaryPath = [temporaryPath copy];
}
return self;
}
- (void)dealloc {
unlink([_temporaryPath fileSystemRepresentation]);
ARC_RELEASE(_fileName);
ARC_RELEASE(_temporaryPath);
ARC_DEALLOC(super);
}
- (NSString*)description {
return [NSString stringWithFormat:@"<%@ | '%@' | '%@>'", [self class], self.mimeType, _fileName];
}
@end
@implementation GCDWebServerMultiPartFormRequest
@synthesize arguments=_arguments, files=_files;
+ (void)initialize {
if (_newlineData == nil) {
_newlineData = [[NSData alloc] initWithBytes:"\r\n" length:2];
DCHECK(_newlineData);
}
if (_newlinesData == nil) {
_newlinesData = [[NSData alloc] initWithBytes:"\r\n\r\n" length:4];
DCHECK(_newlinesData);
}
if (_dashNewlineData == nil) {
_dashNewlineData = [[NSData alloc] initWithBytes:"--\r\n" length:4];
DCHECK(_dashNewlineData);
}
}
+ (NSString*)mimeType {
return @"multipart/form-data";
}
- (id)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])) {
NSString* boundary = _ExtractHeaderParameter(self.contentType, @"boundary");
if (boundary) {
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;
}
- (BOOL)open {
DCHECK(_parserData == nil);
_parserData = [[NSMutableData alloc] initWithCapacity:kMultiPartBufferSize];
_parserState = kParserState_Start;
return YES;
}
// http://www.w3.org/TR/html401/interact/forms.html#h-17.13.4
- (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 = [headers objectForKey:@"Content-Disposition"];
if ([[contentDisposition lowercaseString] hasPrefix:@"form-data;"]) {
controlName = _ExtractHeaderParameter(contentDisposition, @"name");
fileName = _ExtractHeaderParameter(contentDisposition, @"filename");
}
_controlName = [controlName copy];
_fileName = [fileName copy];
_contentType = ARC_RETAIN([headers objectForKey:@"Content-Type"]);
}
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] initWithBytesNoCopy:(void*)dataBytes length:dataLength freeWhenDone:NO];
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;
}
- (NSInteger)write:(const void*)buffer maxLength:(NSUInteger)length {
DCHECK(_parserData != nil);
[_parserData appendBytes:buffer length:length];
return ([self _parseData] ? length : -1);
}
- (BOOL)close {
DCHECK(_parserData != nil);
ARC_RELEASE(_parserData);
_parserData = nil;
ARC_RELEASE(_controlName);
_controlName = nil;
ARC_RELEASE(_fileName);
_fileName = nil;
ARC_RELEASE(_contentType);
_contentType = nil;
if (_tmpFile > 0) {
close(_tmpFile);
unlink([_tmpPath fileSystemRepresentation]);
_tmpFile = 0;
}
ARC_RELEASE(_tmpPath);
_tmpPath = nil;
return (_parserState == kParserState_End ? YES : NO);
}
- (void)dealloc {
DCHECK(_parserData == nil);
ARC_RELEASE(_arguments);
ARC_RELEASE(_files);
ARC_RELEASE(_boundary);
ARC_DEALLOC(super);
}
@end

View File

@@ -1,83 +0,0 @@
/*
Copyright (c) 2012-2014, 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 <Foundation/Foundation.h>
@interface GCDWebServerResponse : NSObject
@property(nonatomic, readonly) NSString* contentType;
@property(nonatomic, readonly) NSUInteger contentLength;
@property(nonatomic) NSInteger statusCode; // Default is 200
@property(nonatomic) NSUInteger cacheControlMaxAge; // Default is 0 seconds i.e. "no-cache"
@property(nonatomic, readonly) NSDictionary* additionalHeaders;
+ (GCDWebServerResponse*) response;
- (id)init;
- (id)initWithContentType:(NSString*)type contentLength:(NSUInteger)length; // Pass nil contentType to indicate empty body
- (void)setValue:(NSString*)value forAdditionalHeader:(NSString*)header;
- (BOOL)hasBody; // Convenience method
@end
@interface GCDWebServerResponse (Subclassing)
- (BOOL)open; // Implementation required
- (NSInteger)read:(void*)buffer maxLength:(NSUInteger)length; // Implementation required
- (BOOL)close; // Implementation required
@end
@interface GCDWebServerResponse (Extensions)
+ (GCDWebServerResponse*)responseWithStatusCode:(NSInteger)statusCode;
+ (GCDWebServerResponse*)responseWithRedirect:(NSURL*)location permanent:(BOOL)permanent;
- (id)initWithStatusCode:(NSInteger)statusCode;
- (id)initWithRedirect:(NSURL*)location permanent:(BOOL)permanent;
@end
@interface GCDWebServerDataResponse : GCDWebServerResponse
+ (GCDWebServerDataResponse*)responseWithData:(NSData*)data contentType:(NSString*)type;
- (id)initWithData:(NSData*)data contentType:(NSString*)type;
@end
@interface GCDWebServerDataResponse (Extensions)
+ (GCDWebServerDataResponse*)responseWithText:(NSString*)text;
+ (GCDWebServerDataResponse*)responseWithHTML:(NSString*)html;
+ (GCDWebServerDataResponse*)responseWithHTMLTemplate:(NSString*)path variables:(NSDictionary*)variables;
+ (GCDWebServerDataResponse*)responseWithJSONObject:(id)object;
+ (GCDWebServerDataResponse*)responseWithJSONObject:(id)object contentType:(NSString*)type;
- (id)initWithText:(NSString*)text; // Encodes using UTF-8
- (id)initWithHTML:(NSString*)html; // Encodes using UTF-8
- (id)initWithHTMLTemplate:(NSString*)path variables:(NSDictionary*)variables; // Simple template system that replaces all occurences of "%variable%" with corresponding value (encodes using UTF-8)
- (id)initWithJSONObject:(id)object;
- (id)initWithJSONObject:(id)object contentType:(NSString*)type;
@end
@interface GCDWebServerFileResponse : GCDWebServerResponse
+ (GCDWebServerFileResponse*)responseWithFile:(NSString*)path;
+ (GCDWebServerFileResponse*)responseWithFile:(NSString*)path isAttachment:(BOOL)attachment;
+ (GCDWebServerFileResponse*)responseWithFile:(NSString*)path byteRange:(NSRange)range;
+ (GCDWebServerFileResponse*)responseWithFile:(NSString*)path byteRange:(NSRange)range isAttachment:(BOOL)attachment;
- (id)initWithFile:(NSString*)path;
- (id)initWithFile:(NSString*)path isAttachment:(BOOL)attachment;
- (id)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, -bytes] from end of file
- (id)initWithFile:(NSString*)path byteRange:(NSRange)range isAttachment:(BOOL)attachment;
@end

View File

@@ -1,381 +0,0 @@
/*
Copyright (c) 2012-2014, 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 <sys/stat.h>
#import "GCDWebServerPrivate.h"
@interface GCDWebServerResponse () {
@private
NSString* _type;
NSUInteger _length;
NSInteger _status;
NSUInteger _maxAge;
NSMutableDictionary* _headers;
}
@end
@interface GCDWebServerDataResponse () {
@private
NSData* _data;
NSInteger _offset;
}
@end
@interface GCDWebServerFileResponse () {
@private
NSString* _path;
NSUInteger _offset;
NSUInteger _size;
int _file;
}
@end
@implementation GCDWebServerResponse
@synthesize contentType=_type, contentLength=_length, statusCode=_status, cacheControlMaxAge=_maxAge, additionalHeaders=_headers;
+ (GCDWebServerResponse*)response {
return ARC_AUTORELEASE([[[self class] alloc] init]);
}
- (id)init {
return [self initWithContentType:nil contentLength:0];
}
- (id)initWithContentType:(NSString*)type contentLength:(NSUInteger)length {
if ((self = [super init])) {
_type = [type copy];
_length = length;
_status = 200;
_maxAge = 0;
_headers = [[NSMutableDictionary alloc] init];
if ((_length > 0) && (_type == nil)) {
_type = [kGCDWebServerDefaultMimeType copy];
}
}
return self;
}
- (void)dealloc {
ARC_RELEASE(_type);
ARC_RELEASE(_headers);
ARC_DEALLOC(super);
}
- (void)setValue:(NSString*)value forAdditionalHeader:(NSString*)header {
[_headers setValue:value forKey:header];
}
- (BOOL)hasBody {
return _type ? YES : NO;
}
@end
@implementation GCDWebServerResponse (Subclassing)
- (BOOL)open {
[self doesNotRecognizeSelector:_cmd];
return NO;
}
- (NSInteger)read:(void*)buffer maxLength:(NSUInteger)length {
[self doesNotRecognizeSelector:_cmd];
return -1;
}
- (BOOL)close {
[self doesNotRecognizeSelector:_cmd];
return NO;
}
@end
@implementation GCDWebServerResponse (Extensions)
+ (GCDWebServerResponse*)responseWithStatusCode:(NSInteger)statusCode {
return ARC_AUTORELEASE([[self alloc] initWithStatusCode:statusCode]);
}
+ (GCDWebServerResponse*)responseWithRedirect:(NSURL*)location permanent:(BOOL)permanent {
return ARC_AUTORELEASE([[self alloc] initWithRedirect:location permanent:permanent]);
}
- (id)initWithStatusCode:(NSInteger)statusCode {
if ((self = [self initWithContentType:nil contentLength:0])) {
self.statusCode = statusCode;
}
return self;
}
- (id)initWithRedirect:(NSURL*)location permanent:(BOOL)permanent {
if ((self = [self initWithContentType:nil contentLength:0])) {
self.statusCode = permanent ? 301 : 307;
[self setValue:[location absoluteString] forAdditionalHeader:@"Location"];
}
return self;
}
@end
@implementation GCDWebServerDataResponse
+ (GCDWebServerDataResponse*)responseWithData:(NSData*)data contentType:(NSString*)type {
return ARC_AUTORELEASE([[[self class] alloc] initWithData:data contentType:type]);
}
- (id)initWithData:(NSData*)data contentType:(NSString*)type {
if (data == nil) {
DNOT_REACHED();
ARC_RELEASE(self);
return nil;
}
if ((self = [super initWithContentType:type contentLength:data.length])) {
_data = ARC_RETAIN(data);
_offset = -1;
}
return self;
}
- (void)dealloc {
DCHECK(_offset < 0);
ARC_RELEASE(_data);
ARC_DEALLOC(super);
}
- (BOOL)open {
DCHECK(_offset < 0);
_offset = 0;
return YES;
}
- (NSInteger)read:(void*)buffer maxLength:(NSUInteger)length {
DCHECK(_offset >= 0);
NSInteger size = 0;
if (_offset < (NSInteger)_data.length) {
size = MIN(_data.length - _offset, length);
bcopy((char*)_data.bytes + _offset, buffer, size);
_offset += size;
}
return size;
}
- (BOOL)close {
DCHECK(_offset >= 0);
_offset = -1;
return YES;
}
@end
@implementation GCDWebServerDataResponse (Extensions)
+ (GCDWebServerDataResponse*)responseWithText:(NSString*)text {
return ARC_AUTORELEASE([[self alloc] initWithText:text]);
}
+ (GCDWebServerDataResponse*)responseWithHTML:(NSString*)html {
return ARC_AUTORELEASE([[self alloc] initWithHTML:html]);
}
+ (GCDWebServerDataResponse*)responseWithHTMLTemplate:(NSString*)path variables:(NSDictionary*)variables {
return ARC_AUTORELEASE([[self alloc] initWithHTMLTemplate:path variables:variables]);
}
+ (GCDWebServerDataResponse*)responseWithJSONObject:(id)object {
return ARC_AUTORELEASE([[self alloc] initWithJSONObject:object]);
}
+ (GCDWebServerDataResponse*)responseWithJSONObject:(id)object contentType:(NSString*)type {
return ARC_AUTORELEASE([[self alloc] initWithJSONObject:object contentType:type]);
}
- (id)initWithText:(NSString*)text {
NSData* data = [text dataUsingEncoding:NSUTF8StringEncoding];
if (data == nil) {
DNOT_REACHED();
ARC_RELEASE(self);
return nil;
}
return [self initWithData:data contentType:@"text/plain; charset=utf-8"];
}
- (id)initWithHTML:(NSString*)html {
NSData* data = [html dataUsingEncoding:NSUTF8StringEncoding];
if (data == nil) {
DNOT_REACHED();
ARC_RELEASE(self);
return nil;
}
return [self initWithData:data contentType:@"text/html; charset=utf-8"];
}
- (id)initWithHTMLTemplate:(NSString*)path variables:(NSDictionary*)variables {
NSMutableString* html = [[NSMutableString alloc] initWithContentsOfFile:path encoding:NSUTF8StringEncoding error:NULL];
[variables enumerateKeysAndObjectsUsingBlock:^(NSString* key, NSString* value, BOOL* stop) {
[html replaceOccurrencesOfString:[NSString stringWithFormat:@"%%%@%%", key] withString:value options:0 range:NSMakeRange(0, html.length)];
}];
id response = [self initWithHTML:html];
ARC_RELEASE(html);
return response;
}
- (id)initWithJSONObject:(id)object {
return [self initWithJSONObject:object contentType:@"application/json"];
}
- (id)initWithJSONObject:(id)object contentType:(NSString*)type {
NSData* data = [NSJSONSerialization dataWithJSONObject:object options:0 error:NULL];
if (data == nil) {
ARC_RELEASE(self);
return nil;
}
return [self initWithData:data contentType:type];
}
@end
@implementation GCDWebServerFileResponse
+ (GCDWebServerFileResponse*)responseWithFile:(NSString*)path {
return ARC_AUTORELEASE([[[self class] alloc] initWithFile:path]);
}
+ (GCDWebServerFileResponse*)responseWithFile:(NSString*)path isAttachment:(BOOL)attachment {
return ARC_AUTORELEASE([[[self class] alloc] initWithFile:path isAttachment:attachment]);
}
+ (GCDWebServerFileResponse*)responseWithFile:(NSString*)path byteRange:(NSRange)range {
return ARC_AUTORELEASE([[[self class] alloc] initWithFile:path byteRange:range]);
}
+ (GCDWebServerFileResponse*)responseWithFile:(NSString*)path byteRange:(NSRange)range isAttachment:(BOOL)attachment {
return ARC_AUTORELEASE([[[self class] alloc] initWithFile:path byteRange:range isAttachment:attachment]);
}
- (id)initWithFile:(NSString*)path {
return [self initWithFile:path byteRange:NSMakeRange(NSNotFound, 0) isAttachment:NO];
}
- (id)initWithFile:(NSString*)path isAttachment:(BOOL)attachment {
return [self initWithFile:path byteRange:NSMakeRange(NSNotFound, 0) isAttachment:attachment];
}
- (id)initWithFile:(NSString*)path byteRange:(NSRange)range {
return [self initWithFile:path byteRange:range isAttachment:NO];
}
- (id)initWithFile:(NSString*)path byteRange:(NSRange)range isAttachment:(BOOL)attachment {
struct stat info;
if (lstat([path fileSystemRepresentation], &info) || !(info.st_mode & S_IFREG)) {
DNOT_REACHED();
ARC_RELEASE(self);
return nil;
}
if ((range.location != NSNotFound) || (range.length > 0)) {
if (range.location != NSNotFound) {
range.location = MIN(range.location, (NSUInteger)info.st_size);
range.length = MIN(range.length, (NSUInteger)info.st_size - range.location);
} else {
range.length = MIN(range.length, (NSUInteger)info.st_size);
range.location = (NSUInteger)info.st_size - range.length;
}
if (range.length == 0) {
ARC_RELEASE(self);
return nil; // TODO: Return 416 status code and "Content-Range: bytes */{file length}" header
}
}
if ((self = [super initWithContentType:GCDWebServerGetMimeTypeForExtension([path pathExtension]) contentLength:(range.location != NSNotFound ? range.length : (NSUInteger)info.st_size)])) {
_path = [path copy];
if (range.location != NSNotFound) {
_offset = range.location;
_size = range.length;
[self setStatusCode:206];
[self setValue:[NSString stringWithFormat:@"bytes %i-%i/%i", (int)range.location, (int)(range.location + range.length - 1), (int)info.st_size] forAdditionalHeader:@"Content-Range"];
LOG_DEBUG(@"Using content bytes range [%i-%i] for file \"%@\"", (int)range.location, (int)(range.location + range.length - 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
NSData* data = [[path lastPathComponent] dataUsingEncoding:NSISOLatin1StringEncoding allowLossyConversion:YES];
NSString* fileName = data ? [[NSString alloc] initWithData:data encoding:NSISOLatin1StringEncoding] : nil;
if (fileName) {
[self setValue:[NSString stringWithFormat:@"attachment; filename=\"%@\"", fileName] forAdditionalHeader:@"Content-Disposition"];
ARC_RELEASE(fileName);
} else {
DNOT_REACHED();
}
}
}
return self;
}
- (void)dealloc {
DCHECK(_file <= 0);
ARC_RELEASE(_path);
ARC_DEALLOC(super);
}
- (BOOL)open {
DCHECK(_file <= 0);
_file = open([_path fileSystemRepresentation], O_NOFOLLOW | O_RDONLY);
if (_file <= 0) {
return NO;
}
if (lseek(_file, _offset, SEEK_SET) != (off_t)_offset) {
close(_file);
_file = 0;
return NO;
}
return YES;
}
- (NSInteger)read:(void*)buffer maxLength:(NSUInteger)length {
DCHECK(_file > 0);
ssize_t result = read(_file, buffer, MIN(length, _size));
if (result > 0) {
_size -= result;
}
return result;
}
- (BOOL)close {
DCHECK(_file > 0);
int result = close(_file);
_file = 0;
return (result == 0 ? YES : NO);
}
@end

View File

@@ -0,0 +1,156 @@
/*
Copyright (c) 2012-2014, 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 "GCDWebServer.h"
@class GCDWebDAVServer;
/**
* Delegate methods for GCDWebDAVServer.
*
* @warning These methods are always called on the main thread in a serialized way.
*/
@protocol GCDWebDAVServerDelegate <GCDWebServerDelegate>
@optional
/**
* This method is called whenever a file has been downloaded.
*/
- (void)davServer:(GCDWebDAVServer*)server didDownloadFileAtPath:(NSString*)path;
/**
* This method is called whenever a file has been uploaded.
*/
- (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;
/**
* This method is called whenever a file or directory has been copied.
*/
- (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;
/**
* This method is called whenever a directory has been created.
*/
- (void)davServer:(GCDWebDAVServer*)server didCreateDirectoryAtPath:(NSString*)path;
@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
/**
* Returns the upload directory as specified when the server was initialized.
*/
@property(nonatomic, readonly) NSString* uploadDirectory;
/**
* Sets the delegate for the server.
*/
@property(nonatomic, assign) 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* 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;
@end
/**
* Hooks to customize the behavior of GCDWebDAVServer.
*
* @warning These methods can be called on any GCD thread.
*/
@interface GCDWebDAVServer (Subclassing)
/**
* This method is called to check if a file upload is allowed to complete.
* 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 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

View File

@@ -0,0 +1,688 @@
/*
Copyright (c) 2012-2014, 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.
*/
#if !__has_feature(objc_arc)
#error GCDWebDAVServer requires ARC
#endif
// WebDAV specifications: http://webdav.org/specs/rfc4918.html
// Requires "HEADER_SEARCH_PATHS = $(SDKROOT)/usr/include/libxml2" in Xcode build settings
#import <libxml/parser.h>
#import "GCDWebDAVServer.h"
#import "GCDWebServerFunctions.h"
#import "GCDWebServerDataRequest.h"
#import "GCDWebServerFileRequest.h"
#import "GCDWebServerDataResponse.h"
#import "GCDWebServerErrorResponse.h"
#import "GCDWebServerFileResponse.h"
#define kXMLParseOptions (XML_PARSE_NONET | XML_PARSE_RECOVER | XML_PARSE_NOBLANKS | XML_PARSE_COMPACT | XML_PARSE_NOWARNING | XML_PARSE_NOERROR)
typedef NS_ENUM(NSInteger, DAVProperties) {
kDAVProperty_ResourceType = (1 << 0),
kDAVProperty_CreationDate = (1 << 1),
kDAVProperty_LastModified = (1 << 2),
kDAVProperty_ContentLength = (1 << 3),
kDAVAllProperties = kDAVProperty_ResourceType | kDAVProperty_CreationDate | kDAVProperty_LastModified | kDAVProperty_ContentLength
};
@interface GCDWebDAVServer () {
@private
NSString* _uploadDirectory;
NSArray* _allowedExtensions;
BOOL _allowHidden;
}
@end
@implementation GCDWebDAVServer (Methods)
// Must match implementation in GCDWebUploader
- (BOOL)_checkSandboxedPath:(NSString*)path {
return [[path stringByStandardizingPath] hasPrefix:_uploadDirectory];
}
- (BOOL)_checkFileExtension:(NSString*)fileName {
if (_allowedExtensions && ![_allowedExtensions containsObject:[[fileName pathExtension] lowercaseString]]) {
return NO;
}
return YES;
}
static inline BOOL _IsMacFinder(GCDWebServerRequest* request) {
NSString* userAgentHeader = [request.headers objectForKey:@"User-Agent"];
return ([userAgentHeader hasPrefix:@"WebDAVFS/"] || [userAgentHeader hasPrefix:@"WebDAVLib/"]); // OS X WebDAV client
}
- (GCDWebServerResponse*)performOPTIONS:(GCDWebServerRequest*)request {
GCDWebServerResponse* response = [GCDWebServerResponse response];
if (_IsMacFinder(request)) {
[response setValue:@"1, 2" forAdditionalHeader:@"DAV"]; // Classes 1 and 2
} else {
[response setValue:@"1" forAdditionalHeader:@"DAV"]; // Class 1
}
return response;
}
- (GCDWebServerResponse*)performGET:(GCDWebServerRequest*)request {
NSString* relativePath = request.path;
NSString* absolutePath = [_uploadDirectory stringByAppendingPathComponent:relativePath];
BOOL isDirectory = NO;
if (![self _checkSandboxedPath:absolutePath] || ![[NSFileManager defaultManager] fileExistsAtPath:absolutePath isDirectory:&isDirectory]) {
return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_NotFound message:@"\"%@\" does not exist", relativePath];
}
NSString* itemName = [absolutePath lastPathComponent];
if (([itemName hasPrefix:@"."] && !_allowHidden) || (!isDirectory && ![self _checkFileExtension: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
if (isDirectory) {
return [GCDWebServerResponse response];
}
if ([self.delegate respondsToSelector:@selector(davServer:didDownloadFileAtPath:)]) {
dispatch_async(dispatch_get_main_queue(), ^{
[self.delegate davServer:self didDownloadFileAtPath:absolutePath];
});
}
return [GCDWebServerFileResponse responseWithFile:absolutePath];
}
- (GCDWebServerResponse*)performPUT:(GCDWebServerFileRequest*)request {
if ([request hasByteRange]) {
return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_BadRequest message:@"Range uploads not supported"];
}
NSString* relativePath = request.path;
NSString* absolutePath = [_uploadDirectory stringByAppendingPathComponent:relativePath];
if (![self _checkSandboxedPath:absolutePath]) {
return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_NotFound message:@"\"%@\" does not exist", relativePath];
}
BOOL isDirectory;
if (![[NSFileManager defaultManager] fileExistsAtPath:[absolutePath stringByDeletingLastPathComponent] isDirectory:&isDirectory] || !isDirectory) {
return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_Conflict message:@"Missing intermediate collection(s) for \"%@\"", relativePath];
}
BOOL existing = [[NSFileManager defaultManager] fileExistsAtPath:absolutePath isDirectory:&isDirectory];
if (existing && isDirectory) {
return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_MethodNotAllowed message:@"PUT not allowed on existing collection \"%@\"", relativePath];
}
NSString* fileName = [absolutePath lastPathComponent];
if (([fileName hasPrefix:@"."] && !_allowHidden) || ![self _checkFileExtension:fileName]) {
return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_Forbidden message:@"Uploading file name \"%@\" is not allowed", fileName];
}
if (![self shouldUploadFileAtPath:absolutePath withTemporaryFile:request.temporaryPath]) {
return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_Forbidden message:@"Uploading file to \"%@\" is not permitted", relativePath];
}
[[NSFileManager defaultManager] removeItemAtPath:absolutePath error:NULL];
NSError* error = nil;
if (![[NSFileManager defaultManager] moveItemAtPath:request.temporaryPath toPath:absolutePath error:&error]) {
return [GCDWebServerErrorResponse responseWithServerError:kGCDWebServerHTTPStatusCode_InternalServerError underlyingError:error message:@"Failed moving uploaded file to \"%@\"", relativePath];
}
if ([self.delegate respondsToSelector:@selector(davServer:didUploadFileAtPath:)]) {
dispatch_async(dispatch_get_main_queue(), ^{
[self.delegate davServer:self didUploadFileAtPath:absolutePath];
});
}
return [GCDWebServerResponse responseWithStatusCode:(existing ? kGCDWebServerHTTPStatusCode_NoContent : kGCDWebServerHTTPStatusCode_Created)];
}
- (GCDWebServerResponse*)performDELETE:(GCDWebServerRequest*)request {
NSString* depthHeader = [request.headers objectForKey:@"Depth"];
if (depthHeader && ![depthHeader isEqualToString:@"infinity"]) {
return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_BadRequest message:@"Unsupported 'Depth' header: %@", depthHeader];
}
NSString* relativePath = request.path;
NSString* absolutePath = [_uploadDirectory stringByAppendingPathComponent:relativePath];
BOOL isDirectory = NO;
if (![self _checkSandboxedPath:absolutePath] || ![[NSFileManager defaultManager] fileExistsAtPath:absolutePath isDirectory:&isDirectory]) {
return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_NotFound message:@"\"%@\" does not exist", relativePath];
}
NSString* itemName = [absolutePath lastPathComponent];
if (([itemName hasPrefix:@"."] && !_allowHidden) || (!isDirectory && ![self _checkFileExtension:itemName])) {
return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_Forbidden message:@"Deleting item name \"%@\" is not allowed", itemName];
}
if (![self shouldDeleteItemAtPath:absolutePath]) {
return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_Forbidden message:@"Deleting \"%@\" is not permitted", relativePath];
}
NSError* error = nil;
if (![[NSFileManager defaultManager] removeItemAtPath:absolutePath error:&error]) {
return [GCDWebServerErrorResponse responseWithServerError:kGCDWebServerHTTPStatusCode_InternalServerError underlyingError:error message:@"Failed deleting \"%@\"", relativePath];
}
if ([self.delegate respondsToSelector:@selector(davServer:didDeleteItemAtPath:)]) {
dispatch_async(dispatch_get_main_queue(), ^{
[self.delegate davServer:self didDeleteItemAtPath:absolutePath];
});
}
return [GCDWebServerResponse responseWithStatusCode:kGCDWebServerHTTPStatusCode_NoContent];
}
- (GCDWebServerResponse*)performMKCOL:(GCDWebServerDataRequest*)request {
if ([request hasBody] && (request.contentLength > 0)) {
return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_UnsupportedMediaType message:@"Unexpected request body for MKCOL method"];
}
NSString* relativePath = request.path;
NSString* absolutePath = [_uploadDirectory stringByAppendingPathComponent:relativePath];
if (![self _checkSandboxedPath:absolutePath]) {
return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_NotFound message:@"\"%@\" does not exist", relativePath];
}
BOOL isDirectory;
if (![[NSFileManager defaultManager] fileExistsAtPath:[absolutePath stringByDeletingLastPathComponent] isDirectory:&isDirectory] || !isDirectory) {
return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_Conflict message:@"Missing intermediate collection(s) for \"%@\"", relativePath];
}
NSString* directoryName = [absolutePath lastPathComponent];
if (!_allowHidden && [directoryName hasPrefix:@"."]) {
return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_Forbidden message:@"Creating directory name \"%@\" is not allowed", directoryName];
}
if (![self shouldCreateDirectoryAtPath:absolutePath]) {
return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_Forbidden message:@"Creating directory \"%@\" is not permitted", relativePath];
}
NSError* error = nil;
if (![[NSFileManager defaultManager] createDirectoryAtPath:absolutePath withIntermediateDirectories:NO attributes:nil error:&error]) {
return [GCDWebServerErrorResponse responseWithServerError:kGCDWebServerHTTPStatusCode_InternalServerError underlyingError:error message:@"Failed creating directory \"%@\"", relativePath];
}
#ifdef __GCDWEBSERVER_ENABLE_TESTING__
NSString* creationDateHeader = [request.headers objectForKey:@"X-GCDWebServer-CreationDate"];
if (creationDateHeader) {
NSDate* date = GCDWebServerParseISO8601(creationDateHeader);
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];
}
}
#endif
if ([self.delegate respondsToSelector:@selector(davServer:didCreateDirectoryAtPath:)]) {
dispatch_async(dispatch_get_main_queue(), ^{
[self.delegate davServer:self didCreateDirectoryAtPath:absolutePath];
});
}
return [GCDWebServerResponse responseWithStatusCode:kGCDWebServerHTTPStatusCode_Created];
}
- (GCDWebServerResponse*)performCOPY:(GCDWebServerRequest*)request isMove:(BOOL)isMove {
if (!isMove) {
NSString* depthHeader = [request.headers objectForKey:@"Depth"]; // TODO: Support "Depth: 0"
if (depthHeader && ![depthHeader isEqualToString:@"infinity"]) {
return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_BadRequest message:@"Unsupported 'Depth' header: %@", depthHeader];
}
}
NSString* srcRelativePath = request.path;
NSString* srcAbsolutePath = [_uploadDirectory stringByAppendingPathComponent:srcRelativePath];
if (![self _checkSandboxedPath:srcAbsolutePath]) {
return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_NotFound message:@"\"%@\" does not exist", srcRelativePath];
}
NSString* dstRelativePath = [request.headers objectForKey:@"Destination"];
NSRange range = [dstRelativePath rangeOfString:[request.headers objectForKey:@"Host"]];
if ((dstRelativePath == nil) || (range.location == NSNotFound)) {
return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_BadRequest message:@"Malformed 'Destination' header: %@", dstRelativePath];
}
dstRelativePath = [[dstRelativePath substringFromIndex:(range.location + range.length)] stringByReplacingPercentEscapesUsingEncoding:NSUTF8StringEncoding];
NSString* dstAbsolutePath = [_uploadDirectory stringByAppendingPathComponent:dstRelativePath];
if (![self _checkSandboxedPath:dstAbsolutePath]) {
return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_NotFound message:@"\"%@\" does not exist", srcRelativePath];
}
BOOL isDirectory;
if (![[NSFileManager defaultManager] fileExistsAtPath:[dstAbsolutePath stringByDeletingLastPathComponent] isDirectory:&isDirectory] || !isDirectory) {
return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_Conflict message:@"Invalid destination \"%@\"", dstRelativePath];
}
NSString* itemName = [dstAbsolutePath lastPathComponent];
if ((!_allowHidden && [itemName hasPrefix:@"."]) || (!isDirectory && ![self _checkFileExtension:itemName])) {
return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_Forbidden message:@"%@ to item name \"%@\" is not allowed", isMove ? @"Moving" : @"Copying", itemName];
}
NSString* overwriteHeader = [request.headers objectForKey:@"Overwrite"];
BOOL existing = [[NSFileManager defaultManager] fileExistsAtPath:dstAbsolutePath];
if (existing && ((isMove && ![overwriteHeader isEqualToString:@"T"]) || (!isMove && [overwriteHeader isEqualToString:@"F"]))) {
return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_PreconditionFailed message:@"Destination \"%@\" already exists", dstRelativePath];
}
if (isMove) {
if (![self shouldMoveItemFromPath:srcAbsolutePath toPath:dstAbsolutePath]) {
return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_Forbidden message:@"Moving \"%@\" to \"%@\" is not permitted", srcRelativePath, dstRelativePath];
}
} else {
if (![self shouldCopyItemFromPath:srcAbsolutePath toPath:dstAbsolutePath]) {
return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_Forbidden message:@"Copying \"%@\" to \"%@\" is not permitted", srcRelativePath, dstRelativePath];
}
}
NSError* error = nil;
if (isMove) {
[[NSFileManager defaultManager] removeItemAtPath:dstAbsolutePath error:NULL];
if (![[NSFileManager defaultManager] moveItemAtPath:srcAbsolutePath toPath:dstAbsolutePath error:&error]) {
return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_Forbidden underlyingError:error message:@"Failed copying \"%@\" to \"%@\"", srcRelativePath, dstRelativePath];
}
} else {
if (![[NSFileManager defaultManager] copyItemAtPath:srcAbsolutePath toPath:dstAbsolutePath error:&error]) {
return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_Forbidden underlyingError:error message:@"Failed copying \"%@\" to \"%@\"", srcRelativePath, dstRelativePath];
}
}
if (isMove) {
if ([self.delegate respondsToSelector:@selector(davServer:didMoveItemFromPath:toPath:)]) {
dispatch_async(dispatch_get_main_queue(), ^{
[self.delegate davServer:self didMoveItemFromPath:srcAbsolutePath toPath:dstAbsolutePath];
});
}
} else {
if ([self.delegate respondsToSelector:@selector(davServer:didCopyItemFromPath:toPath:)]) {
dispatch_async(dispatch_get_main_queue(), ^{
[self.delegate davServer:self didCopyItemFromPath:srcAbsolutePath toPath:dstAbsolutePath];
});
}
}
return [GCDWebServerResponse responseWithStatusCode:(existing ? kGCDWebServerHTTPStatusCode_NoContent : kGCDWebServerHTTPStatusCode_Created)];
}
static inline xmlNodePtr _XMLChildWithName(xmlNodePtr child, const xmlChar* name) {
while (child) {
if ((child->type == XML_ELEMENT_NODE) && !xmlStrcmp(child->name, name)) {
return child;
}
child = child->next;
}
return NULL;
}
- (void)_addPropertyResponseForItem:(NSString*)itemPath resource:(NSString*)resourcePath properties:(DAVProperties)properties xmlString:(NSMutableString*)xmlString {
CFStringRef escapedPath = CFURLCreateStringByAddingPercentEscapes(kCFAllocatorDefault, (__bridge CFStringRef)resourcePath, NULL, CFSTR("<&>?+"), kCFStringEncodingUTF8);
if (escapedPath) {
NSDictionary* attributes = [[NSFileManager defaultManager] attributesOfItemAtPath:itemPath error:NULL];
NSString* type = [attributes objectForKey:NSFileType];
BOOL isFile = [type isEqualToString:NSFileTypeRegular];
BOOL isDirectory = [type isEqualToString:NSFileTypeDirectory];
if ((isFile && [self _checkFileExtension:itemPath]) || isDirectory) {
[xmlString appendString:@"<D:response>"];
[xmlString appendFormat:@"<D:href>%@</D:href>", escapedPath];
[xmlString appendString:@"<D:propstat>"];
[xmlString appendString:@"<D:prop>"];
if (properties & kDAVProperty_ResourceType) {
if (isDirectory) {
[xmlString appendString:@"<D:resourcetype><D:collection/></D:resourcetype>"];
} else {
[xmlString appendString:@"<D:resourcetype/>"];
}
}
if ((properties & kDAVProperty_CreationDate) && [attributes objectForKey:NSFileCreationDate]) {
[xmlString appendFormat:@"<D:creationdate>%@</D:creationdate>", GCDWebServerFormatISO8601([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
[xmlString appendFormat:@"<D:getlastmodified>%@</D:getlastmodified>", GCDWebServerFormatRFC822([attributes fileModificationDate])];
}
if ((properties & kDAVProperty_ContentLength) && !isDirectory && [attributes objectForKey:NSFileSize]) {
[xmlString appendFormat:@"<D:getcontentlength>%llu</D:getcontentlength>", [attributes fileSize]];
}
[xmlString appendString:@"</D:prop>"];
[xmlString appendString:@"<D:status>HTTP/1.1 200 OK</D:status>"];
[xmlString appendString:@"</D:propstat>"];
[xmlString appendString:@"</D:response>\n"];
}
CFRelease(escapedPath);
} else {
[self logError:@"Failed escaping path: %@", itemPath];
}
}
- (GCDWebServerResponse*)performPROPFIND:(GCDWebServerDataRequest*)request {
NSInteger depth;
NSString* depthHeader = [request.headers objectForKey:@"Depth"];
if ([depthHeader isEqualToString:@"0"]) {
depth = 0;
} else if ([depthHeader isEqualToString:@"1"]) {
depth = 1;
} else {
return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_BadRequest message:@"Unsupported 'Depth' header: %@", depthHeader]; // TODO: Return 403 / propfind-finite-depth for "infinity" depth
}
DAVProperties properties = 0;
if (request.data.length) {
BOOL success = YES;
xmlDocPtr document = xmlReadMemory(request.data.bytes, (int)request.data.length, NULL, NULL, kXMLParseOptions);
if (document) {
xmlNodePtr rootNode = _XMLChildWithName(document->children, (const xmlChar*)"propfind");
xmlNodePtr allNode = rootNode ? _XMLChildWithName(rootNode->children, (const xmlChar*)"allprop") : NULL;
xmlNodePtr propNode = rootNode ? _XMLChildWithName(rootNode->children, (const xmlChar*)"prop") : NULL;
if (allNode) {
properties = kDAVAllProperties;
} else if (propNode) {
xmlNodePtr node = propNode->children;
while (node) {
if (!xmlStrcmp(node->name, (const xmlChar*)"resourcetype")) {
properties |= kDAVProperty_ResourceType;
} else if (!xmlStrcmp(node->name, (const xmlChar*)"creationdate")) {
properties |= kDAVProperty_CreationDate;
} else if (!xmlStrcmp(node->name, (const xmlChar*)"getlastmodified")) {
properties |= kDAVProperty_LastModified;
} else if (!xmlStrcmp(node->name, (const xmlChar*)"getcontentlength")) {
properties |= kDAVProperty_ContentLength;
} else {
[self logWarning:@"Unknown DAV property requested \"%s\"", node->name];
}
node = node->next;
}
} else {
success = NO;
}
xmlFreeDoc(document);
} else {
success = NO;
}
if (!success) {
NSString* string = [[NSString alloc] initWithData:request.data encoding:NSUTF8StringEncoding];
return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_BadRequest message:@"Invalid DAV properties:\n%@", string];
}
} else {
properties = kDAVAllProperties;
}
NSString* relativePath = request.path;
NSString* absolutePath = [_uploadDirectory stringByAppendingPathComponent:relativePath];
BOOL isDirectory = NO;
if (![self _checkSandboxedPath:absolutePath] || ![[NSFileManager defaultManager] fileExistsAtPath:absolutePath isDirectory:&isDirectory]) {
return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_NotFound message:@"\"%@\" does not exist", relativePath];
}
NSString* itemName = [absolutePath lastPathComponent];
if (([itemName hasPrefix:@"."] && !_allowHidden) || (!isDirectory && ![self _checkFileExtension:itemName])) {
return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_Forbidden message:@"Retrieving properties for item name \"%@\" is not allowed", itemName];
}
NSArray* items = nil;
if (isDirectory) {
NSError* error = nil;
items = [[NSFileManager defaultManager] contentsOfDirectoryAtPath:absolutePath error:&error];
if (items == nil) {
return [GCDWebServerErrorResponse responseWithServerError:kGCDWebServerHTTPStatusCode_InternalServerError underlyingError:error message:@"Failed listing directory \"%@\"", relativePath];
}
}
NSMutableString* xmlString = [NSMutableString stringWithString:@"<?xml version=\"1.0\" encoding=\"utf-8\" ?>"];
[xmlString appendString:@"<D:multistatus xmlns:D=\"DAV:\">\n"];
if (![relativePath hasPrefix:@"/"]) {
relativePath = [@"/" stringByAppendingString:relativePath];
}
[self _addPropertyResponseForItem:absolutePath resource:relativePath properties:properties xmlString:xmlString];
if (depth == 1) {
if (![relativePath hasSuffix:@"/"]) {
relativePath = [relativePath stringByAppendingString:@"/"];
}
for (NSString* item in items) {
if (_allowHidden || ![item hasPrefix:@"."]) {
[self _addPropertyResponseForItem:[absolutePath stringByAppendingPathComponent:item] resource:[relativePath stringByAppendingString:item] properties:properties xmlString:xmlString];
}
}
}
[xmlString appendString:@"</D:multistatus>"];
GCDWebServerDataResponse* response = [GCDWebServerDataResponse responseWithData:[xmlString dataUsingEncoding:NSUTF8StringEncoding]
contentType:@"application/xml; charset=\"utf-8\""];
response.statusCode = kGCDWebServerHTTPStatusCode_MultiStatus;
return response;
}
- (GCDWebServerResponse*)performLOCK:(GCDWebServerDataRequest*)request {
if (!_IsMacFinder(request)) {
return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_MethodNotAllowed message:@"LOCK method only allowed for Mac Finder"];
}
NSString* relativePath = request.path;
NSString* absolutePath = [_uploadDirectory stringByAppendingPathComponent:relativePath];
BOOL isDirectory = NO;
if (![self _checkSandboxedPath:absolutePath] || ![[NSFileManager defaultManager] fileExistsAtPath:absolutePath isDirectory:&isDirectory]) {
return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_NotFound message:@"\"%@\" does not exist", relativePath];
}
NSString* depthHeader = [request.headers objectForKey:@"Depth"];
NSString* timeoutHeader = [request.headers objectForKey:@"Timeout"];
NSString* scope = nil;
NSString* type = nil;
NSString* owner = nil;
NSString* token = nil;
BOOL success = YES;
xmlDocPtr document = xmlReadMemory(request.data.bytes, (int)request.data.length, NULL, NULL, kXMLParseOptions);
if (document) {
xmlNodePtr node = _XMLChildWithName(document->children, (const xmlChar*)"lockinfo");
if (node) {
xmlNodePtr scopeNode = _XMLChildWithName(node->children, (const xmlChar*)"lockscope");
if (scopeNode && scopeNode->children && scopeNode->children->name) {
scope = [NSString stringWithUTF8String:(const char*)scopeNode->children->name];
}
xmlNodePtr typeNode = _XMLChildWithName(node->children, (const xmlChar*)"locktype");
if (typeNode && typeNode->children && typeNode->children->name) {
type = [NSString stringWithUTF8String:(const char*)typeNode->children->name];
}
xmlNodePtr ownerNode = _XMLChildWithName(node->children, (const xmlChar*)"owner");
if (ownerNode) {
ownerNode = _XMLChildWithName(ownerNode->children, (const xmlChar*)"href");
if (ownerNode && ownerNode->children && ownerNode->children->content) {
owner = [NSString stringWithUTF8String:(const char*)ownerNode->children->content];
}
}
} else {
success = NO;
}
xmlFreeDoc(document);
} else {
success = NO;
}
if (!success) {
NSString* string = [[NSString alloc] initWithData:request.data encoding:NSUTF8StringEncoding];
return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_BadRequest message:@"Invalid DAV properties:\n%@", string];
}
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];
}
NSString* itemName = [absolutePath lastPathComponent];
if ((!_allowHidden && [itemName hasPrefix:@"."]) || (!isDirectory && ![self _checkFileExtension:itemName])) {
return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_Forbidden message:@"Locking item name \"%@\" is not allowed", itemName];
}
#ifdef __GCDWEBSERVER_ENABLE_TESTING__
NSString* lockTokenHeader = [request.headers objectForKey:@"X-GCDWebServer-LockToken"];
if (lockTokenHeader) {
token = lockTokenHeader;
}
#endif
if (!token) {
CFUUIDRef uuid = CFUUIDCreate(kCFAllocatorDefault);
CFStringRef string = CFUUIDCreateString(kCFAllocatorDefault, uuid);
token = [NSString stringWithFormat:@"urn:uuid:%@", (__bridge NSString*)string];
CFRelease(string);
CFRelease(uuid);
}
NSMutableString* xmlString = [NSMutableString stringWithString:@"<?xml version=\"1.0\" encoding=\"utf-8\" ?>"];
[xmlString appendString:@"<D:prop xmlns:D=\"DAV:\">\n"];
[xmlString appendString:@"<D:lockdiscovery>\n<D:activelock>\n"];
[xmlString appendFormat:@"<D:locktype><D:%@/></D:locktype>\n", type];
[xmlString appendFormat:@"<D:lockscope><D:%@/></D:lockscope>\n", scope];
[xmlString appendFormat:@"<D:depth>%@</D:depth>\n", depthHeader];
if (owner) {
[xmlString appendFormat:@"<D:owner><D:href>%@</D:href></D:owner>\n", owner];
}
if (timeoutHeader) {
[xmlString appendFormat:@"<D:timeout>%@</D:timeout>\n", timeoutHeader];
}
[xmlString appendFormat:@"<D:locktoken><D:href>%@</D:href></D:locktoken>\n", token];
NSString* lockroot = [@"http://" stringByAppendingString:[[request.headers objectForKey:@"Host"] stringByAppendingString:[@"/" stringByAppendingString:relativePath]]];
[xmlString appendFormat:@"<D:lockroot><D:href>%@</D:href></D:lockroot>\n", lockroot];
[xmlString appendString:@"</D:activelock>\n</D:lockdiscovery>\n"];
[xmlString appendString:@"</D:prop>"];
[self logVerbose:@"WebDAV pretending to lock \"%@\"", relativePath];
GCDWebServerDataResponse* response = [GCDWebServerDataResponse responseWithData:[xmlString dataUsingEncoding:NSUTF8StringEncoding]
contentType:@"application/xml; charset=\"utf-8\""];
return response;
}
- (GCDWebServerResponse*)performUNLOCK:(GCDWebServerRequest*)request {
if (!_IsMacFinder(request)) {
return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_MethodNotAllowed message:@"UNLOCK method only allowed for Mac Finder"];
}
NSString* relativePath = request.path;
NSString* absolutePath = [_uploadDirectory stringByAppendingPathComponent:relativePath];
BOOL isDirectory = NO;
if (![self _checkSandboxedPath:absolutePath] || ![[NSFileManager defaultManager] fileExistsAtPath:absolutePath isDirectory:&isDirectory]) {
return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_NotFound message:@"\"%@\" does not exist", relativePath];
}
NSString* tokenHeader = [request.headers objectForKey:@"Lock-Token"];
if (!tokenHeader.length) {
return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_BadRequest message:@"Missing 'Lock-Token' header"];
}
NSString* itemName = [absolutePath lastPathComponent];
if ((!_allowHidden && [itemName hasPrefix:@"."]) || (!isDirectory && ![self _checkFileExtension:itemName])) {
return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_Forbidden message:@"Unlocking item name \"%@\" is not allowed", itemName];
}
[self logVerbose:@"WebDAV pretending to unlock \"%@\"", relativePath];
return [GCDWebServerResponse responseWithStatusCode:kGCDWebServerHTTPStatusCode_NoContent];
}
@end
@implementation GCDWebDAVServer
@synthesize uploadDirectory=_uploadDirectory, allowedFileExtensions=_allowedExtensions, allowHiddenItems=_allowHidden;
- (instancetype)initWithUploadDirectory:(NSString*)path {
if ((self = [super init])) {
_uploadDirectory = [[path stringByStandardizingPath] 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
@implementation GCDWebDAVServer (Subclassing)
- (BOOL)shouldUploadFileAtPath:(NSString*)path withTemporaryFile:(NSString*)tempPath {
return YES;
}
- (BOOL)shouldMoveItemFromPath:(NSString*)fromPath toPath:(NSString*)toPath {
return YES;
}
- (BOOL)shouldCopyItemFromPath:(NSString*)fromPath toPath:(NSString*)toPath {
return YES;
}
- (BOOL)shouldDeleteItemAtPath:(NSString*)path {
return YES;
}
- (BOOL)shouldCreateDirectoryAtPath:(NSString*)path {
return YES;
}
@end

49
GCDWebServer.podspec Normal file
View File

@@ -0,0 +1,49 @@
# http://guides.cocoapods.org/syntax/podspec.html
# http://guides.cocoapods.org/making/getting-setup-with-trunk.html
# $ sudo gem update cocoapods
# (optional) $ pod trunk register {email} {name} --description={computer}
# $ pod trunk push
# DELETE THIS SECTION BEFORE PROCEEDING!
Pod::Spec.new do |s|
s.name = 'GCDWebServer'
s.version = '3.2'
s.author = { 'Pierre-Olivier Latour' => 'info@pol-online.net' }
s.license = { :type => 'BSD', :file => 'LICENSE' }
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.source = { :git => 'https://github.com/swisspol/GCDWebServer.git', :tag => s.version.to_s }
s.ios.deployment_target = '5.0'
s.osx.deployment_target = '10.7'
s.requires_arc = true
s.default_subspec = 'Core'
s.subspec 'Core' do |cs|
cs.source_files = 'GCDWebServer/**/*.{h,m}'
cs.private_header_files = "GCDWebServer/Core/GCDWebServerPrivate.h"
cs.requires_arc = true
cs.ios.library = 'z'
cs.ios.frameworks = 'MobileCoreServices', 'CFNetwork'
cs.osx.library = 'z'
cs.osx.framework = 'SystemConfiguration'
end
s.subspec 'WebDAV' do |cs|
cs.dependency 'GCDWebServer/Core'
cs.source_files = 'GCDWebDAVServer/*.{h,m}'
cs.requires_arc = true
cs.ios.library = 'xml2'
cs.osx.library = 'xml2'
cs.compiler_flags = '-I$(SDKROOT)/usr/include/libxml2'
end
s.subspec 'WebUploader' do |cs|
cs.dependency 'GCDWebServer/Core'
cs.source_files = 'GCDWebUploader/*.{h,m}'
cs.requires_arc = true
cs.resource = "GCDWebUploader/GCDWebUploader.bundle"
end
end

View File

@@ -24,20 +24,44 @@
/* Begin PBXBuildFile section */
E208D149167B76B700500836 /* CFNetwork.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = E208D148167B76B700500836 /* CFNetwork.framework */; };
E208D1B3167BB17E00500836 /* CoreServices.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = E208D1B2167BB17E00500836 /* CoreServices.framework */; };
E22112851690B63A0048D2B2 /* GCDWebServer.m in Sources */ = {isa = PBXBuildFile; fileRef = E221127D1690B63A0048D2B2 /* GCDWebServer.m */; };
E22112861690B63A0048D2B2 /* GCDWebServer.m in Sources */ = {isa = PBXBuildFile; fileRef = E221127D1690B63A0048D2B2 /* GCDWebServer.m */; };
E22112871690B63A0048D2B2 /* GCDWebServerConnection.m in Sources */ = {isa = PBXBuildFile; fileRef = E221127F1690B63A0048D2B2 /* GCDWebServerConnection.m */; };
E22112881690B63A0048D2B2 /* GCDWebServerConnection.m in Sources */ = {isa = PBXBuildFile; fileRef = E221127F1690B63A0048D2B2 /* GCDWebServerConnection.m */; };
E22112891690B63A0048D2B2 /* GCDWebServerRequest.m in Sources */ = {isa = PBXBuildFile; fileRef = E22112821690B63A0048D2B2 /* GCDWebServerRequest.m */; };
E221128A1690B63A0048D2B2 /* GCDWebServerRequest.m in Sources */ = {isa = PBXBuildFile; fileRef = E22112821690B63A0048D2B2 /* GCDWebServerRequest.m */; };
E221128B1690B63A0048D2B2 /* GCDWebServerResponse.m in Sources */ = {isa = PBXBuildFile; fileRef = E22112841690B63A0048D2B2 /* GCDWebServerResponse.m */; };
E221128C1690B63A0048D2B2 /* GCDWebServerResponse.m in Sources */ = {isa = PBXBuildFile; fileRef = E22112841690B63A0048D2B2 /* GCDWebServerResponse.m */; };
E221128F1690B6470048D2B2 /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = E221128E1690B6470048D2B2 /* main.m */; };
E22112951690B64F0048D2B2 /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = E22112921690B64F0048D2B2 /* AppDelegate.m */; };
E22112971690B64F0048D2B2 /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = E22112941690B64F0048D2B2 /* main.m */; };
E22112991690B7AA0048D2B2 /* CFNetwork.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = E22112981690B7AA0048D2B2 /* CFNetwork.framework */; };
E221129B1690B7B10048D2B2 /* UIKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = E221129A1690B7B10048D2B2 /* UIKit.framework */; };
E221129D1690B7BA0048D2B2 /* MobileCoreServices.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = E221129C1690B7BA0048D2B2 /* MobileCoreServices.framework */; };
E28BAE3418F99C810095C089 /* GCDWebServer.m in Sources */ = {isa = PBXBuildFile; fileRef = E28BAE1718F99C810095C089 /* GCDWebServer.m */; };
E28BAE3518F99C810095C089 /* GCDWebServer.m in Sources */ = {isa = PBXBuildFile; fileRef = E28BAE1718F99C810095C089 /* GCDWebServer.m */; };
E28BAE3618F99C810095C089 /* GCDWebServerConnection.m in Sources */ = {isa = PBXBuildFile; fileRef = E28BAE1918F99C810095C089 /* GCDWebServerConnection.m */; };
E28BAE3718F99C810095C089 /* GCDWebServerConnection.m in Sources */ = {isa = PBXBuildFile; fileRef = E28BAE1918F99C810095C089 /* GCDWebServerConnection.m */; };
E28BAE3818F99C810095C089 /* GCDWebServerFunctions.m in Sources */ = {isa = PBXBuildFile; fileRef = E28BAE1B18F99C810095C089 /* GCDWebServerFunctions.m */; };
E28BAE3918F99C810095C089 /* GCDWebServerFunctions.m in Sources */ = {isa = PBXBuildFile; fileRef = E28BAE1B18F99C810095C089 /* GCDWebServerFunctions.m */; };
E28BAE3A18F99C810095C089 /* GCDWebServerRequest.m in Sources */ = {isa = PBXBuildFile; fileRef = E28BAE1F18F99C810095C089 /* GCDWebServerRequest.m */; };
E28BAE3B18F99C810095C089 /* GCDWebServerRequest.m in Sources */ = {isa = PBXBuildFile; fileRef = E28BAE1F18F99C810095C089 /* GCDWebServerRequest.m */; };
E28BAE3C18F99C810095C089 /* GCDWebServerResponse.m in Sources */ = {isa = PBXBuildFile; fileRef = E28BAE2118F99C810095C089 /* GCDWebServerResponse.m */; };
E28BAE3D18F99C810095C089 /* GCDWebServerResponse.m in Sources */ = {isa = PBXBuildFile; fileRef = E28BAE2118F99C810095C089 /* GCDWebServerResponse.m */; };
E28BAE3E18F99C810095C089 /* GCDWebServerDataRequest.m in Sources */ = {isa = PBXBuildFile; fileRef = E28BAE2418F99C810095C089 /* GCDWebServerDataRequest.m */; };
E28BAE3F18F99C810095C089 /* GCDWebServerDataRequest.m in Sources */ = {isa = PBXBuildFile; fileRef = E28BAE2418F99C810095C089 /* GCDWebServerDataRequest.m */; };
E28BAE4018F99C810095C089 /* GCDWebServerFileRequest.m in Sources */ = {isa = PBXBuildFile; fileRef = E28BAE2618F99C810095C089 /* GCDWebServerFileRequest.m */; };
E28BAE4118F99C810095C089 /* GCDWebServerFileRequest.m in Sources */ = {isa = PBXBuildFile; fileRef = E28BAE2618F99C810095C089 /* GCDWebServerFileRequest.m */; };
E28BAE4218F99C810095C089 /* GCDWebServerMultiPartFormRequest.m in Sources */ = {isa = PBXBuildFile; fileRef = E28BAE2818F99C810095C089 /* GCDWebServerMultiPartFormRequest.m */; };
E28BAE4318F99C810095C089 /* GCDWebServerMultiPartFormRequest.m in Sources */ = {isa = PBXBuildFile; fileRef = E28BAE2818F99C810095C089 /* GCDWebServerMultiPartFormRequest.m */; };
E28BAE4418F99C810095C089 /* GCDWebServerURLEncodedFormRequest.m in Sources */ = {isa = PBXBuildFile; fileRef = E28BAE2A18F99C810095C089 /* GCDWebServerURLEncodedFormRequest.m */; };
E28BAE4518F99C810095C089 /* GCDWebServerURLEncodedFormRequest.m in Sources */ = {isa = PBXBuildFile; fileRef = E28BAE2A18F99C810095C089 /* GCDWebServerURLEncodedFormRequest.m */; };
E28BAE4618F99C810095C089 /* GCDWebServerDataResponse.m in Sources */ = {isa = PBXBuildFile; fileRef = E28BAE2D18F99C810095C089 /* GCDWebServerDataResponse.m */; };
E28BAE4718F99C810095C089 /* GCDWebServerDataResponse.m in Sources */ = {isa = PBXBuildFile; fileRef = E28BAE2D18F99C810095C089 /* GCDWebServerDataResponse.m */; };
E28BAE4818F99C810095C089 /* GCDWebServerErrorResponse.m in Sources */ = {isa = PBXBuildFile; fileRef = E28BAE2F18F99C810095C089 /* GCDWebServerErrorResponse.m */; };
E28BAE4918F99C810095C089 /* GCDWebServerErrorResponse.m in Sources */ = {isa = PBXBuildFile; fileRef = E28BAE2F18F99C810095C089 /* GCDWebServerErrorResponse.m */; };
E28BAE4A18F99C810095C089 /* GCDWebServerFileResponse.m in Sources */ = {isa = PBXBuildFile; fileRef = E28BAE3118F99C810095C089 /* GCDWebServerFileResponse.m */; };
E28BAE4B18F99C810095C089 /* GCDWebServerFileResponse.m in Sources */ = {isa = PBXBuildFile; fileRef = E28BAE3118F99C810095C089 /* GCDWebServerFileResponse.m */; };
E28BAE4C18F99C810095C089 /* GCDWebServerStreamedResponse.m in Sources */ = {isa = PBXBuildFile; fileRef = E28BAE3318F99C810095C089 /* GCDWebServerStreamedResponse.m */; };
E28BAE4D18F99C810095C089 /* GCDWebServerStreamedResponse.m in Sources */ = {isa = PBXBuildFile; fileRef = E28BAE3318F99C810095C089 /* GCDWebServerStreamedResponse.m */; };
E2A0E80A18F3432600C580B1 /* GCDWebDAVServer.m in Sources */ = {isa = PBXBuildFile; fileRef = E2A0E80918F3432600C580B1 /* GCDWebDAVServer.m */; };
E2A0E80B18F3432600C580B1 /* GCDWebDAVServer.m in Sources */ = {isa = PBXBuildFile; fileRef = E2A0E80918F3432600C580B1 /* GCDWebDAVServer.m */; };
E2A0E80D18F35C9A00C580B1 /* libxml2.dylib in Frameworks */ = {isa = PBXBuildFile; fileRef = E2A0E80C18F35C9A00C580B1 /* libxml2.dylib */; };
E2A0E80F18F35CA300C580B1 /* libxml2.dylib in Frameworks */ = {isa = PBXBuildFile; fileRef = E2A0E80E18F35CA300C580B1 /* libxml2.dylib */; };
E2B0D4A718F13495009A7927 /* libz.dylib in Frameworks */ = {isa = PBXBuildFile; fileRef = E2B0D4A618F13495009A7927 /* libz.dylib */; };
E2B0D4A918F134A8009A7927 /* libz.dylib in Frameworks */ = {isa = PBXBuildFile; fileRef = E2B0D4A818F134A8009A7927 /* libz.dylib */; };
E2BE850A18E77ECA0061360B /* GCDWebUploader.bundle in Resources */ = {isa = PBXBuildFile; fileRef = E2BE850718E77ECA0061360B /* GCDWebUploader.bundle */; };
E2BE850B18E77ECA0061360B /* GCDWebUploader.m in Sources */ = {isa = PBXBuildFile; fileRef = E2BE850918E77ECA0061360B /* GCDWebUploader.m */; };
E2BE850C18E785940061360B /* GCDWebUploader.m in Sources */ = {isa = PBXBuildFile; fileRef = E2BE850918E77ECA0061360B /* GCDWebUploader.m */; };
@@ -80,15 +104,6 @@
E208D148167B76B700500836 /* CFNetwork.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CFNetwork.framework; path = System/Library/Frameworks/CFNetwork.framework; sourceTree = SDKROOT; };
E208D1B2167BB17E00500836 /* CoreServices.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreServices.framework; path = System/Library/Frameworks/CoreServices.framework; sourceTree = SDKROOT; };
E221125A1690B4DE0048D2B2 /* GCDWebServer.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = GCDWebServer.app; sourceTree = BUILT_PRODUCTS_DIR; };
E221127C1690B63A0048D2B2 /* GCDWebServer.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = GCDWebServer.h; sourceTree = "<group>"; };
E221127D1690B63A0048D2B2 /* GCDWebServer.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GCDWebServer.m; sourceTree = "<group>"; };
E221127E1690B63A0048D2B2 /* GCDWebServerConnection.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = GCDWebServerConnection.h; sourceTree = "<group>"; };
E221127F1690B63A0048D2B2 /* GCDWebServerConnection.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GCDWebServerConnection.m; sourceTree = "<group>"; };
E22112801690B63A0048D2B2 /* GCDWebServerPrivate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = GCDWebServerPrivate.h; sourceTree = "<group>"; };
E22112811690B63A0048D2B2 /* GCDWebServerRequest.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = GCDWebServerRequest.h; sourceTree = "<group>"; };
E22112821690B63A0048D2B2 /* GCDWebServerRequest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GCDWebServerRequest.m; sourceTree = "<group>"; };
E22112831690B63A0048D2B2 /* GCDWebServerResponse.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = GCDWebServerResponse.h; sourceTree = "<group>"; };
E22112841690B63A0048D2B2 /* GCDWebServerResponse.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GCDWebServerResponse.m; sourceTree = "<group>"; };
E221128E1690B6470048D2B2 /* main.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = "<group>"; };
E22112911690B64F0048D2B2 /* AppDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = "<group>"; };
E22112921690B64F0048D2B2 /* AppDelegate.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AppDelegate.m; sourceTree = "<group>"; };
@@ -97,6 +112,42 @@
E22112981690B7AA0048D2B2 /* CFNetwork.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CFNetwork.framework; path = Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS6.0.sdk/System/Library/Frameworks/CFNetwork.framework; sourceTree = DEVELOPER_DIR; };
E221129A1690B7B10048D2B2 /* UIKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = UIKit.framework; path = Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS6.0.sdk/System/Library/Frameworks/UIKit.framework; sourceTree = DEVELOPER_DIR; };
E221129C1690B7BA0048D2B2 /* MobileCoreServices.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = MobileCoreServices.framework; path = Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS6.0.sdk/System/Library/Frameworks/MobileCoreServices.framework; sourceTree = DEVELOPER_DIR; };
E26DC18719E84B2200C68DDC /* GCDWebServer.podspec */ = {isa = PBXFileReference; lastKnownFileType = text; path = GCDWebServer.podspec; sourceTree = "<group>"; };
E26DC18819E84BC000C68DDC /* README.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = "<group>"; };
E28BAE1618F99C810095C089 /* GCDWebServer.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = GCDWebServer.h; sourceTree = "<group>"; };
E28BAE1718F99C810095C089 /* GCDWebServer.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GCDWebServer.m; sourceTree = "<group>"; };
E28BAE1818F99C810095C089 /* GCDWebServerConnection.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = GCDWebServerConnection.h; sourceTree = "<group>"; };
E28BAE1918F99C810095C089 /* GCDWebServerConnection.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GCDWebServerConnection.m; sourceTree = "<group>"; };
E28BAE1A18F99C810095C089 /* GCDWebServerFunctions.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = GCDWebServerFunctions.h; sourceTree = "<group>"; };
E28BAE1B18F99C810095C089 /* GCDWebServerFunctions.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GCDWebServerFunctions.m; sourceTree = "<group>"; };
E28BAE1C18F99C810095C089 /* GCDWebServerHTTPStatusCodes.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = GCDWebServerHTTPStatusCodes.h; sourceTree = "<group>"; };
E28BAE1D18F99C810095C089 /* GCDWebServerPrivate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = GCDWebServerPrivate.h; sourceTree = "<group>"; };
E28BAE1E18F99C810095C089 /* GCDWebServerRequest.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = GCDWebServerRequest.h; sourceTree = "<group>"; };
E28BAE1F18F99C810095C089 /* GCDWebServerRequest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GCDWebServerRequest.m; sourceTree = "<group>"; };
E28BAE2018F99C810095C089 /* GCDWebServerResponse.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = GCDWebServerResponse.h; sourceTree = "<group>"; };
E28BAE2118F99C810095C089 /* GCDWebServerResponse.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GCDWebServerResponse.m; sourceTree = "<group>"; };
E28BAE2318F99C810095C089 /* GCDWebServerDataRequest.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = GCDWebServerDataRequest.h; sourceTree = "<group>"; };
E28BAE2418F99C810095C089 /* GCDWebServerDataRequest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GCDWebServerDataRequest.m; sourceTree = "<group>"; };
E28BAE2518F99C810095C089 /* GCDWebServerFileRequest.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = GCDWebServerFileRequest.h; sourceTree = "<group>"; };
E28BAE2618F99C810095C089 /* GCDWebServerFileRequest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GCDWebServerFileRequest.m; sourceTree = "<group>"; };
E28BAE2718F99C810095C089 /* GCDWebServerMultiPartFormRequest.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = GCDWebServerMultiPartFormRequest.h; sourceTree = "<group>"; };
E28BAE2818F99C810095C089 /* GCDWebServerMultiPartFormRequest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GCDWebServerMultiPartFormRequest.m; sourceTree = "<group>"; };
E28BAE2918F99C810095C089 /* GCDWebServerURLEncodedFormRequest.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = GCDWebServerURLEncodedFormRequest.h; sourceTree = "<group>"; };
E28BAE2A18F99C810095C089 /* GCDWebServerURLEncodedFormRequest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GCDWebServerURLEncodedFormRequest.m; sourceTree = "<group>"; };
E28BAE2C18F99C810095C089 /* GCDWebServerDataResponse.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = GCDWebServerDataResponse.h; sourceTree = "<group>"; };
E28BAE2D18F99C810095C089 /* GCDWebServerDataResponse.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GCDWebServerDataResponse.m; sourceTree = "<group>"; };
E28BAE2E18F99C810095C089 /* GCDWebServerErrorResponse.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = GCDWebServerErrorResponse.h; sourceTree = "<group>"; };
E28BAE2F18F99C810095C089 /* GCDWebServerErrorResponse.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GCDWebServerErrorResponse.m; sourceTree = "<group>"; };
E28BAE3018F99C810095C089 /* GCDWebServerFileResponse.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = GCDWebServerFileResponse.h; sourceTree = "<group>"; };
E28BAE3118F99C810095C089 /* GCDWebServerFileResponse.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GCDWebServerFileResponse.m; sourceTree = "<group>"; };
E28BAE3218F99C810095C089 /* GCDWebServerStreamedResponse.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = GCDWebServerStreamedResponse.h; sourceTree = "<group>"; };
E28BAE3318F99C810095C089 /* GCDWebServerStreamedResponse.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GCDWebServerStreamedResponse.m; sourceTree = "<group>"; };
E2A0E80818F3432600C580B1 /* GCDWebDAVServer.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = GCDWebDAVServer.h; sourceTree = "<group>"; };
E2A0E80918F3432600C580B1 /* GCDWebDAVServer.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GCDWebDAVServer.m; sourceTree = "<group>"; };
E2A0E80C18F35C9A00C580B1 /* libxml2.dylib */ = {isa = PBXFileReference; lastKnownFileType = "compiled.mach-o.dylib"; name = libxml2.dylib; path = Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS7.1.sdk/usr/lib/libxml2.dylib; sourceTree = DEVELOPER_DIR; };
E2A0E80E18F35CA300C580B1 /* libxml2.dylib */ = {isa = PBXFileReference; lastKnownFileType = "compiled.mach-o.dylib"; name = libxml2.dylib; path = usr/lib/libxml2.dylib; sourceTree = SDKROOT; };
E2B0D4A618F13495009A7927 /* libz.dylib */ = {isa = PBXFileReference; lastKnownFileType = "compiled.mach-o.dylib"; name = libz.dylib; path = usr/lib/libz.dylib; sourceTree = SDKROOT; };
E2B0D4A818F134A8009A7927 /* libz.dylib */ = {isa = PBXFileReference; lastKnownFileType = "compiled.mach-o.dylib"; name = libz.dylib; path = Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS7.1.sdk/usr/lib/libz.dylib; sourceTree = DEVELOPER_DIR; };
E2BE850718E77ECA0061360B /* GCDWebUploader.bundle */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.plug-in"; path = GCDWebUploader.bundle; sourceTree = "<group>"; };
E2BE850818E77ECA0061360B /* GCDWebUploader.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = GCDWebUploader.h; sourceTree = "<group>"; };
E2BE850918E77ECA0061360B /* GCDWebUploader.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GCDWebUploader.m; sourceTree = "<group>"; };
@@ -111,6 +162,8 @@
E2BE851118E79DAF0061360B /* SystemConfiguration.framework in Frameworks */,
E208D1B3167BB17E00500836 /* CoreServices.framework in Frameworks */,
E208D149167B76B700500836 /* CFNetwork.framework in Frameworks */,
E2A0E80F18F35CA300C580B1 /* libxml2.dylib in Frameworks */,
E2B0D4A718F13495009A7927 /* libz.dylib in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -121,6 +174,8 @@
E221129D1690B7BA0048D2B2 /* MobileCoreServices.framework in Frameworks */,
E221129B1690B7B10048D2B2 /* UIKit.framework in Frameworks */,
E22112991690B7AA0048D2B2 /* CFNetwork.framework in Frameworks */,
E2A0E80D18F35C9A00C580B1 /* libxml2.dylib in Frameworks */,
E2B0D4A918F134A8009A7927 /* libz.dylib in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -130,7 +185,10 @@
08FB7794FE84155DC02AAC07 /* LittleCMS */ = {
isa = PBXGroup;
children = (
E221127B1690B63A0048D2B2 /* CGDWebServer */,
E26DC18819E84BC000C68DDC /* README.md */,
E26DC18719E84B2200C68DDC /* GCDWebServer.podspec */,
E28BAE1418F99C810095C089 /* GCDWebServer */,
E2A0E80718F3432600C580B1 /* GCDWebDAVServer */,
E2BE850618E77ECA0061360B /* GCDWebUploader */,
E221128D1690B6470048D2B2 /* Mac */,
E22112901690B64F0048D2B2 /* iOS */,
@@ -150,22 +208,6 @@
name = Products;
sourceTree = "<group>";
};
E221127B1690B63A0048D2B2 /* CGDWebServer */ = {
isa = PBXGroup;
children = (
E221127C1690B63A0048D2B2 /* GCDWebServer.h */,
E221127D1690B63A0048D2B2 /* GCDWebServer.m */,
E221127E1690B63A0048D2B2 /* GCDWebServerConnection.h */,
E221127F1690B63A0048D2B2 /* GCDWebServerConnection.m */,
E22112801690B63A0048D2B2 /* GCDWebServerPrivate.h */,
E22112811690B63A0048D2B2 /* GCDWebServerRequest.h */,
E22112821690B63A0048D2B2 /* GCDWebServerRequest.m */,
E22112831690B63A0048D2B2 /* GCDWebServerResponse.h */,
E22112841690B63A0048D2B2 /* GCDWebServerResponse.m */,
);
path = CGDWebServer;
sourceTree = "<group>";
};
E221128D1690B6470048D2B2 /* Mac */ = {
isa = PBXGroup;
children = (
@@ -191,6 +233,8 @@
E221129C1690B7BA0048D2B2 /* MobileCoreServices.framework */,
E221129A1690B7B10048D2B2 /* UIKit.framework */,
E22112981690B7AA0048D2B2 /* CFNetwork.framework */,
E2A0E80C18F35C9A00C580B1 /* libxml2.dylib */,
E2B0D4A818F134A8009A7927 /* libz.dylib */,
);
name = "iOS Frameworks and Libraries";
sourceTree = "<group>";
@@ -201,10 +245,80 @@
E2BE851018E79DAF0061360B /* SystemConfiguration.framework */,
E208D1B2167BB17E00500836 /* CoreServices.framework */,
E208D148167B76B700500836 /* CFNetwork.framework */,
E2A0E80E18F35CA300C580B1 /* libxml2.dylib */,
E2B0D4A618F13495009A7927 /* libz.dylib */,
);
name = "Mac Frameworks and Libraries";
sourceTree = "<group>";
};
E28BAE1418F99C810095C089 /* GCDWebServer */ = {
isa = PBXGroup;
children = (
E28BAE1518F99C810095C089 /* Core */,
E28BAE2218F99C810095C089 /* Requests */,
E28BAE2B18F99C810095C089 /* Responses */,
);
path = GCDWebServer;
sourceTree = "<group>";
};
E28BAE1518F99C810095C089 /* Core */ = {
isa = PBXGroup;
children = (
E28BAE1618F99C810095C089 /* GCDWebServer.h */,
E28BAE1718F99C810095C089 /* GCDWebServer.m */,
E28BAE1818F99C810095C089 /* GCDWebServerConnection.h */,
E28BAE1918F99C810095C089 /* GCDWebServerConnection.m */,
E28BAE1A18F99C810095C089 /* GCDWebServerFunctions.h */,
E28BAE1B18F99C810095C089 /* GCDWebServerFunctions.m */,
E28BAE1C18F99C810095C089 /* GCDWebServerHTTPStatusCodes.h */,
E28BAE1D18F99C810095C089 /* GCDWebServerPrivate.h */,
E28BAE1E18F99C810095C089 /* GCDWebServerRequest.h */,
E28BAE1F18F99C810095C089 /* GCDWebServerRequest.m */,
E28BAE2018F99C810095C089 /* GCDWebServerResponse.h */,
E28BAE2118F99C810095C089 /* GCDWebServerResponse.m */,
);
path = Core;
sourceTree = "<group>";
};
E28BAE2218F99C810095C089 /* Requests */ = {
isa = PBXGroup;
children = (
E28BAE2318F99C810095C089 /* GCDWebServerDataRequest.h */,
E28BAE2418F99C810095C089 /* GCDWebServerDataRequest.m */,
E28BAE2518F99C810095C089 /* GCDWebServerFileRequest.h */,
E28BAE2618F99C810095C089 /* GCDWebServerFileRequest.m */,
E28BAE2718F99C810095C089 /* GCDWebServerMultiPartFormRequest.h */,
E28BAE2818F99C810095C089 /* GCDWebServerMultiPartFormRequest.m */,
E28BAE2918F99C810095C089 /* GCDWebServerURLEncodedFormRequest.h */,
E28BAE2A18F99C810095C089 /* GCDWebServerURLEncodedFormRequest.m */,
);
path = Requests;
sourceTree = "<group>";
};
E28BAE2B18F99C810095C089 /* Responses */ = {
isa = PBXGroup;
children = (
E28BAE2C18F99C810095C089 /* GCDWebServerDataResponse.h */,
E28BAE2D18F99C810095C089 /* GCDWebServerDataResponse.m */,
E28BAE2E18F99C810095C089 /* GCDWebServerErrorResponse.h */,
E28BAE2F18F99C810095C089 /* GCDWebServerErrorResponse.m */,
E28BAE3018F99C810095C089 /* GCDWebServerFileResponse.h */,
E28BAE3118F99C810095C089 /* GCDWebServerFileResponse.m */,
E28BAE3218F99C810095C089 /* GCDWebServerStreamedResponse.h */,
E28BAE3318F99C810095C089 /* GCDWebServerStreamedResponse.m */,
);
path = Responses;
sourceTree = "<group>";
};
E2A0E80718F3432600C580B1 /* GCDWebDAVServer */ = {
isa = PBXGroup;
children = (
E2A0E80818F3432600C580B1 /* GCDWebDAVServer.h */,
E2A0E80918F3432600C580B1 /* GCDWebDAVServer.m */,
);
path = GCDWebDAVServer;
sourceTree = "<group>";
};
E2BE850618E77ECA0061360B /* GCDWebUploader */ = {
isa = PBXGroup;
children = (
@@ -222,6 +336,7 @@
isa = PBXNativeTarget;
buildConfigurationList = 1DEB928508733DD80010E9CD /* Build configuration list for PBXNativeTarget "GCDWebServer (Mac)" */;
buildPhases = (
E21F038418FB37D80043AD1E /* Delete GCDWebUploader.bundle */,
E2BE850E18E788910061360B /* CopyFiles */,
8DD76FAB0486AB0100D96B5E /* Sources */,
8DD76FAD0486AB0100D96B5E /* Frameworks */,
@@ -259,7 +374,7 @@
08FB7793FE84155DC02AAC07 /* Project object */ = {
isa = PBXProject;
attributes = {
LastUpgradeCheck = 0510;
LastUpgradeCheck = 0610;
};
buildConfigurationList = 1DEB928908733DD80010E9CD /* Build configuration list for PBXProject "GCDWebServer" */;
compatibilityVersion = "Xcode 3.2";
@@ -294,17 +409,45 @@
};
/* End PBXResourcesBuildPhase section */
/* Begin PBXShellScriptBuildPhase section */
E21F038418FB37D80043AD1E /* Delete GCDWebUploader.bundle */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputPaths = (
);
name = "Delete GCDWebUploader.bundle";
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "cd \"$BUILT_PRODUCTS_DIR\"\nrm -rf \"GCDWebUploader.bundle\"\n";
showEnvVarsInLog = 0;
};
/* End PBXShellScriptBuildPhase section */
/* Begin PBXSourcesBuildPhase section */
8DD76FAB0486AB0100D96B5E /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
E22112851690B63A0048D2B2 /* GCDWebServer.m in Sources */,
E22112871690B63A0048D2B2 /* GCDWebServerConnection.m in Sources */,
E22112891690B63A0048D2B2 /* GCDWebServerRequest.m in Sources */,
E221128B1690B63A0048D2B2 /* GCDWebServerResponse.m in Sources */,
E28BAE4618F99C810095C089 /* GCDWebServerDataResponse.m in Sources */,
E28BAE3818F99C810095C089 /* GCDWebServerFunctions.m in Sources */,
E28BAE4A18F99C810095C089 /* GCDWebServerFileResponse.m in Sources */,
E28BAE4418F99C810095C089 /* GCDWebServerURLEncodedFormRequest.m in Sources */,
E28BAE3A18F99C810095C089 /* GCDWebServerRequest.m in Sources */,
E28BAE3418F99C810095C089 /* GCDWebServer.m in Sources */,
E28BAE3618F99C810095C089 /* GCDWebServerConnection.m in Sources */,
E28BAE3C18F99C810095C089 /* GCDWebServerResponse.m in Sources */,
E28BAE4018F99C810095C089 /* GCDWebServerFileRequest.m in Sources */,
E28BAE4C18F99C810095C089 /* GCDWebServerStreamedResponse.m in Sources */,
E28BAE3E18F99C810095C089 /* GCDWebServerDataRequest.m in Sources */,
E2A0E80A18F3432600C580B1 /* GCDWebDAVServer.m in Sources */,
E28BAE4218F99C810095C089 /* GCDWebServerMultiPartFormRequest.m in Sources */,
E2BE850C18E785940061360B /* GCDWebUploader.m in Sources */,
E221128F1690B6470048D2B2 /* main.m in Sources */,
E28BAE4818F99C810095C089 /* GCDWebServerErrorResponse.m in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -312,13 +455,23 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
E22112861690B63A0048D2B2 /* GCDWebServer.m in Sources */,
E22112881690B63A0048D2B2 /* GCDWebServerConnection.m in Sources */,
E221128A1690B63A0048D2B2 /* GCDWebServerRequest.m in Sources */,
E221128C1690B63A0048D2B2 /* GCDWebServerResponse.m in Sources */,
E2A0E80B18F3432600C580B1 /* GCDWebDAVServer.m in Sources */,
E28BAE4918F99C810095C089 /* GCDWebServerErrorResponse.m in Sources */,
E28BAE4518F99C810095C089 /* GCDWebServerURLEncodedFormRequest.m in Sources */,
E28BAE4B18F99C810095C089 /* GCDWebServerFileResponse.m in Sources */,
E28BAE3918F99C810095C089 /* GCDWebServerFunctions.m in Sources */,
E28BAE4118F99C810095C089 /* GCDWebServerFileRequest.m in Sources */,
E28BAE4D18F99C810095C089 /* GCDWebServerStreamedResponse.m in Sources */,
E28BAE3F18F99C810095C089 /* GCDWebServerDataRequest.m in Sources */,
E2BE850B18E77ECA0061360B /* GCDWebUploader.m in Sources */,
E22112951690B64F0048D2B2 /* AppDelegate.m in Sources */,
E28BAE4718F99C810095C089 /* GCDWebServerDataResponse.m in Sources */,
E28BAE3D18F99C810095C089 /* GCDWebServerResponse.m in Sources */,
E28BAE3518F99C810095C089 /* GCDWebServer.m in Sources */,
E28BAE3718F99C810095C089 /* GCDWebServerConnection.m in Sources */,
E28BAE3B18F99C810095C089 /* GCDWebServerRequest.m in Sources */,
E22112971690B64F0048D2B2 /* main.m in Sources */,
E28BAE4318F99C810095C089 /* GCDWebServerMultiPartFormRequest.m in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -341,7 +494,6 @@
1DEB928608733DD80010E9CD /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
ARCHS = "$(ARCHS_STANDARD)";
MACOSX_DEPLOYMENT_TARGET = 10.7;
PRODUCT_NAME = GCDWebServer;
SDKROOT = macosx;
@@ -351,7 +503,6 @@
1DEB928708733DD80010E9CD /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
ARCHS = "$(ARCHS_STANDARD)";
MACOSX_DEPLOYMENT_TARGET = 10.7;
PRODUCT_NAME = GCDWebServer;
SDKROOT = macosx;
@@ -362,18 +513,29 @@
isa = XCBuildConfiguration;
buildSettings = {
CLANG_ENABLE_OBJC_ARC = YES;
ENABLE_STRICT_OBJC_MSGSEND = YES;
GCC_OPTIMIZATION_LEVEL = 0;
GCC_PREPROCESSOR_DEFINITIONS = "DEBUG=1";
GCC_PREPROCESSOR_DEFINITIONS_NOT_USED_IN_PRECOMPS = __GCDWEBSERVER_ENABLE_TESTING__;
HEADER_SEARCH_PATHS = "$(SDKROOT)/usr/include/libxml2";
ONLY_ACTIVE_ARCH = YES;
WARNING_CFLAGS = (
"-Wall",
"-Weverything",
"-Wshadow",
"-Wshorten-64-to-32",
"-Wno-vla",
"-Wno-explicit-ownership-type",
"-Wno-gnu-statement-expression",
"-Wno-direct-ivar-access",
"-Wno-implicit-retain-self",
"-Wno-assign-enum",
"-Wno-format-nonliteral",
"-Wno-cast-align",
"-Wno-padded",
"-Wno-documentation",
"-Wno-documentation-unknown-command",
"-Wno-objc-missing-property-synthesis",
);
};
name = Debug;
@@ -382,8 +544,10 @@
isa = XCBuildConfiguration;
buildSettings = {
CLANG_ENABLE_OBJC_ARC = YES;
GCC_PREPROCESSOR_DEFINITIONS = NDEBUG;
ENABLE_STRICT_OBJC_MSGSEND = YES;
GCC_PREPROCESSOR_DEFINITIONS_NOT_USED_IN_PRECOMPS = __GCDWEBSERVER_ENABLE_TESTING__;
GCC_TREAT_WARNINGS_AS_ERRORS = YES;
HEADER_SEARCH_PATHS = "$(SDKROOT)/usr/include/libxml2";
WARNING_CFLAGS = "-Wall";
};
name = Release;
@@ -391,10 +555,9 @@
E22112761690B4DF0048D2B2 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
ARCHS = "$(ARCHS_STANDARD)";
CODE_SIGN_IDENTITY = "iPhone Developer";
INFOPLIST_FILE = iOS/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 5.1.1;
IPHONEOS_DEPLOYMENT_TARGET = 5.0;
PRODUCT_NAME = GCDWebServer;
PROVISIONING_PROFILE = "";
SDKROOT = iphoneos;
@@ -405,10 +568,9 @@
E22112771690B4DF0048D2B2 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
ARCHS = "$(ARCHS_STANDARD)";
CODE_SIGN_IDENTITY = "iPhone Developer";
INFOPLIST_FILE = iOS/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 5.1.1;
IPHONEOS_DEPLOYMENT_TARGET = 5.0;
PRODUCT_NAME = GCDWebServer;
PROVISIONING_PROFILE = "";
SDKROOT = iphoneos;

View File

@@ -0,0 +1,567 @@
/*
Copyright (c) 2012-2014, 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 <TargetConditionals.h>
#import "GCDWebServerRequest.h"
#import "GCDWebServerResponse.h"
/**
* 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* (^GCDWebServerMatchBlock)(NSString* requestMethod, NSURL* requestURL, NSDictionary* requestHeaders, NSString* urlPath, NSDictionary* 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* (^GCDWebServerProcessBlock)(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* response);
typedef void (^GCDWebServerAsyncProcessBlock)(GCDWebServerRequest* request, GCDWebServerCompletionBlock completionBlock);
/**
* 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 an empty string.
*/
extern NSString* const GCDWebServerOption_BonjourName;
/**
* 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;
/**
* 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;
#if TARGET_OS_IPHONE
/**
* 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
/**
* 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;
/**
* Delegate methods for GCDWebServer.
*
* @warning These methods are always called on the main thread in a serialized way.
*/
@protocol GCDWebServerDelegate <NSObject>
@optional
/**
* This method is called after the server has successfully started.
*/
- (void)webServerDidStart:(GCDWebServer*)server;
/**
* This method is called after the Bonjour registration for the server has
* successfully completed.
*/
- (void)webServerDidCompleteBonjourRegistration:(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;
@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
/**
* Sets the delegate for the server.
*/
@property(nonatomic, assign) id<GCDWebServerDelegate> delegate;
/**
* Returns YES if the server is currently running.
*/
@property(nonatomic, readonly, getter=isRunning) BOOL running;
/**
* 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) 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) NSString* bonjourType;
/**
* This method is the designated initializer for the class.
*/
- (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;
/**
* 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;
/**
* Starts the server with explicit options. This method is the designated way
* to start the server.
*
* Returns NO if the server failed to start and sets "error" argument if not NULL.
*/
- (BOOL)startWithOptions:(NSDictionary*)options error:(NSError**)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
@interface GCDWebServer (Extensions)
/**
* Returns the server's URL.
*
* @warning This property is only valid if the server is running.
*/
@property(nonatomic, readonly) 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) NSURL* bonjourServerURL;
/**
* 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:(NSString*)name;
#if !TARGET_OS_IPHONE
/**
* 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:(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:(NSDictionary*)options error:(NSError**)error;
#endif
@end
@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;
/**
* 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
@interface GCDWebServer (GETHandlers)
/**
* 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:(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:(NSString*)indexFilename cacheAge:(NSUInteger)cacheAge allowRangeRequests:(BOOL)allowRangeRequests;
@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
* - CocoaLumberjack: https://github.com/CocoaLumberjack/CocoaLumberjack
*
* For both the built-in logging facility and CocoaLumberjack, 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(...)
* GWS_LOG_EXCEPTION(__EXCEPTION__)
*
* IMPORTANT: Except for GWS_LOG_EXCEPTION() which gets passed an NSException,
* 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)
/**
* Sets the log level of the logging facility below which log messages are discarded.
*
* @warning The interpretation of the "level" argument depends on the logging
* facility used at compile time.
*/
+ (void)setLogLevel:(int)level;
/**
* 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);
/**
* Logs an exception to the logging facility at the EXCEPTION level.
*/
- (void)logException:(NSException*)exception;
@end
#ifdef __GCDWEBSERVER_ENABLE_TESTING__
@interface GCDWebServer (Testing)
/**
* 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:(NSDictionary*)options inDirectory:(NSString*)path;
@end
#endif

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,179 @@
/*
Copyright (c) 2012-2014, 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 "GCDWebServer.h"
@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
/**
* Returns the GCDWebServer that owns the connection.
*/
@property(nonatomic, readonly) GCDWebServer* server;
/**
* 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;
/**
* 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;
/**
* Returns the total number of bytes received from the remote peer (i.e. client)
* so far.
*/
@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;
@end
/**
* 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)
/**
* This method is called when the connection is opened.
*
* Return NO to reject the connection e.g. after validating the local
* 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*)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.
*/
- (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:(GCDWebServerRequest*)request withStatusCode:(NSInteger)statusCode;
/**
* Called when the connection is closed.
*/
- (void)close;
@end

View File

@@ -0,0 +1,845 @@
/*
Copyright (c) 2012-2014, 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.
*/
#if !__has_feature(objc_arc)
#error GCDWebServer requires ARC
#endif
#import <TargetConditionals.h>
#import <netdb.h>
#ifdef __GCDWEBSERVER_ENABLE_TESTING__
#import <libkern/OSAtomic.h>
#endif
#import "GCDWebServerPrivate.h"
#define kHeadersReadCapacity (1 * 1024)
#define kBodyReadCapacity (256 * 1024)
typedef void (^ReadDataCompletionBlock)(BOOL success);
typedef void (^ReadHeadersCompletionBlock)(NSData* extraData);
typedef void (^ReadBodyCompletionBlock)(BOOL success);
typedef void (^WriteDataCompletionBlock)(BOOL success);
typedef void (^WriteHeadersCompletionBlock)(BOOL success);
typedef void (^WriteBodyCompletionBlock)(BOOL success);
static NSData* _CRLFData = nil;
static NSData* _CRLFCRLFData = nil;
static NSData* _continueData = nil;
static NSData* _lastChunkData = nil;
static NSString* _digestAuthenticationNonce = nil;
#ifdef __GCDWEBSERVER_ENABLE_TESTING__
static int32_t _connectionCounter = 0;
#endif
@interface GCDWebServerConnection () {
@private
GCDWebServer* _server;
NSData* _localAddress;
NSData* _remoteAddress;
CFSocketNativeHandle _socket;
NSUInteger _bytesRead;
NSUInteger _bytesWritten;
BOOL _virtualHEAD;
CFHTTPMessageRef _requestMessage;
GCDWebServerRequest* _request;
GCDWebServerHandler* _handler;
CFHTTPMessageRef _responseMessage;
GCDWebServerResponse* _response;
NSInteger _statusCode;
BOOL _opened;
#ifdef __GCDWEBSERVER_ENABLE_TESTING__
NSUInteger _connectionIndex;
NSString* _requestPath;
int _requestFD;
NSString* _responsePath;
int _responseFD;
#endif
}
@end
@implementation GCDWebServerConnection (Read)
- (void)_readData:(NSMutableData*)data withLength:(NSUInteger)length completionBlock:(ReadDataCompletionBlock)block {
dispatch_read(_socket, length, kGCDWebServerGCDQueue, ^(dispatch_data_t buffer, int error) {
@autoreleasepool {
if (error == 0) {
size_t size = dispatch_data_get_size(buffer);
if (size > 0) {
NSUInteger originalLength = data.length;
dispatch_data_apply(buffer, ^bool(dispatch_data_t region, size_t chunkOffset, const void* chunkBytes, size_t chunkSize) {
[data appendBytes:chunkBytes length:chunkSize];
return true;
});
[self didReadBytes:((char*)data.bytes + originalLength) length:(data.length - originalLength)];
block(YES);
} else {
if (_bytesRead > 0) {
GWS_LOG_ERROR(@"No more data available on socket %i", _socket);
} else {
GWS_LOG_WARNING(@"No data received from socket %i", _socket);
}
block(NO);
}
} else {
GWS_LOG_ERROR(@"Error while reading from socket %i: %s (%i)", _socket, strerror(error), error);
block(NO);
}
}
});
}
- (void)_readHeaders:(NSMutableData*)headersData withCompletionBlock:(ReadHeadersCompletionBlock)block {
GWS_DCHECK(_requestMessage);
[self _readData:headersData withLength:NSUIntegerMax completionBlock:^(BOOL success) {
if (success) {
NSRange range = [headersData rangeOfData:_CRLFCRLFData options:0 range:NSMakeRange(0, headersData.length)];
if (range.location == NSNotFound) {
[self _readHeaders:headersData withCompletionBlock:block];
} else {
NSUInteger length = range.location + range.length;
if (CFHTTPMessageAppendBytes(_requestMessage, headersData.bytes, length)) {
if (CFHTTPMessageIsHeaderComplete(_requestMessage)) {
block([headersData subdataWithRange:NSMakeRange(length, headersData.length - length)]);
} else {
GWS_LOG_ERROR(@"Failed parsing request headers from socket %i", _socket);
block(nil);
}
} else {
GWS_LOG_ERROR(@"Failed appending request headers data from socket %i", _socket);
block(nil);
}
}
} else {
block(nil);
}
}];
}
- (void)_readBodyWithRemainingLength:(NSUInteger)length completionBlock:(ReadBodyCompletionBlock)block {
GWS_DCHECK([_request hasBody] && ![_request usesChunkedTransferEncoding]);
NSMutableData* bodyData = [[NSMutableData alloc] initWithCapacity:kBodyReadCapacity];
[self _readData:bodyData withLength:length completionBlock:^(BOOL success) {
if (success) {
if (bodyData.length <= length) {
NSError* error = nil;
if ([_request performWriteData:bodyData error:&error]) {
NSUInteger remainingLength = length - bodyData.length;
if (remainingLength) {
[self _readBodyWithRemainingLength:remainingLength completionBlock:block];
} else {
block(YES);
}
} else {
GWS_LOG_ERROR(@"Failed writing request body on socket %i: %@", _socket, error);
block(NO);
}
} else {
GWS_LOG_ERROR(@"Unexpected extra content reading request body on socket %i", _socket);
block(NO);
GWS_DNOT_REACHED();
}
} else {
block(NO);
}
}];
}
static inline NSUInteger _ScanHexNumber(const void* bytes, NSUInteger size) {
char buffer[size + 1];
bcopy(bytes, buffer, size);
buffer[size] = 0;
char* end = NULL;
long result = strtol(buffer, &end, 16);
return ((end != NULL) && (*end == 0) && (result >= 0) ? result : NSNotFound);
}
- (void)_readNextBodyChunk:(NSMutableData*)chunkData completionBlock:(ReadBodyCompletionBlock)block {
GWS_DCHECK([_request hasBody] && [_request usesChunkedTransferEncoding]);
while (1) {
NSRange range = [chunkData rangeOfData:_CRLFData options:0 range:NSMakeRange(0, chunkData.length)];
if (range.location == NSNotFound) {
break;
}
NSRange extensionRange = [chunkData rangeOfData:[NSData dataWithBytes:";" length:1] options:0 range:NSMakeRange(0, range.location)]; // Ignore chunk extensions
NSUInteger length = _ScanHexNumber((char*)chunkData.bytes, extensionRange.location != NSNotFound ? extensionRange.location : range.location);
if (length != NSNotFound) {
if (length) {
if (chunkData.length < range.location + range.length + length + 2) {
break;
}
const char* ptr = (char*)chunkData.bytes + range.location + range.length + length;
if ((*ptr == '\r') && (*(ptr + 1) == '\n')) {
NSError* error = nil;
if ([_request performWriteData:[chunkData subdataWithRange:NSMakeRange(range.location + range.length, length)] error:&error]) {
[chunkData replaceBytesInRange:NSMakeRange(0, range.location + range.length + length + 2) withBytes:NULL length:0];
} else {
GWS_LOG_ERROR(@"Failed writing request body on socket %i: %@", _socket, error);
block(NO);
return;
}
} else {
GWS_LOG_ERROR(@"Missing terminating CRLF sequence for chunk reading request body on socket %i", _socket);
block(NO);
return;
}
} else {
NSRange trailerRange = [chunkData rangeOfData:_CRLFCRLFData options:0 range:NSMakeRange(range.location, chunkData.length - range.location)]; // Ignore trailers
if (trailerRange.location != NSNotFound) {
block(YES);
return;
}
}
} else {
GWS_LOG_ERROR(@"Invalid chunk length reading request body on socket %i", _socket);
block(NO);
return;
}
}
[self _readData:chunkData withLength:NSUIntegerMax completionBlock:^(BOOL success) {
if (success) {
[self _readNextBodyChunk:chunkData completionBlock:block];
} else {
block(NO);
}
}];
}
@end
@implementation GCDWebServerConnection (Write)
- (void)_writeData:(NSData*)data withCompletionBlock:(WriteDataCompletionBlock)block {
dispatch_data_t buffer = dispatch_data_create(data.bytes, data.length, kGCDWebServerGCDQueue, ^{
[data self]; // Keeps ARC from releasing data too early
});
dispatch_write(_socket, buffer, kGCDWebServerGCDQueue, ^(dispatch_data_t remainingData, int error) {
@autoreleasepool {
if (error == 0) {
GWS_DCHECK(remainingData == NULL);
[self didWriteBytes:data.bytes length:data.length];
block(YES);
} else {
GWS_LOG_ERROR(@"Error while writing to socket %i: %s (%i)", _socket, strerror(error), error);
block(NO);
}
}
});
#if !OS_OBJECT_USE_OBJC_RETAIN_RELEASE
dispatch_release(buffer);
#endif
}
- (void)_writeHeadersWithCompletionBlock:(WriteHeadersCompletionBlock)block {
GWS_DCHECK(_responseMessage);
CFDataRef data = CFHTTPMessageCopySerializedMessage(_responseMessage);
[self _writeData:(__bridge NSData*)data withCompletionBlock:block];
CFRelease(data);
}
- (void)_writeBodyWithCompletionBlock:(WriteBodyCompletionBlock)block {
GWS_DCHECK([_response hasBody]);
[_response performReadDataWithCompletion:^(NSData* data, NSError* error) {
if (data) {
if (data.length) {
if (_response.usesChunkedTransferEncoding) {
const char* hexString = [[NSString stringWithFormat:@"%lx", (unsigned long)data.length] UTF8String];
size_t hexLength = strlen(hexString);
NSData* chunk = [NSMutableData dataWithLength:(hexLength + 2 + data.length + 2)];
if (chunk == nil) {
GWS_LOG_ERROR(@"Failed allocating memory for response body chunk for socket %i: %@", _socket, error);
block(NO);
return;
}
char* ptr = (char*)[(NSMutableData*)chunk mutableBytes];
bcopy(hexString, ptr, hexLength);
ptr += hexLength;
*ptr++ = '\r';
*ptr++ = '\n';
bcopy(data.bytes, ptr, data.length);
ptr += data.length;
*ptr++ = '\r';
*ptr = '\n';
data = chunk;
}
[self _writeData:data withCompletionBlock:^(BOOL success) {
if (success) {
[self _writeBodyWithCompletionBlock:block];
} else {
block(NO);
}
}];
} else {
if (_response.usesChunkedTransferEncoding) {
[self _writeData:_lastChunkData withCompletionBlock:^(BOOL success) {
block(success);
}];
} else {
block(YES);
}
}
} else {
GWS_LOG_ERROR(@"Failed reading response body for socket %i: %@", _socket, error);
block(NO);
}
}];
}
@end
@implementation GCDWebServerConnection
@synthesize server=_server, localAddressData=_localAddress, remoteAddressData=_remoteAddress, totalBytesRead=_bytesRead, totalBytesWritten=_bytesWritten;
+ (void)initialize {
if (_CRLFData == nil) {
_CRLFData = [[NSData alloc] initWithBytes:"\r\n" length:2];
GWS_DCHECK(_CRLFData);
}
if (_CRLFCRLFData == nil) {
_CRLFCRLFData = [[NSData alloc] initWithBytes:"\r\n\r\n" length:4];
GWS_DCHECK(_CRLFCRLFData);
}
if (_continueData == nil) {
CFHTTPMessageRef message = CFHTTPMessageCreateResponse(kCFAllocatorDefault, 100, NULL, kCFHTTPVersion1_1);
_continueData = CFBridgingRelease(CFHTTPMessageCopySerializedMessage(message));
CFRelease(message);
GWS_DCHECK(_continueData);
}
if (_lastChunkData == nil) {
_lastChunkData = [[NSData alloc] initWithBytes:"0\r\n\r\n" length:5];
}
if (_digestAuthenticationNonce == nil) {
CFUUIDRef uuid = CFUUIDCreate(kCFAllocatorDefault);
_digestAuthenticationNonce = GCDWebServerComputeMD5Digest(@"%@", CFBridgingRelease(CFUUIDCreateString(kCFAllocatorDefault, uuid)));
CFRelease(uuid);
}
}
- (BOOL)isUsingIPv6 {
const struct sockaddr* localSockAddr = _localAddress.bytes;
return (localSockAddr->sa_family == AF_INET6);
}
- (void)_initializeResponseHeadersWithStatusCode:(NSInteger)statusCode {
_statusCode = statusCode;
_responseMessage = CFHTTPMessageCreateResponse(kCFAllocatorDefault, statusCode, NULL, kCFHTTPVersion1_1);
CFHTTPMessageSetHeaderFieldValue(_responseMessage, CFSTR("Connection"), CFSTR("Close"));
CFHTTPMessageSetHeaderFieldValue(_responseMessage, CFSTR("Server"), (__bridge CFStringRef)_server.serverName);
CFHTTPMessageSetHeaderFieldValue(_responseMessage, CFSTR("Date"), (__bridge CFStringRef)GCDWebServerFormatRFC822([NSDate date]));
}
- (void)_startProcessingRequest {
GWS_DCHECK(_responseMessage == NULL);
GCDWebServerResponse* preflightResponse = [self preflightRequest:_request];
if (preflightResponse) {
[self _finishProcessingRequest:preflightResponse];
} else {
[self processRequest:_request completion:^(GCDWebServerResponse* processResponse) {
[self _finishProcessingRequest:processResponse];
}];
}
}
// http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html
- (void)_finishProcessingRequest:(GCDWebServerResponse*)response {
GWS_DCHECK(_responseMessage == NULL);
BOOL hasBody = NO;
if (response) {
response = [self overrideResponse:response forRequest:_request];
}
if (response) {
if ([response hasBody]) {
[response prepareForReading];
hasBody = !_virtualHEAD;
}
NSError* error = nil;
if (hasBody && ![response performOpen:&error]) {
GWS_LOG_ERROR(@"Failed opening response body for socket %i: %@", _socket, error);
} else {
_response = response;
}
}
if (_response) {
[self _initializeResponseHeadersWithStatusCode:_response.statusCode];
if (_response.lastModifiedDate) {
CFHTTPMessageSetHeaderFieldValue(_responseMessage, CFSTR("Last-Modified"), (__bridge CFStringRef)GCDWebServerFormatRFC822(_response.lastModifiedDate));
}
if (_response.eTag) {
CFHTTPMessageSetHeaderFieldValue(_responseMessage, CFSTR("ETag"), (__bridge CFStringRef)_response.eTag);
}
if ((_response.statusCode >= 200) && (_response.statusCode < 300)) {
if (_response.cacheControlMaxAge > 0) {
CFHTTPMessageSetHeaderFieldValue(_responseMessage, CFSTR("Cache-Control"), (__bridge CFStringRef)[NSString stringWithFormat:@"max-age=%i, public", (int)_response.cacheControlMaxAge]);
} else {
CFHTTPMessageSetHeaderFieldValue(_responseMessage, CFSTR("Cache-Control"), CFSTR("no-cache"));
}
}
if (_response.contentType != nil) {
CFHTTPMessageSetHeaderFieldValue(_responseMessage, CFSTR("Content-Type"), (__bridge CFStringRef)GCDWebServerNormalizeHeaderValue(_response.contentType));
}
if (_response.contentLength != NSUIntegerMax) {
CFHTTPMessageSetHeaderFieldValue(_responseMessage, CFSTR("Content-Length"), (__bridge CFStringRef)[NSString stringWithFormat:@"%lu", (unsigned long)_response.contentLength]);
}
if (_response.usesChunkedTransferEncoding) {
CFHTTPMessageSetHeaderFieldValue(_responseMessage, CFSTR("Transfer-Encoding"), CFSTR("chunked"));
}
[_response.additionalHeaders enumerateKeysAndObjectsUsingBlock:^(id key, id obj, BOOL* stop) {
CFHTTPMessageSetHeaderFieldValue(_responseMessage, (__bridge CFStringRef)key, (__bridge CFStringRef)obj);
}];
[self _writeHeadersWithCompletionBlock:^(BOOL success) {
if (success) {
if (hasBody) {
[self _writeBodyWithCompletionBlock:^(BOOL successInner) {
[_response performClose]; // TODO: There's nothing we can do on failure as headers have already been sent
}];
}
} else if (hasBody) {
[_response performClose];
}
}];
} else {
[self abortRequest:_request withStatusCode:kGCDWebServerHTTPStatusCode_InternalServerError];
}
}
- (void)_readBodyWithLength:(NSUInteger)length initialData:(NSData*)initialData {
NSError* error = nil;
if (![_request performOpen:&error]) {
GWS_LOG_ERROR(@"Failed opening request body for socket %i: %@", _socket, error);
[self abortRequest:_request withStatusCode:kGCDWebServerHTTPStatusCode_InternalServerError];
return;
}
if (initialData.length) {
if (![_request performWriteData:initialData error:&error]) {
GWS_LOG_ERROR(@"Failed writing request body on socket %i: %@", _socket, error);
if (![_request performClose:&error]) {
GWS_LOG_ERROR(@"Failed closing request body for socket %i: %@", _socket, error);
}
[self abortRequest:_request withStatusCode:kGCDWebServerHTTPStatusCode_InternalServerError];
return;
}
length -= initialData.length;
}
if (length) {
[self _readBodyWithRemainingLength:length completionBlock:^(BOOL success) {
NSError* localError = nil;
if ([_request performClose:&localError]) {
[self _startProcessingRequest];
} else {
GWS_LOG_ERROR(@"Failed closing request body for socket %i: %@", _socket, error);
[self abortRequest:_request withStatusCode:kGCDWebServerHTTPStatusCode_InternalServerError];
}
}];
} else {
if ([_request performClose:&error]) {
[self _startProcessingRequest];
} else {
GWS_LOG_ERROR(@"Failed closing request body for socket %i: %@", _socket, error);
[self abortRequest:_request withStatusCode:kGCDWebServerHTTPStatusCode_InternalServerError];
}
}
}
- (void)_readChunkedBodyWithInitialData:(NSData*)initialData {
NSError* error = nil;
if (![_request performOpen:&error]) {
GWS_LOG_ERROR(@"Failed opening request body for socket %i: %@", _socket, error);
[self abortRequest:_request withStatusCode:kGCDWebServerHTTPStatusCode_InternalServerError];
return;
}
NSMutableData* chunkData = [[NSMutableData alloc] initWithData:initialData];
[self _readNextBodyChunk:chunkData completionBlock:^(BOOL success) {
NSError* localError = nil;
if ([_request performClose:&localError]) {
[self _startProcessingRequest];
} else {
GWS_LOG_ERROR(@"Failed closing request body for socket %i: %@", _socket, error);
[self abortRequest:_request withStatusCode:kGCDWebServerHTTPStatusCode_InternalServerError];
}
}];
}
- (void)_readRequestHeaders {
_requestMessage = CFHTTPMessageCreateEmpty(kCFAllocatorDefault, true);
NSMutableData* headersData = [[NSMutableData alloc] initWithCapacity:kHeadersReadCapacity];
[self _readHeaders:headersData withCompletionBlock:^(NSData* extraData) {
if (extraData) {
NSString* requestMethod = CFBridgingRelease(CFHTTPMessageCopyRequestMethod(_requestMessage)); // Method verbs are case-sensitive and uppercase
if (_server.shouldAutomaticallyMapHEADToGET && [requestMethod isEqualToString:@"HEAD"]) {
requestMethod = @"GET";
_virtualHEAD = YES;
}
NSDictionary* requestHeaders = CFBridgingRelease(CFHTTPMessageCopyAllHeaderFields(_requestMessage)); // Header names are case-insensitive but CFHTTPMessageCopyAllHeaderFields() will standardize the common ones
NSURL* requestURL = CFBridgingRelease(CFHTTPMessageCopyRequestURL(_requestMessage));
if (requestURL) {
requestURL = [self rewriteRequestURL:requestURL withMethod:requestMethod headers:requestHeaders];
GWS_DCHECK(requestURL);
}
NSString* requestPath = requestURL ? GCDWebServerUnescapeURLString(CFBridgingRelease(CFURLCopyPath((CFURLRef)requestURL))) : nil; // Don't use -[NSURL path] which strips the ending slash
NSString* queryString = requestURL ? CFBridgingRelease(CFURLCopyQueryString((CFURLRef)requestURL, NULL)) : nil; // Don't use -[NSURL query] to make sure query is not unescaped;
NSDictionary* requestQuery = queryString ? GCDWebServerParseURLEncodedForm(queryString) : @{};
if (requestMethod && requestURL && requestHeaders && requestPath && requestQuery) {
for (_handler in _server.handlers) {
_request = _handler.matchBlock(requestMethod, requestURL, requestHeaders, requestPath, requestQuery);
if (_request) {
break;
}
}
if (_request) {
if ([_request hasBody]) {
[_request prepareForWriting];
if (_request.usesChunkedTransferEncoding || (extraData.length <= _request.contentLength)) {
NSString* expectHeader = [requestHeaders objectForKey:@"Expect"];
if (expectHeader) {
if ([expectHeader caseInsensitiveCompare:@"100-continue"] == NSOrderedSame) { // TODO: Actually validate request before continuing
[self _writeData:_continueData withCompletionBlock:^(BOOL success) {
if (success) {
if (_request.usesChunkedTransferEncoding) {
[self _readChunkedBodyWithInitialData:extraData];
} else {
[self _readBodyWithLength:_request.contentLength initialData:extraData];
}
}
}];
} else {
GWS_LOG_ERROR(@"Unsupported 'Expect' / 'Content-Length' header combination on socket %i", _socket);
[self abortRequest:_request withStatusCode:kGCDWebServerHTTPStatusCode_ExpectationFailed];
}
} else {
if (_request.usesChunkedTransferEncoding) {
[self _readChunkedBodyWithInitialData:extraData];
} else {
[self _readBodyWithLength:_request.contentLength initialData:extraData];
}
}
} else {
GWS_LOG_ERROR(@"Unexpected 'Content-Length' header value on socket %i", _socket);
[self abortRequest:_request withStatusCode:kGCDWebServerHTTPStatusCode_BadRequest];
}
} else {
[self _startProcessingRequest];
}
} else {
_request = [[GCDWebServerRequest alloc] initWithMethod:requestMethod url:requestURL headers:requestHeaders path:requestPath query:requestQuery];
GWS_DCHECK(_request);
[self abortRequest:_request withStatusCode:kGCDWebServerHTTPStatusCode_MethodNotAllowed];
}
} else {
[self abortRequest:nil withStatusCode:kGCDWebServerHTTPStatusCode_InternalServerError];
GWS_DNOT_REACHED();
}
} else {
[self abortRequest:nil withStatusCode:kGCDWebServerHTTPStatusCode_InternalServerError];
}
}];
}
- (id)initWithServer:(GCDWebServer*)server localAddress:(NSData*)localAddress remoteAddress:(NSData*)remoteAddress socket:(CFSocketNativeHandle)socket {
if ((self = [super init])) {
_server = server;
_localAddress = localAddress;
_remoteAddress = remoteAddress;
_socket = socket;
GWS_LOG_DEBUG(@"Did open connection on socket %i", _socket);
[_server willStartConnection:self];
if (![self open]) {
close(_socket);
return nil;
}
_opened = YES;
[self _readRequestHeaders];
}
return self;
}
- (NSString*)localAddressString {
return GCDWebServerStringFromSockAddr(_localAddress.bytes, YES);
}
- (NSString*)remoteAddressString {
return GCDWebServerStringFromSockAddr(_remoteAddress.bytes, YES);
}
- (void)dealloc {
int result = close(_socket);
if (result != 0) {
GWS_LOG_ERROR(@"Failed closing socket %i for connection: %s (%i)", _socket, strerror(errno), errno);
} else {
GWS_LOG_DEBUG(@"Did close connection on socket %i", _socket);
}
if (_opened) {
[self close];
}
[_server didEndConnection:self];
if (_requestMessage) {
CFRelease(_requestMessage);
}
if (_responseMessage) {
CFRelease(_responseMessage);
}
}
@end
@implementation GCDWebServerConnection (Subclassing)
- (BOOL)open {
#ifdef __GCDWEBSERVER_ENABLE_TESTING__
if (_server.recordingEnabled) {
_connectionIndex = OSAtomicIncrement32(&_connectionCounter);
_requestPath = [NSTemporaryDirectory() stringByAppendingPathComponent:[[NSProcessInfo processInfo] globallyUniqueString]];
_requestFD = open([_requestPath fileSystemRepresentation], O_CREAT | O_TRUNC | O_WRONLY, S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH);
GWS_DCHECK(_requestFD > 0);
_responsePath = [NSTemporaryDirectory() stringByAppendingPathComponent:[[NSProcessInfo processInfo] globallyUniqueString]];
_responseFD = open([_responsePath fileSystemRepresentation], O_CREAT | O_TRUNC | O_WRONLY, S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH);
GWS_DCHECK(_responseFD > 0);
}
#endif
return YES;
}
- (void)didReadBytes:(const void*)bytes length:(NSUInteger)length {
GWS_LOG_DEBUG(@"Connection received %lu bytes on socket %i", (unsigned long)length, _socket);
_bytesRead += length;
#ifdef __GCDWEBSERVER_ENABLE_TESTING__
if ((_requestFD > 0) && (write(_requestFD, bytes, length) != (ssize_t)length)) {
GWS_LOG_ERROR(@"Failed recording request data: %s (%i)", strerror(errno), errno);
close(_requestFD);
_requestFD = 0;
}
#endif
}
- (void)didWriteBytes:(const void*)bytes length:(NSUInteger)length {
GWS_LOG_DEBUG(@"Connection sent %lu bytes on socket %i", (unsigned long)length, _socket);
_bytesWritten += length;
#ifdef __GCDWEBSERVER_ENABLE_TESTING__
if ((_responseFD > 0) && (write(_responseFD, bytes, length) != (ssize_t)length)) {
GWS_LOG_ERROR(@"Failed recording response data: %s (%i)", strerror(errno), errno);
close(_responseFD);
_responseFD = 0;
}
#endif
}
- (NSURL*)rewriteRequestURL:(NSURL*)url withMethod:(NSString*)method headers:(NSDictionary*)headers {
return url;
}
// https://tools.ietf.org/html/rfc2617
- (GCDWebServerResponse*)preflightRequest:(GCDWebServerRequest*)request {
GWS_LOG_DEBUG(@"Connection on socket %i preflighting request \"%@ %@\" with %lu bytes body", _socket, _virtualHEAD ? @"HEAD" : _request.method, _request.path, (unsigned long)_bytesRead);
GCDWebServerResponse* response = nil;
if (_server.authenticationBasicAccounts) {
__block BOOL authenticated = NO;
NSString* authorizationHeader = [request.headers objectForKey:@"Authorization"];
if ([authorizationHeader hasPrefix:@"Basic "]) {
NSString* basicAccount = [authorizationHeader substringFromIndex:6];
[_server.authenticationBasicAccounts enumerateKeysAndObjectsUsingBlock:^(NSString* username, NSString* digest, BOOL* stop) {
if ([basicAccount isEqualToString:digest]) {
authenticated = YES;
*stop = YES;
}
}];
}
if (!authenticated) {
response = [GCDWebServerResponse responseWithStatusCode:kGCDWebServerHTTPStatusCode_Unauthorized];
[response setValue:[NSString stringWithFormat:@"Basic realm=\"%@\"", _server.authenticationRealm] forAdditionalHeader:@"WWW-Authenticate"];
}
} else if (_server.authenticationDigestAccounts) {
BOOL authenticated = NO;
BOOL isStaled = NO;
NSString* authorizationHeader = [request.headers objectForKey:@"Authorization"];
if ([authorizationHeader hasPrefix:@"Digest "]) {
NSString* realm = GCDWebServerExtractHeaderValueParameter(authorizationHeader, @"realm");
if ([realm isEqualToString:_server.authenticationRealm]) {
NSString* nonce = GCDWebServerExtractHeaderValueParameter(authorizationHeader, @"nonce");
if ([nonce isEqualToString:_digestAuthenticationNonce]) {
NSString* username = GCDWebServerExtractHeaderValueParameter(authorizationHeader, @"username");
NSString* uri = GCDWebServerExtractHeaderValueParameter(authorizationHeader, @"uri");
NSString* actualResponse = GCDWebServerExtractHeaderValueParameter(authorizationHeader, @"response");
NSString* ha1 = [_server.authenticationDigestAccounts objectForKey:username];
NSString* ha2 = GCDWebServerComputeMD5Digest(@"%@:%@", request.method, uri); // We cannot use "request.path" as the query string is required
NSString* expectedResponse = GCDWebServerComputeMD5Digest(@"%@:%@:%@", ha1, _digestAuthenticationNonce, ha2);
if ([actualResponse isEqualToString:expectedResponse]) {
authenticated = YES;
}
} else if (nonce.length) {
isStaled = YES;
}
}
}
if (!authenticated) {
response = [GCDWebServerResponse responseWithStatusCode:kGCDWebServerHTTPStatusCode_Unauthorized];
[response setValue:[NSString stringWithFormat:@"Digest realm=\"%@\", nonce=\"%@\"%@", _server.authenticationRealm, _digestAuthenticationNonce, isStaled ? @", stale=TRUE" : @""] forAdditionalHeader:@"WWW-Authenticate"]; // TODO: Support Quality of Protection ("qop")
}
}
return response;
}
- (void)processRequest:(GCDWebServerRequest*)request completion:(GCDWebServerCompletionBlock)completion {
GWS_LOG_DEBUG(@"Connection on socket %i processing request \"%@ %@\" with %lu bytes body", _socket, _virtualHEAD ? @"HEAD" : _request.method, _request.path, (unsigned long)_bytesRead);
@try {
_handler.asyncProcessBlock(request, completion);
}
@catch (NSException* exception) {
GWS_LOG_EXCEPTION(exception);
}
}
// http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.26
static inline BOOL _CompareResources(NSString* responseETag, NSString* requestETag, NSDate* responseLastModified, NSDate* requestLastModified) {
if ([requestETag isEqualToString:@"*"] && (!responseLastModified || !requestLastModified || ([responseLastModified compare:requestLastModified] != NSOrderedDescending))) {
return YES;
} else {
if ([responseETag isEqualToString:requestETag]) {
return YES;
}
if (responseLastModified && requestLastModified && ([responseLastModified compare:requestLastModified] != NSOrderedDescending)) {
return YES;
}
}
return NO;
}
- (GCDWebServerResponse*)overrideResponse:(GCDWebServerResponse*)response forRequest:(GCDWebServerRequest*)request {
if ((response.statusCode >= 200) && (response.statusCode < 300) && _CompareResources(response.eTag, request.ifNoneMatch, response.lastModifiedDate, request.ifModifiedSince)) {
NSInteger code = [request.method isEqualToString:@"HEAD"] || [request.method isEqualToString:@"GET"] ? kGCDWebServerHTTPStatusCode_NotModified : kGCDWebServerHTTPStatusCode_PreconditionFailed;
GCDWebServerResponse* newResponse = [GCDWebServerResponse responseWithStatusCode:code];
newResponse.cacheControlMaxAge = response.cacheControlMaxAge;
newResponse.lastModifiedDate = response.lastModifiedDate;
newResponse.eTag = response.eTag;
GWS_DCHECK(newResponse);
return newResponse;
}
return response;
}
- (void)abortRequest:(GCDWebServerRequest*)request withStatusCode:(NSInteger)statusCode {
GWS_DCHECK(_responseMessage == NULL);
GWS_DCHECK((statusCode >= 400) && (statusCode < 600));
[self _initializeResponseHeadersWithStatusCode:statusCode];
[self _writeHeadersWithCompletionBlock:^(BOOL success) {
; // Nothing more to do
}];
GWS_LOG_DEBUG(@"Connection aborted with status code %i on socket %i", (int)statusCode, _socket);
}
- (void)close {
#ifdef __GCDWEBSERVER_ENABLE_TESTING__
if (_requestPath) {
BOOL success = NO;
NSError* error = nil;
if (_requestFD > 0) {
close(_requestFD);
NSString* name = [NSString stringWithFormat:@"%03lu-%@.request", (unsigned long)_connectionIndex, _virtualHEAD ? @"HEAD" : _request.method];
success = [[NSFileManager defaultManager] moveItemAtPath:_requestPath toPath:[[[NSFileManager defaultManager] currentDirectoryPath] stringByAppendingPathComponent:name] error:&error];
}
if (!success) {
GWS_LOG_ERROR(@"Failed saving recorded request: %@", error);
GWS_DNOT_REACHED();
}
unlink([_requestPath fileSystemRepresentation]);
}
if (_responsePath) {
BOOL success = NO;
NSError* error = nil;
if (_responseFD > 0) {
close(_responseFD);
NSString* name = [NSString stringWithFormat:@"%03lu-%i.response", (unsigned long)_connectionIndex, (int)_statusCode];
success = [[NSFileManager defaultManager] moveItemAtPath:_responsePath toPath:[[[NSFileManager defaultManager] currentDirectoryPath] stringByAppendingPathComponent:name] error:&error];
}
if (!success) {
GWS_LOG_ERROR(@"Failed saving recorded response: %@", error);
GWS_DNOT_REACHED();
}
unlink([_responsePath fileSystemRepresentation]);
}
#endif
if (_request) {
GWS_LOG_VERBOSE(@"[%@] %@ %i \"%@ %@\" (%lu | %lu)", self.localAddressString, self.remoteAddressString, (int)_statusCode, _virtualHEAD ? @"HEAD" : _request.method, _request.path, (unsigned long)_bytesRead, (unsigned long)_bytesWritten);
} else {
GWS_LOG_VERBOSE(@"[%@] %@ %i \"(invalid request)\" (%lu | %lu)", self.localAddressString, self.remoteAddressString, (int)_statusCode, (unsigned long)_bytesRead, (unsigned long)_bytesWritten);
}
}
@end

View File

@@ -0,0 +1,101 @@
/*
Copyright (c) 2012-2014, 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 <Foundation/Foundation.h>
#ifdef __cplusplus
extern "C" {
#endif
/**
* Converts a file extension to the corresponding MIME type.
* If there is no match, "application/octet-stream" is returned.
*/
NSString* GCDWebServerGetMimeTypeForExtension(NSString* extension);
/**
* 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* GCDWebServerEscapeURLString(NSString* string);
/**
* Unescapes a URL percent-encoded string.
*/
NSString* 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* 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* 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);
/**
* 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* 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);
/**
* 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* GCDWebServerParseISO8601(NSString* string);
#ifdef __cplusplus
}
#endif

View File

@@ -0,0 +1,299 @@
/*
Copyright (c) 2012-2014, 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.
*/
#if !__has_feature(objc_arc)
#error GCDWebServer requires ARC
#endif
#import <TargetConditionals.h>
#if TARGET_OS_IPHONE
#import <MobileCoreServices/MobileCoreServices.h>
#else
#import <SystemConfiguration/SystemConfiguration.h>
#endif
#import <CommonCrypto/CommonDigest.h>
#import <ifaddrs.h>
#import <net/if.h>
#import <netdb.h>
#import "GCDWebServerPrivate.h"
static NSDateFormatter* _dateFormatterRFC822 = nil;
static NSDateFormatter* _dateFormatterISO8601 = nil;
static dispatch_queue_t _dateFormatterQueue = NULL;
// TODO: Handle RFC 850 and ANSI C's asctime() format
void GCDWebServerInitializeFunctions() {
GWS_DCHECK([NSThread isMainThread]); // NSDateFormatter should be initialized on main thread
if (_dateFormatterRFC822 == nil) {
_dateFormatterRFC822 = [[NSDateFormatter alloc] init];
_dateFormatterRFC822.timeZone = [NSTimeZone timeZoneWithAbbreviation:@"GMT"];
_dateFormatterRFC822.dateFormat = @"EEE',' dd MMM yyyy HH':'mm':'ss 'GMT'";
_dateFormatterRFC822.locale = [[NSLocale alloc] initWithLocaleIdentifier:@"en_US"];
GWS_DCHECK(_dateFormatterRFC822);
}
if (_dateFormatterISO8601 == nil) {
_dateFormatterISO8601 = [[NSDateFormatter alloc] init];
_dateFormatterISO8601.timeZone = [NSTimeZone timeZoneWithAbbreviation:@"GMT"];
_dateFormatterISO8601.dateFormat = @"yyyy-MM-dd'T'HH:mm:ss'+00:00'";
_dateFormatterISO8601.locale = [[NSLocale alloc] initWithLocaleIdentifier:@"en_US"];
GWS_DCHECK(_dateFormatterISO8601);
}
if (_dateFormatterQueue == NULL) {
_dateFormatterQueue = dispatch_queue_create(NULL, DISPATCH_QUEUE_SERIAL);
GWS_DCHECK(_dateFormatterQueue);
}
}
NSString* GCDWebServerNormalizeHeaderValue(NSString* value) {
if (value) {
NSRange range = [value rangeOfString:@";"]; // Assume part before ";" separator is case-insensitive
if (range.location != NSNotFound) {
value = [[[value substringToIndex:range.location] lowercaseString] stringByAppendingString:[value substringFromIndex:range.location]];
} else {
value = [value lowercaseString];
}
}
return value;
}
NSString* GCDWebServerTruncateHeaderValue(NSString* value) {
NSRange range = [value rangeOfString:@";"];
return range.location != NSNotFound ? [value substringToIndex:range.location] : value;
}
NSString* GCDWebServerExtractHeaderValueParameter(NSString* value, NSString* name) {
NSString* parameter = nil;
NSScanner* scanner = [[NSScanner alloc] initWithString:value];
[scanner setCaseSensitive:NO]; // Assume parameter names are case-insensitive
NSString* string = [NSString stringWithFormat:@"%@=", name];
if ([scanner scanUpToString:string intoString:NULL]) {
[scanner scanString:string intoString:NULL];
if ([scanner scanString:@"\"" intoString:NULL]) {
[scanner scanUpToString:@"\"" intoString:&parameter];
} else {
[scanner scanUpToCharactersFromSet:[NSCharacterSet whitespaceCharacterSet] intoString:&parameter];
}
}
return parameter;
}
// http://www.w3schools.com/tags/ref_charactersets.asp
NSStringEncoding GCDWebServerStringEncodingFromCharset(NSString* charset) {
NSStringEncoding encoding = kCFStringEncodingInvalidId;
if (charset) {
encoding = CFStringConvertEncodingToNSStringEncoding(CFStringConvertIANACharSetNameToEncoding((CFStringRef)charset));
}
return (encoding != kCFStringEncodingInvalidId ? encoding : NSUTF8StringEncoding);
}
NSString* GCDWebServerFormatRFC822(NSDate* date) {
__block NSString* string;
dispatch_sync(_dateFormatterQueue, ^{
string = [_dateFormatterRFC822 stringFromDate:date];
});
return string;
}
NSDate* GCDWebServerParseRFC822(NSString* string) {
__block NSDate* date;
dispatch_sync(_dateFormatterQueue, ^{
date = [_dateFormatterRFC822 dateFromString:string];
});
return date;
}
NSString* GCDWebServerFormatISO8601(NSDate* date) {
__block NSString* string;
dispatch_sync(_dateFormatterQueue, ^{
string = [_dateFormatterISO8601 stringFromDate:date];
});
return string;
}
NSDate* GCDWebServerParseISO8601(NSString* string) {
__block NSDate* date;
dispatch_sync(_dateFormatterQueue, ^{
date = [_dateFormatterISO8601 dateFromString:string];
});
return date;
}
BOOL GCDWebServerIsTextContentType(NSString* type) {
return ([type hasPrefix:@"text/"] || [type hasPrefix:@"application/json"] || [type hasPrefix:@"application/xml"]);
}
NSString* GCDWebServerDescribeData(NSData* data, NSString* type) {
if (GCDWebServerIsTextContentType(type)) {
NSString* charset = GCDWebServerExtractHeaderValueParameter(type, @"charset");
NSString* string = [[NSString alloc] initWithData:data encoding:GCDWebServerStringEncodingFromCharset(charset)];
if (string) {
return string;
}
}
return [NSString stringWithFormat:@"<%lu bytes>", (unsigned long)data.length];
}
NSString* GCDWebServerGetMimeTypeForExtension(NSString* extension) {
static NSDictionary* _overrides = nil;
if (_overrides == nil) {
_overrides = [[NSDictionary alloc] initWithObjectsAndKeys:
@"text/css", @"css",
nil];
}
NSString* mimeType = nil;
extension = [extension lowercaseString];
if (extension.length) {
mimeType = [_overrides objectForKey:extension];
if (mimeType == nil) {
CFStringRef uti = UTTypeCreatePreferredIdentifierForTag(kUTTagClassFilenameExtension, (__bridge CFStringRef)extension, NULL);
if (uti) {
mimeType = CFBridgingRelease(UTTypeCopyPreferredTagWithClass(uti, kUTTagClassMIMEType));
CFRelease(uti);
}
}
}
return mimeType ? mimeType : kGCDWebServerDefaultMimeType;
}
NSString* GCDWebServerEscapeURLString(NSString* string) {
return CFBridgingRelease(CFURLCreateStringByAddingPercentEscapes(kCFAllocatorDefault, (CFStringRef)string, NULL, CFSTR(":@/?&=+"), kCFStringEncodingUTF8));
}
NSString* GCDWebServerUnescapeURLString(NSString* string) {
return CFBridgingRelease(CFURLCreateStringByReplacingPercentEscapesUsingEncoding(kCFAllocatorDefault, (CFStringRef)string, CFSTR(""), kCFStringEncodingUTF8));
}
NSDictionary* GCDWebServerParseURLEncodedForm(NSString* form) {
NSMutableDictionary* parameters = [NSMutableDictionary dictionary];
NSScanner* scanner = [[NSScanner alloc] initWithString:form];
[scanner setCharactersToBeSkipped:nil];
while (1) {
NSString* key = nil;
if (![scanner scanUpToString:@"=" intoString:&key] || [scanner isAtEnd]) {
break;
}
[scanner setScanLocation:([scanner scanLocation] + 1)];
NSString* value = nil;
[scanner scanUpToString:@"&" intoString:&value];
if (value == nil) {
value = @"";
}
key = [key stringByReplacingOccurrencesOfString:@"+" withString:@" "];
NSString* unescapedKey = key ? GCDWebServerUnescapeURLString(key) : nil;
value = [value stringByReplacingOccurrencesOfString:@"+" withString:@" "];
NSString* unescapedValue = value ? GCDWebServerUnescapeURLString(value) : nil;
if (unescapedKey && unescapedValue) {
[parameters setObject:unescapedValue forKey:unescapedKey];
} else {
GWS_LOG_WARNING(@"Failed parsing URL encoded form for key \"%@\" and value \"%@\"", key, value);
GWS_DNOT_REACHED();
}
if ([scanner isAtEnd]) {
break;
}
[scanner setScanLocation:([scanner scanLocation] + 1)];
}
return parameters;
}
NSString* GCDWebServerStringFromSockAddr(const struct sockaddr* addr, BOOL includeService) {
NSString* string = nil;
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) {
string = includeService ? [NSString stringWithFormat:@"%s:%s", hostBuffer, serviceBuffer] : [NSString stringWithUTF8String:hostBuffer];
} else {
GWS_DNOT_REACHED();
}
return string;
}
NSString* GCDWebServerGetPrimaryIPAddress(BOOL useIPv6) {
NSString* address = nil;
#if TARGET_OS_IPHONE
#if !TARGET_IPHONE_SIMULATOR
const char* primaryInterface = "en0"; // WiFi interface on iOS
#endif
#else
const char* primaryInterface = NULL;
SCDynamicStoreRef store = SCDynamicStoreCreate(kCFAllocatorDefault, CFSTR("GCDWebServer"), NULL, NULL);
if (store) {
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) {
primaryInterface = [[NSString stringWithString:[(__bridge NSDictionary*)info objectForKey:@"PrimaryInterface"]] UTF8String];
CFRelease(info);
}
CFRelease(store);
}
if (primaryInterface == NULL) {
primaryInterface = "lo0";
}
#endif
struct ifaddrs* list;
if (getifaddrs(&list) >= 0) {
for (struct ifaddrs* ifap = list; ifap; ifap = ifap->ifa_next) {
#if TARGET_IPHONE_SIMULATOR
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
#else
if (strcmp(ifap->ifa_name, primaryInterface))
#endif
{
continue;
}
if ((ifap->ifa_flags & IFF_UP) && ((!useIPv6 && (ifap->ifa_addr->sa_family == AF_INET)) || (useIPv6 && (ifap->ifa_addr->sa_family == AF_INET6)))) {
address = GCDWebServerStringFromSockAddr(ifap->ifa_addr, NO);
break;
}
}
freeifaddrs(list);
}
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];
CC_MD5(string, (CC_LONG)strlen(string), md5);
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 stringWithUTF8String:buffer];
}

View File

@@ -0,0 +1,116 @@
/*
Copyright (c) 2012-2014, 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.
*/
// http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html
// http://www.iana.org/assignments/http-status-codes/http-status-codes.xhtml
#import <Foundation/Foundation.h>
/**
* Convenience constants for "informational" HTTP status codes.
*/
typedef NS_ENUM(NSInteger, GCDWebServerInformationalHTTPStatusCode) {
kGCDWebServerHTTPStatusCode_Continue = 100,
kGCDWebServerHTTPStatusCode_SwitchingProtocols = 101,
kGCDWebServerHTTPStatusCode_Processing = 102
};
/**
* Convenience constants for "successful" HTTP status codes.
*/
typedef NS_ENUM(NSInteger, GCDWebServerSuccessfulHTTPStatusCode) {
kGCDWebServerHTTPStatusCode_OK = 200,
kGCDWebServerHTTPStatusCode_Created = 201,
kGCDWebServerHTTPStatusCode_Accepted = 202,
kGCDWebServerHTTPStatusCode_NonAuthoritativeInformation = 203,
kGCDWebServerHTTPStatusCode_NoContent = 204,
kGCDWebServerHTTPStatusCode_ResetContent = 205,
kGCDWebServerHTTPStatusCode_PartialContent = 206,
kGCDWebServerHTTPStatusCode_MultiStatus = 207,
kGCDWebServerHTTPStatusCode_AlreadyReported = 208
};
/**
* Convenience constants for "redirection" HTTP status codes.
*/
typedef NS_ENUM(NSInteger, GCDWebServerRedirectionHTTPStatusCode) {
kGCDWebServerHTTPStatusCode_MultipleChoices = 300,
kGCDWebServerHTTPStatusCode_MovedPermanently = 301,
kGCDWebServerHTTPStatusCode_Found = 302,
kGCDWebServerHTTPStatusCode_SeeOther = 303,
kGCDWebServerHTTPStatusCode_NotModified = 304,
kGCDWebServerHTTPStatusCode_UseProxy = 305,
kGCDWebServerHTTPStatusCode_TemporaryRedirect = 307,
kGCDWebServerHTTPStatusCode_PermanentRedirect = 308
};
/**
* Convenience constants for "client error" HTTP status codes.
*/
typedef NS_ENUM(NSInteger, GCDWebServerClientErrorHTTPStatusCode) {
kGCDWebServerHTTPStatusCode_BadRequest = 400,
kGCDWebServerHTTPStatusCode_Unauthorized = 401,
kGCDWebServerHTTPStatusCode_PaymentRequired = 402,
kGCDWebServerHTTPStatusCode_Forbidden = 403,
kGCDWebServerHTTPStatusCode_NotFound = 404,
kGCDWebServerHTTPStatusCode_MethodNotAllowed = 405,
kGCDWebServerHTTPStatusCode_NotAcceptable = 406,
kGCDWebServerHTTPStatusCode_ProxyAuthenticationRequired = 407,
kGCDWebServerHTTPStatusCode_RequestTimeout = 408,
kGCDWebServerHTTPStatusCode_Conflict = 409,
kGCDWebServerHTTPStatusCode_Gone = 410,
kGCDWebServerHTTPStatusCode_LengthRequired = 411,
kGCDWebServerHTTPStatusCode_PreconditionFailed = 412,
kGCDWebServerHTTPStatusCode_RequestEntityTooLarge = 413,
kGCDWebServerHTTPStatusCode_RequestURITooLong = 414,
kGCDWebServerHTTPStatusCode_UnsupportedMediaType = 415,
kGCDWebServerHTTPStatusCode_RequestedRangeNotSatisfiable = 416,
kGCDWebServerHTTPStatusCode_ExpectationFailed = 417,
kGCDWebServerHTTPStatusCode_UnprocessableEntity = 422,
kGCDWebServerHTTPStatusCode_Locked = 423,
kGCDWebServerHTTPStatusCode_FailedDependency = 424,
kGCDWebServerHTTPStatusCode_UpgradeRequired = 426,
kGCDWebServerHTTPStatusCode_PreconditionRequired = 428,
kGCDWebServerHTTPStatusCode_TooManyRequests = 429,
kGCDWebServerHTTPStatusCode_RequestHeaderFieldsTooLarge = 431
};
/**
* Convenience constants for "server error" HTTP status codes.
*/
typedef NS_ENUM(NSInteger, GCDWebServerServerErrorHTTPStatusCode) {
kGCDWebServerHTTPStatusCode_InternalServerError = 500,
kGCDWebServerHTTPStatusCode_NotImplemented = 501,
kGCDWebServerHTTPStatusCode_BadGateway = 502,
kGCDWebServerHTTPStatusCode_ServiceUnavailable = 503,
kGCDWebServerHTTPStatusCode_GatewayTimeout = 504,
kGCDWebServerHTTPStatusCode_HTTPVersionNotSupported = 505,
kGCDWebServerHTTPStatusCode_InsufficientStorage = 507,
kGCDWebServerHTTPStatusCode_LoopDetected = 508,
kGCDWebServerHTTPStatusCode_NotExtended = 510,
kGCDWebServerHTTPStatusCode_NetworkAuthenticationRequired = 511
};

View File

@@ -0,0 +1,228 @@
/*
Copyright (c) 2012-2014, 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 <os/object.h>
#import <sys/socket.h>
/**
* All GCDWebServer headers.
*/
#import "GCDWebServerHTTPStatusCodes.h"
#import "GCDWebServerFunctions.h"
#import "GCDWebServer.h"
#import "GCDWebServerConnection.h"
#import "GCDWebServerDataRequest.h"
#import "GCDWebServerFileRequest.h"
#import "GCDWebServerMultiPartFormRequest.h"
#import "GCDWebServerURLEncodedFormRequest.h"
#import "GCDWebServerDataResponse.h"
#import "GCDWebServerErrorResponse.h"
#import "GCDWebServerFileResponse.h"
#import "GCDWebServerStreamedResponse.h"
/**
* Automatically detect if XLFacility is available and if so use it as a
* logging facility.
*/
#if defined(__has_include) && __has_include("XLFacilityMacros.h")
#define __GCDWEBSERVER_LOGGING_FACILITY_XLFACILITY__
#undef XLOG_TAG
#define XLOG_TAG @"gcdwebserver.internal"
#import "XLFacilityMacros.h"
#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_LOG_EXCEPTION(__EXCEPTION__) XLOG_EXCEPTION(__EXCEPTION__)
#define GWS_DCHECK(__CONDITION__) XLOG_DEBUG_CHECK(__CONDITION__)
#define GWS_DNOT_REACHED() XLOG_DEBUG_UNREACHABLE()
/**
* Automatically detect if CocoaLumberJack is available and if so use
* it as a logging facility.
*/
#elif defined(__has_include) && __has_include("DDLogMacros.h")
#import "DDLogMacros.h"
#define __GCDWEBSERVER_LOGGING_FACILITY_COCOALUMBERJACK__
#undef LOG_LEVEL_DEF
#define LOG_LEVEL_DEF GCDWebServerLogLevel
extern int GCDWebServerLogLevel;
#define GWS_LOG_DEBUG(...) DDLogDebug(__VA_ARGS__)
#define GWS_LOG_VERBOSE(...) DDLogVerbose(__VA_ARGS__)
#define GWS_LOG_INFO(...) DDLogInfo(__VA_ARGS__)
#define GWS_LOG_WARNING(...) DDLogWarn(__VA_ARGS__)
#define GWS_LOG_ERROR(...) DDLogError(__VA_ARGS__)
#define GWS_LOG_EXCEPTION(__EXCEPTION__) DDLogError(@"%@", __EXCEPTION__)
/**
* Check if a custom logging facility should be used instead.
*/
#elif defined(__GCDWEBSERVER_LOGGING_HEADER__)
#define __GCDWEBSERVER_LOGGING_FACILITY_CUSTOM__
#import __GCDWEBSERVER_LOGGING_HEADER__
/**
* If all of the above fail, then use GCDWebServer built-in
* logging facility.
*/
#else
#define __GCDWEBSERVER_LOGGING_FACILITY_BUILTIN__
typedef NS_ENUM(int, GCDWebServerLoggingLevel) {
kGCDWebServerLoggingLevel_Debug = 0,
kGCDWebServerLoggingLevel_Verbose,
kGCDWebServerLoggingLevel_Info,
kGCDWebServerLoggingLevel_Warning,
kGCDWebServerLoggingLevel_Error,
kGCDWebServerLoggingLevel_Exception,
};
extern GCDWebServerLoggingLevel GCDWebServerLogLevel;
extern void GCDWebServerLogMessage(GCDWebServerLoggingLevel level, NSString* format, ...) NS_FORMAT_FUNCTION(2, 3);
#if DEBUG
#define GWS_LOG_DEBUG(...) do { if (GCDWebServerLogLevel <= kGCDWebServerLoggingLevel_Debug) GCDWebServerLogMessage(kGCDWebServerLoggingLevel_Debug, __VA_ARGS__); } while (0)
#else
#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)
#define GWS_LOG_EXCEPTION(__EXCEPTION__) do { if (GCDWebServerLogLevel <= kGCDWebServerLoggingLevel_Exception) GCDWebServerLogMessage(kGCDWebServerLoggingLevel_Exception, @"%@", __EXCEPTION__); } 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
/**
* GCDWebServer internal constants and APIs.
*/
#define kGCDWebServerDefaultMimeType @"application/octet-stream"
#define kGCDWebServerGCDQueue dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)
#define kGCDWebServerErrorDomain @"GCDWebServerErrorDomain"
static inline BOOL GCDWebServerIsValidByteRange(NSRange range) {
return ((range.location != NSUIntegerMax) || (range.length > 0));
}
static inline NSError* GCDWebServerMakePosixError(int code) {
return [NSError errorWithDomain:NSPOSIXErrorDomain code:code userInfo:@{NSLocalizedDescriptionKey: [NSString stringWithUTF8String:strerror(code)]}];
}
extern void GCDWebServerInitializeFunctions();
extern NSString* GCDWebServerNormalizeHeaderValue(NSString* value);
extern NSString* GCDWebServerTruncateHeaderValue(NSString* value);
extern NSString* GCDWebServerExtractHeaderValueParameter(NSString* header, NSString* attribute);
extern NSStringEncoding GCDWebServerStringEncodingFromCharset(NSString* charset);
extern BOOL GCDWebServerIsTextContentType(NSString* type);
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 ()
- (id)initWithServer:(GCDWebServer*)server localAddress:(NSData*)localAddress remoteAddress:(NSData*)remoteAddress socket:(CFSocketNativeHandle)socket;
@end
@interface GCDWebServer ()
@property(nonatomic, readonly) NSArray* handlers;
@property(nonatomic, readonly) NSString* serverName;
@property(nonatomic, readonly) NSString* authenticationRealm;
@property(nonatomic, readonly) NSDictionary* authenticationBasicAccounts;
@property(nonatomic, readonly) NSDictionary* authenticationDigestAccounts;
@property(nonatomic, readonly) BOOL shouldAutomaticallyMapHEADToGET;
- (void)willStartConnection:(GCDWebServerConnection*)connection;
- (void)didEndConnection:(GCDWebServerConnection*)connection;
@end
@interface GCDWebServerHandler : NSObject
@property(nonatomic, readonly) GCDWebServerMatchBlock matchBlock;
@property(nonatomic, readonly) GCDWebServerAsyncProcessBlock asyncProcessBlock;
@end
@interface GCDWebServerRequest ()
@property(nonatomic, readonly) BOOL usesChunkedTransferEncoding;
- (void)prepareForWriting;
- (BOOL)performOpen:(NSError**)error;
- (BOOL)performWriteData:(NSData*)data error:(NSError**)error;
- (BOOL)performClose:(NSError**)error;
- (void)setAttribute:(id)attribute forKey:(NSString*)key;
@end
@interface GCDWebServerResponse ()
@property(nonatomic, readonly) NSDictionary* additionalHeaders;
@property(nonatomic, readonly) BOOL usesChunkedTransferEncoding;
- (void)prepareForReading;
- (BOOL)performOpen:(NSError**)error;
- (void)performReadDataWithCompletion:(GCDWebServerBodyReaderCompletionBlock)block;
- (void)performClose;
@end

View File

@@ -0,0 +1,182 @@
/*
Copyright (c) 2012-2014, 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 <Foundation/Foundation.h>
/**
* 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>
/**
* 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
/**
* 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>
/**
* Returns the HTTP method for the request.
*/
@property(nonatomic, readonly) NSString* method;
/**
* Returns the URL for the request.
*/
@property(nonatomic, readonly) NSURL* URL;
/**
* Returns the HTTP headers for the request.
*/
@property(nonatomic, readonly) NSDictionary* headers;
/**
* Returns the path component of the URL for the request.
*/
@property(nonatomic, readonly) NSString* path;
/**
* Returns the parsed and unescaped query component of the URL for the request.
*
* @warning This property will be nil if there is no query in the URL.
*/
@property(nonatomic, readonly) NSDictionary* query;
/**
* 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) 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) NSDate* ifModifiedSince;
/**
* Returns the parsed "If-None-Match" header or nil if absent or malformed.
*/
@property(nonatomic, readonly) 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;
/**
* This method is the designated initializer for the class.
*/
- (instancetype)initWithMethod:(NSString*)method url:(NSURL*)url headers:(NSDictionary*)headers path:(NSString*)path query:(NSDictionary*)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.
*/
- (id)attributeForKey:(NSString*)key;
@end

View File

@@ -0,0 +1,319 @@
/*
Copyright (c) 2012-2014, 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.
*/
#if !__has_feature(objc_arc)
#error GCDWebServer requires ARC
#endif
#import <zlib.h>
#import "GCDWebServerPrivate.h"
NSString* const GCDWebServerRequestAttribute_RegexCaptures = @"GCDWebServerRequestAttribute_RegexCaptures";
#define kZlibErrorDomain @"ZlibErrorDomain"
#define kGZipInitialBufferSize (256 * 1024)
@interface GCDWebServerBodyDecoder : NSObject <GCDWebServerBodyWriter>
- (id)initWithRequest:(GCDWebServerRequest*)request writer:(id<GCDWebServerBodyWriter>)writer;
@end
@interface GCDWebServerGZipDecoder : GCDWebServerBodyDecoder
@end
@interface GCDWebServerBodyDecoder () {
@private
GCDWebServerRequest* __unsafe_unretained _request;
id<GCDWebServerBodyWriter> __unsafe_unretained _writer;
}
@end
@implementation GCDWebServerBodyDecoder
- (id)initWithRequest:(GCDWebServerRequest*)request writer:(id<GCDWebServerBodyWriter>)writer {
if ((self = [super init])) {
_request = request;
_writer = writer;
}
return self;
}
- (BOOL)open:(NSError**)error {
return [_writer open:error];
}
- (BOOL)writeData:(NSData*)data error:(NSError**)error {
return [_writer writeData:data error:error];
}
- (BOOL)close:(NSError**)error {
return [_writer close:error];
}
@end
@interface GCDWebServerGZipDecoder () {
@private
z_stream _stream;
BOOL _finished;
}
@end
@implementation GCDWebServerGZipDecoder
- (BOOL)open:(NSError**)error {
int result = inflateInit2(&_stream, 15 + 16);
if (result != Z_OK) {
*error = [NSError errorWithDomain:kZlibErrorDomain code:result userInfo:nil];
return NO;
}
if (![super open:error]) {
deflateEnd(&_stream);
return NO;
}
return YES;
}
- (BOOL)writeData:(NSData*)data error:(NSError**)error {
GWS_DCHECK(!_finished);
_stream.next_in = (Bytef*)data.bytes;
_stream.avail_in = (uInt)data.length;
NSMutableData* decodedData = [[NSMutableData alloc] initWithLength:kGZipInitialBufferSize];
if (decodedData == nil) {
GWS_DNOT_REACHED();
return NO;
}
NSUInteger length = 0;
while (1) {
NSUInteger maxLength = decodedData.length - length;
_stream.next_out = (Bytef*)((char*)decodedData.mutableBytes + length);
_stream.avail_out = (uInt)maxLength;
int result = inflate(&_stream, Z_NO_FLUSH);
if ((result != Z_OK) && (result != Z_STREAM_END)) {
*error = [NSError errorWithDomain:kZlibErrorDomain code:result userInfo:nil];
return NO;
}
length += maxLength - _stream.avail_out;
if (_stream.avail_out > 0) {
if (result == Z_STREAM_END) {
_finished = YES;
}
break;
}
decodedData.length = 2 * decodedData.length; // zlib has used all the output buffer so resize it and try again in case more data is available
}
decodedData.length = length;
BOOL success = length ? [super writeData:decodedData error:error] : YES; // No need to call writer if we have no data yet
return success;
}
- (BOOL)close:(NSError**)error {
GWS_DCHECK(_finished);
inflateEnd(&_stream);
return [super close:error];
}
@end
@interface 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;
NSMutableArray* _decoders;
NSMutableDictionary* _attributes;
id<GCDWebServerBodyWriter> __unsafe_unretained _writer;
}
@end
@implementation GCDWebServerRequest : NSObject
@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])) {
_method = [method copy];
_url = url;
_headers = headers;
_path = [path copy];
_query = query;
_type = GCDWebServerNormalizeHeaderValue([_headers objectForKey:@"Content-Type"]);
_chunked = [GCDWebServerNormalizeHeaderValue([_headers objectForKey:@"Transfer-Encoding"]) isEqualToString:@"chunked"];
NSString* lengthHeader = [_headers objectForKey:@"Content-Length"];
if (lengthHeader) {
NSInteger length = [lengthHeader integerValue];
if (_chunked || (length < 0)) {
GWS_DNOT_REACHED();
return nil;
}
_length = length;
if (_type == nil) {
_type = kGCDWebServerDefaultMimeType;
}
} else if (_chunked) {
if (_type == nil) {
_type = kGCDWebServerDefaultMimeType;
}
_length = NSUIntegerMax;
} else {
if (_type) {
GWS_DNOT_REACHED();
return nil;
}
_length = NSUIntegerMax;
}
NSString* modifiedHeader = [_headers objectForKey:@"If-Modified-Since"];
if (modifiedHeader) {
_modifiedSince = [GCDWebServerParseRFC822(modifiedHeader) copy];
}
_noneMatch = [_headers objectForKey:@"If-None-Match"];
_range = NSMakeRange(NSUIntegerMax, 0);
NSString* rangeHeader = GCDWebServerNormalizeHeaderValue([_headers objectForKey:@"Range"]);
if (rangeHeader) {
if ([rangeHeader hasPrefix:@"bytes="]) {
NSArray* components = [[rangeHeader substringFromIndex:6] componentsSeparatedByString:@","];
if (components.count == 1) {
components = [[components firstObject] componentsSeparatedByString:@"-"];
if (components.count == 2) {
NSString* startString = [components objectAtIndex:0];
NSInteger startValue = [startString integerValue];
NSString* endString = [components objectAtIndex:1];
NSInteger endValue = [endString integerValue];
if (startString.length && (startValue >= 0) && endString.length && (endValue >= startValue)) { // The second 500 bytes: "500-999"
_range.location = startValue;
_range.length = endValue - startValue + 1;
} else if (startString.length && (startValue >= 0)) { // The bytes after 9500 bytes: "9500-"
_range.location = startValue;
_range.length = NSUIntegerMax;
} else if (endString.length && (endValue > 0)) { // The final 500 bytes: "-500"
_range.location = NSUIntegerMax;
_range.length = endValue;
}
}
}
}
if ((_range.location == NSUIntegerMax) && (_range.length == 0)) { // Ignore "Range" header if syntactically invalid
GWS_LOG_WARNING(@"Failed to parse 'Range' header \"%@\" for url: %@", rangeHeader, url);
}
}
if ([[_headers objectForKey:@"Accept-Encoding"] rangeOfString:@"gzip"].location != NSNotFound) {
_gzipAccepted = YES;
}
_decoders = [[NSMutableArray alloc] init];
_attributes = [[NSMutableDictionary alloc] init];
}
return self;
}
- (BOOL)hasBody {
return _type ? YES : NO;
}
- (BOOL)hasByteRange {
return GCDWebServerIsValidByteRange(_range);
}
- (id)attributeForKey:(NSString*)key {
return [_attributes objectForKey:key];
}
- (BOOL)open:(NSError**)error {
return YES;
}
- (BOOL)writeData:(NSData*)data error:(NSError**)error {
return YES;
}
- (BOOL)close:(NSError**)error {
return YES;
}
- (void)prepareForWriting {
_writer = self;
if ([GCDWebServerNormalizeHeaderValue([self.headers objectForKey:@"Content-Encoding"]) isEqualToString:@"gzip"]) {
GCDWebServerGZipDecoder* decoder = [[GCDWebServerGZipDecoder alloc] initWithRequest:self writer:_writer];
[_decoders addObject:decoder];
_writer = decoder;
}
}
- (BOOL)performOpen:(NSError**)error {
GWS_DCHECK(_type);
GWS_DCHECK(_writer);
if (_opened) {
GWS_DNOT_REACHED();
return NO;
}
_opened = YES;
return [_writer open:error];
}
- (BOOL)performWriteData:(NSData*)data error:(NSError**)error {
GWS_DCHECK(_opened);
return [_writer writeData:data error:error];
}
- (BOOL)performClose:(NSError**)error {
GWS_DCHECK(_opened);
return [_writer close:error];
}
- (void)setAttribute:(id)attribute forKey:(NSString*)key {
[_attributes setValue:attribute forKey:key];
}
- (NSString*)description {
NSMutableString* description = [NSMutableString stringWithFormat:@"%@ %@", _method, _path];
for (NSString* argument in [[_query allKeys] sortedArrayUsingSelector:@selector(compare:)]) {
[description appendFormat:@"\n %@ = %@", argument, [_query objectForKey:argument]];
}
[description appendString:@"\n"];
for (NSString* header in [[_headers allKeys] sortedArrayUsingSelector:@selector(compare:)]) {
[description appendFormat:@"\n%@: %@", header, [_headers objectForKey:header]];
}
return description;
}
@end

View File

@@ -0,0 +1,208 @@
/*
Copyright (c) 2012-2014, 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 <Foundation/Foundation.h>
/**
* The GCDWebServerBodyReaderCompletionBlock is passed by GCDWebServer to the
* GCDWebServerBodyReader object when reading data from it asynchronously.
*/
typedef void (^GCDWebServerBodyReaderCompletionBlock)(NSData* data, NSError* 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>
@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.
*/
- (NSData*)readData:(NSError**)error;
/**
* This method is called after all body data has been sent.
*/
- (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
/**
* 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>
/**
* Sets the content type for the body of the response.
*
* The default value is nil i.e. the response has no body.
*
* @warning This property must be set if a body is present.
*/
@property(nonatomic, copy) 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, retain) NSDate* lastModifiedDate;
/**
* Sets the ETag for the response using the "ETag" header.
*
* The default value is nil.
*/
@property(nonatomic, copy) 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;
/**
* This method is the designated initializer for the class.
*/
- (instancetype)init;
/**
* 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:(NSString*)value forAdditionalHeader:(NSString*)header;
/**
* Convenience method that checks if the contentType property is defined.
*/
- (BOOL)hasBody;
@end
@interface GCDWebServerResponse (Extensions)
/**
* Creates a empty response with a specific HTTP status code.
*/
+ (instancetype)responseWithStatusCode:(NSInteger)statusCode;
/**
* Creates an HTTP redirect response to a new URL.
*/
+ (instancetype)responseWithRedirect:(NSURL*)location permanent:(BOOL)permanent;
/**
* Initializes an empty response with a specific HTTP status code.
*/
- (instancetype)initWithStatusCode:(NSInteger)statusCode;
/**
* Initializes an HTTP redirect response to a new URL.
*/
- (instancetype)initWithRedirect:(NSURL*)location permanent:(BOOL)permanent;
@end

View File

@@ -0,0 +1,305 @@
/*
Copyright (c) 2012-2014, 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.
*/
#if !__has_feature(objc_arc)
#error GCDWebServer requires ARC
#endif
#import <zlib.h>
#import "GCDWebServerPrivate.h"
#define kZlibErrorDomain @"ZlibErrorDomain"
#define kGZipInitialBufferSize (256 * 1024)
@interface GCDWebServerBodyEncoder : NSObject <GCDWebServerBodyReader>
- (id)initWithResponse:(GCDWebServerResponse*)response reader:(id<GCDWebServerBodyReader>)reader;
@end
@interface GCDWebServerGZipEncoder : GCDWebServerBodyEncoder
@end
@interface GCDWebServerBodyEncoder () {
@private
GCDWebServerResponse* __unsafe_unretained _response;
id<GCDWebServerBodyReader> __unsafe_unretained _reader;
}
@end
@implementation GCDWebServerBodyEncoder
- (id)initWithResponse:(GCDWebServerResponse*)response reader:(id<GCDWebServerBodyReader>)reader {
if ((self = [super init])) {
_response = response;
_reader = reader;
}
return self;
}
- (BOOL)open:(NSError**)error {
return [_reader open:error];
}
- (NSData*)readData:(NSError**)error {
return [_reader readData:error];
}
- (void)close {
[_reader close];
}
@end
@interface GCDWebServerGZipEncoder () {
@private
z_stream _stream;
BOOL _finished;
}
@end
@implementation GCDWebServerGZipEncoder
- (id)initWithResponse:(GCDWebServerResponse*)response reader:(id<GCDWebServerBodyReader>)reader {
if ((self = [super initWithResponse:response reader:reader])) {
response.contentLength = NSUIntegerMax; // Make sure "Content-Length" header is not set since we don't know it
[response setValue:@"gzip" forAdditionalHeader:@"Content-Encoding"];
}
return self;
}
- (BOOL)open:(NSError**)error {
int result = deflateInit2(&_stream, Z_DEFAULT_COMPRESSION, Z_DEFLATED, 15 + 16, 8, Z_DEFAULT_STRATEGY);
if (result != Z_OK) {
*error = [NSError errorWithDomain:kZlibErrorDomain code:result userInfo:nil];
return NO;
}
if (![super open:error]) {
deflateEnd(&_stream);
return NO;
}
return YES;
}
- (NSData*)readData:(NSError**)error {
NSMutableData* encodedData;
if (_finished) {
encodedData = [[NSMutableData alloc] init];
} else {
encodedData = [[NSMutableData alloc] initWithLength:kGZipInitialBufferSize];
if (encodedData == nil) {
GWS_DNOT_REACHED();
return nil;
}
NSUInteger length = 0;
do {
NSData* data = [super readData:error];
if (data == nil) {
return nil;
}
_stream.next_in = (Bytef*)data.bytes;
_stream.avail_in = (uInt)data.length;
while (1) {
NSUInteger maxLength = encodedData.length - length;
_stream.next_out = (Bytef*)((char*)encodedData.mutableBytes + length);
_stream.avail_out = (uInt)maxLength;
int result = deflate(&_stream, data.length ? Z_NO_FLUSH : Z_FINISH);
if (result == Z_STREAM_END) {
_finished = YES;
} else if (result != Z_OK) {
*error = [NSError errorWithDomain:kZlibErrorDomain code:result userInfo:nil];
return nil;
}
length += maxLength - _stream.avail_out;
if (_stream.avail_out > 0) {
break;
}
encodedData.length = 2 * encodedData.length; // zlib has used all the output buffer so resize it and try again in case more data is available
}
GWS_DCHECK(_stream.avail_in == 0);
} while (length == 0); // Make sure we don't return an empty NSData if not in finished state
encodedData.length = length;
}
return encodedData;
}
- (void)close {
deflateEnd(&_stream);
[super close];
}
@end
@interface GCDWebServerResponse () {
@private
NSString* _type;
NSUInteger _length;
NSInteger _status;
NSUInteger _maxAge;
NSDate* _lastModified;
NSString* _eTag;
NSMutableDictionary* _headers;
BOOL _chunked;
BOOL _gzipped;
BOOL _opened;
NSMutableArray* _encoders;
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 {
return [[[self class] alloc] init];
}
- (instancetype)init {
if ((self = [super init])) {
_type = nil;
_length = NSUIntegerMax;
_status = kGCDWebServerHTTPStatusCode_OK;
_maxAge = 0;
_headers = [[NSMutableDictionary alloc] init];
_encoders = [[NSMutableArray alloc] init];
}
return self;
}
- (void)setValue:(NSString*)value forAdditionalHeader:(NSString*)header {
[_headers setValue:value forKey:header];
}
- (BOOL)hasBody {
return _type ? YES : NO;
}
- (BOOL)usesChunkedTransferEncoding {
return (_type != nil) && (_length == NSUIntegerMax);
}
- (BOOL)open:(NSError**)error {
return YES;
}
- (NSData*)readData:(NSError**)error {
return [NSData data];
}
- (void)close {
;
}
- (void)prepareForReading {
_reader = self;
if (_gzipped) {
GCDWebServerGZipEncoder* encoder = [[GCDWebServerGZipEncoder alloc] initWithResponse:self reader:_reader];
[_encoders addObject:encoder];
_reader = encoder;
}
}
- (BOOL)performOpen:(NSError**)error {
GWS_DCHECK(_type);
GWS_DCHECK(_reader);
if (_opened) {
GWS_DNOT_REACHED();
return NO;
}
_opened = YES;
return [_reader open:error];
}
- (void)performReadDataWithCompletion:(GCDWebServerBodyReaderCompletionBlock)block {
if ([_reader respondsToSelector:@selector(asyncReadDataWithCompletion:)]) {
[_reader asyncReadDataWithCompletion:block];
} else {
NSError* error = nil;
NSData* data = [_reader readData:&error];
block(data, error);
}
}
- (void)performClose {
GWS_DCHECK(_opened);
[_reader close];
}
- (NSString*)description {
NSMutableString* description = [NSMutableString stringWithFormat:@"Status Code = %i", (int)_status];
if (_type) {
[description appendFormat:@"\nContent Type = %@", _type];
}
if (_length != NSUIntegerMax) {
[description appendFormat:@"\nContent Length = %lu", (unsigned long)_length];
}
[description appendFormat:@"\nCache Control Max Age = %lu", (unsigned long)_maxAge];
if (_lastModified) {
[description appendFormat:@"\nLast Modified Date = %@", _lastModified];
}
if (_eTag) {
[description appendFormat:@"\nETag = %@", _eTag];
}
if (_headers.count) {
[description appendString:@"\n"];
for (NSString* header in [[_headers allKeys] sortedArrayUsingSelector:@selector(compare:)]) {
[description appendFormat:@"\n%@: %@", header, [_headers objectForKey:header]];
}
}
return description;
}
@end
@implementation GCDWebServerResponse (Extensions)
+ (instancetype)responseWithStatusCode:(NSInteger)statusCode {
return [[self alloc] initWithStatusCode:statusCode];
}
+ (instancetype)responseWithRedirect:(NSURL*)location permanent:(BOOL)permanent {
return [[self alloc] initWithRedirect:location permanent:permanent];
}
- (instancetype)initWithStatusCode:(NSInteger)statusCode {
if ((self = [self init])) {
self.statusCode = statusCode;
}
return self;
}
- (instancetype)initWithRedirect:(NSURL*)location permanent:(BOOL)permanent {
if ((self = [self init])) {
self.statusCode = permanent ? kGCDWebServerHTTPStatusCode_MovedPermanently : kGCDWebServerHTTPStatusCode_TemporaryRedirect;
[self setValue:[location absoluteString] forAdditionalHeader:@"Location"];
}
return self;
}
@end

View File

@@ -25,24 +25,36 @@
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
#import "GCDWebServer.h"
#import "GCDWebServerRequest.h"
@class GCDWebServerHandler;
/**
* The GCDWebServerDataRequest subclass of GCDWebServerRequest stores the body
* of the HTTP request in memory.
*/
@interface GCDWebServerDataRequest : GCDWebServerRequest
/**
* Returns the data for the request body.
*/
@property(nonatomic, readonly) NSData* data;
@interface GCDWebServerConnection : NSObject
@property(nonatomic, readonly) GCDWebServer* server;
@property(nonatomic, readonly) NSData* localAddressData; // struct sockaddr
@property(nonatomic, readonly) NSString* localAddressString;
@property(nonatomic, readonly) NSData* remoteAddressData; // struct sockaddr
@property(nonatomic, readonly) NSString* remoteAddressString;
@property(nonatomic, readonly) NSUInteger totalBytesRead;
@property(nonatomic, readonly) NSUInteger totalBytesWritten;
@end
@interface GCDWebServerConnection (Subclassing)
- (void)open;
- (void)didUpdateBytesRead; // Called from arbitrary thread after @totalBytesRead is updated - Default implementation does nothing
- (void)didUpdateBytesWritten; // Called from arbitrary thread after @totalBytesWritten is updated - Default implementation does nothing
- (GCDWebServerResponse*)processRequest:(GCDWebServerRequest*)request withBlock:(GCDWebServerProcessBlock)block;
- (void)close;
@interface GCDWebServerDataRequest (Extensions)
/**
* 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) 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) id jsonObject;
@end

View File

@@ -0,0 +1,106 @@
/*
Copyright (c) 2012-2014, 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.
*/
#if !__has_feature(objc_arc)
#error GCDWebServer requires ARC
#endif
#import "GCDWebServerPrivate.h"
@interface GCDWebServerDataRequest () {
@private
NSMutableData* _data;
NSString* _text;
id _jsonObject;
}
@end
@implementation GCDWebServerDataRequest
@synthesize data=_data;
- (BOOL)open:(NSError**)error {
if (self.contentLength != NSUIntegerMax) {
_data = [[NSMutableData alloc] initWithCapacity:self.contentLength];
} else {
_data = [[NSMutableData alloc] init];
}
if (_data == nil) {
*error = [NSError errorWithDomain:kGCDWebServerErrorDomain code:-1 userInfo:@{NSLocalizedDescriptionKey: @"Failed allocating memory"}];
return NO;
}
return YES;
}
- (BOOL)writeData:(NSData*)data error:(NSError**)error {
[_data appendData:data];
return YES;
}
- (BOOL)close:(NSError**)error {
return YES;
}
- (NSString*)description {
NSMutableString* description = [NSMutableString stringWithString:[super description]];
if (_data) {
[description appendString:@"\n\n"];
[description appendString:GCDWebServerDescribeData(_data, self.contentType)];
}
return description;
}
@end
@implementation GCDWebServerDataRequest (Extensions)
- (NSString*)text {
if (_text == nil) {
if ([self.contentType hasPrefix:@"text/"]) {
NSString* charset = GCDWebServerExtractHeaderValueParameter(self.contentType, @"charset");
_text = [[NSString alloc] initWithData:self.data encoding:GCDWebServerStringEncodingFromCharset(charset)];
} else {
GWS_DNOT_REACHED();
}
}
return _text;
}
- (id)jsonObject {
if (_jsonObject == nil) {
NSString* mimeType = GCDWebServerTruncateHeaderValue(self.contentType);
if ([mimeType isEqualToString:@"application/json"] || [mimeType isEqualToString:@"text/json"] || [mimeType isEqualToString:@"text/javascript"]) {
_jsonObject = [NSJSONSerialization JSONObjectWithData:_data options:0 error:NULL];
} else {
GWS_DNOT_REACHED();
}
}
return _jsonObject;
}
@end

View File

@@ -0,0 +1,45 @@
/*
Copyright (c) 2012-2014, 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 "GCDWebServerRequest.h"
/**
* The GCDWebServerFileRequest subclass of GCDWebServerRequest stores the body
* of the HTTP request to a file on disk.
*/
@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;
@end

View File

@@ -0,0 +1,103 @@
/*
Copyright (c) 2012-2014, 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.
*/
#if !__has_feature(objc_arc)
#error GCDWebServer requires ARC
#endif
#import "GCDWebServerPrivate.h"
@interface GCDWebServerFileRequest () {
@private
NSString* _temporaryPath;
int _file;
}
@end
@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])) {
_temporaryPath = [NSTemporaryDirectory() stringByAppendingPathComponent:[[NSProcessInfo processInfo] globallyUniqueString]];
}
return self;
}
- (void)dealloc {
unlink([_temporaryPath fileSystemRepresentation]);
}
- (BOOL)open:(NSError**)error {
_file = open([_temporaryPath fileSystemRepresentation], O_CREAT | O_TRUNC | O_WRONLY, S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH);
if (_file <= 0) {
*error = GCDWebServerMakePosixError(errno);
return NO;
}
return YES;
}
- (BOOL)writeData:(NSData*)data error:(NSError**)error {
if (write(_file, data.bytes, data.length) != (ssize_t)data.length) {
*error = GCDWebServerMakePosixError(errno);
return NO;
}
return YES;
}
- (BOOL)close:(NSError**)error {
if (close(_file) < 0) {
*error = GCDWebServerMakePosixError(errno);
return NO;
}
#ifdef __GCDWEBSERVER_ENABLE_TESTING__
NSString* creationDateHeader = [self.headers objectForKey:@"X-GCDWebServer-CreationDate"];
if (creationDateHeader) {
NSDate* date = GCDWebServerParseISO8601(creationDateHeader);
if (!date || ![[NSFileManager defaultManager] setAttributes:@{NSFileCreationDate: date} ofItemAtPath:_temporaryPath error:error]) {
return NO;
}
}
NSString* modifiedDateHeader = [self.headers objectForKey:@"X-GCDWebServer-ModifiedDate"];
if (modifiedDateHeader) {
NSDate* date = GCDWebServerParseRFC822(modifiedDateHeader);
if (!date || ![[NSFileManager defaultManager] setAttributes:@{NSFileModificationDate: date} ofItemAtPath:_temporaryPath error:error]) {
return NO;
}
}
#endif
return YES;
}
- (NSString*)description {
NSMutableString* description = [NSMutableString stringWithString:[super description]];
[description appendFormat:@"\n\n{%@}", _temporaryPath];
return description;
}
@end

View File

@@ -0,0 +1,132 @@
/*
Copyright (c) 2012-2014, 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 "GCDWebServerRequest.h"
/**
* The GCDWebServerMultiPart class is an abstract class that wraps the content
* of a part.
*/
@interface GCDWebServerMultiPart : NSObject
/**
* 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;
@end
/**
* The GCDWebServerMultiPartArgument subclass of GCDWebServerMultiPart wraps
* the content of a part as data in memory.
*/
@interface GCDWebServerMultiPartArgument : GCDWebServerMultiPart
/**
* Returns the data for the part.
*/
@property(nonatomic, readonly) NSData* data;
/**
* 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) NSString* string;
@end
/**
* The GCDWebServerMultiPartFile subclass of GCDWebServerMultiPart wraps
* the content of a part as a file on disk.
*/
@interface GCDWebServerMultiPartFile : GCDWebServerMultiPart
/**
* 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;
@end
/**
* The GCDWebServerMultiPartFormRequest subclass of GCDWebServerRequest
* parses the body of the HTTP request as a multipart encoded form.
*/
@interface GCDWebServerMultiPartFormRequest : GCDWebServerRequest
/**
* Returns the argument parts from the multipart encoded form as
* name / GCDWebServerMultiPartArgument pairs.
*/
@property(nonatomic, readonly) NSArray* arguments;
/**
* Returns the files parts from the multipart encoded form as
* name / GCDWebServerMultiPartFile pairs.
*/
@property(nonatomic, readonly) NSArray* files;
/**
* Returns the MIME type for multipart encoded forms
* i.e. "multipart/form-data".
*/
+ (NSString*)mimeType;
/**
* Returns the first argument for a given control name or nil if not found.
*/
- (GCDWebServerMultiPartArgument*)firstArgumentForControlName:(NSString*)name;
/**
* Returns the first file for a given control name or nil if not found.
*/
- (GCDWebServerMultiPartFile*)firstFileForControlName:(NSString*)name;
@end

View File

@@ -0,0 +1,439 @@
/*
Copyright (c) 2012-2014, 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.
*/
#if !__has_feature(objc_arc)
#error GCDWebServer requires ARC
#endif
#import "GCDWebServerPrivate.h"
#define kMultiPartBufferSize (256 * 1024)
typedef enum {
kParserState_Undefined = 0,
kParserState_Start,
kParserState_Headers,
kParserState_Content,
kParserState_End
} ParserState;
@interface GCDWebServerMIMEStreamParser : NSObject
- (id)initWithBoundary:(NSString*)boundary defaultControlName:(NSString*)name arguments:(NSMutableArray*)arguments files:(NSMutableArray*)files;
- (BOOL)appendBytes:(const void*)bytes length:(NSUInteger)length;
- (BOOL)isAtEnd;
@end
static NSData* _newlineData = nil;
static NSData* _newlinesData = nil;
static NSData* _dashNewlineData = nil;
@interface GCDWebServerMultiPart () {
@private
NSString* _controlName;
NSString* _contentType;
NSString* _mimeType;
}
@end
@implementation GCDWebServerMultiPart
@synthesize controlName=_controlName, contentType=_contentType, mimeType=_mimeType;
- (id)initWithControlName:(NSString*)name contentType:(NSString*)type {
if ((self = [super init])) {
_controlName = [name copy];
_contentType = [type copy];
_mimeType = GCDWebServerTruncateHeaderValue(_contentType);
}
return self;
}
@end
@interface GCDWebServerMultiPartArgument () {
@private
NSData* _data;
NSString* _string;
}
@end
@implementation GCDWebServerMultiPartArgument
@synthesize data=_data, string=_string;
- (id)initWithControlName:(NSString*)name contentType:(NSString*)type data:(NSData*)data {
if ((self = [super initWithControlName:name contentType:type])) {
_data = data;
if ([self.contentType hasPrefix:@"text/"]) {
NSString* charset = GCDWebServerExtractHeaderValueParameter(self.contentType, @"charset");
_string = [[NSString alloc] initWithData:_data encoding:GCDWebServerStringEncodingFromCharset(charset)];
}
}
return self;
}
- (NSString*)description {
return [NSString stringWithFormat:@"<%@ | '%@' | %lu bytes>", [self class], self.mimeType, (unsigned long)_data.length];
}
@end
@interface GCDWebServerMultiPartFile () {
@private
NSString* _fileName;
NSString* _temporaryPath;
}
@end
@implementation GCDWebServerMultiPartFile
@synthesize fileName=_fileName, temporaryPath=_temporaryPath;
- (id)initWithControlName:(NSString*)name contentType:(NSString*)type fileName:(NSString*)fileName temporaryPath:(NSString*)temporaryPath {
if ((self = [super initWithControlName:name contentType:type])) {
_fileName = [fileName copy];
_temporaryPath = [temporaryPath copy];
}
return self;
}
- (void)dealloc {
unlink([_temporaryPath fileSystemRepresentation]);
}
- (NSString*)description {
return [NSString stringWithFormat:@"<%@ | '%@' | '%@>'", [self class], self.mimeType, _fileName];
}
@end
@interface GCDWebServerMIMEStreamParser () {
@private
NSData* _boundary;
NSString* _defaultcontrolName;
ParserState _state;
NSMutableData* _data;
NSMutableArray* _arguments;
NSMutableArray* _files;
NSString* _controlName;
NSString* _fileName;
NSString* _contentType;
NSString* _tmpPath;
int _tmpFile;
GCDWebServerMIMEStreamParser* _subParser;
}
@end
@implementation GCDWebServerMIMEStreamParser
+ (void)initialize {
if (_newlineData == nil) {
_newlineData = [[NSData alloc] initWithBytes:"\r\n" length:2];
GWS_DCHECK(_newlineData);
}
if (_newlinesData == nil) {
_newlinesData = [[NSData alloc] initWithBytes:"\r\n\r\n" length:4];
GWS_DCHECK(_newlinesData);
}
if (_dashNewlineData == nil) {
_dashNewlineData = [[NSData alloc] initWithBytes:"--\r\n" length:4];
GWS_DCHECK(_dashNewlineData);
}
}
- (id)initWithBoundary:(NSString*)boundary defaultControlName:(NSString*)name arguments:(NSMutableArray*)arguments files:(NSMutableArray*)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 () {
@private
GCDWebServerMIMEStreamParser* _parser;
NSMutableArray* _arguments;
NSMutableArray* _files;
}
@end
@implementation GCDWebServerMultiPartFormRequest
@synthesize arguments=_arguments, files=_files;
+ (NSString*)mimeType {
return @"multipart/form-data";
}
- (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])) {
_arguments = [[NSMutableArray alloc] init];
_files = [[NSMutableArray alloc] init];
}
return self;
}
- (BOOL)open:(NSError**)error {
NSString* boundary = GCDWebServerExtractHeaderValueParameter(self.contentType, @"boundary");
_parser = [[GCDWebServerMIMEStreamParser alloc] initWithBoundary:boundary defaultControlName:nil arguments:_arguments files:_files];
if (_parser == nil) {
*error = [NSError errorWithDomain:kGCDWebServerErrorDomain code:-1 userInfo:@{NSLocalizedDescriptionKey: @"Failed starting to parse multipart form data"}];
return NO;
}
return YES;
}
- (BOOL)writeData:(NSData*)data error:(NSError**)error {
if (![_parser appendBytes:data.bytes length:data.length]) {
*error = [NSError errorWithDomain:kGCDWebServerErrorDomain code:-1 userInfo:@{NSLocalizedDescriptionKey: @"Failed continuing to parse multipart form data"}];
return NO;
}
return YES;
}
- (BOOL)close:(NSError**)error {
BOOL atEnd = [_parser isAtEnd];
_parser = nil;
if (!atEnd) {
*error = [NSError errorWithDomain:kGCDWebServerErrorDomain code:-1 userInfo:@{NSLocalizedDescriptionKey: @"Failed finishing to parse multipart form data"}];
return NO;
}
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 {
NSMutableString* description = [NSMutableString stringWithString:[super description]];
if (_arguments.count) {
[description appendString:@"\n"];
for (GCDWebServerMultiPartArgument* argument in _arguments) {
[description appendFormat:@"\n%@ (%@)\n", argument.controlName, argument.contentType];
[description appendString:GCDWebServerDescribeData(argument.data, argument.contentType)];
}
}
if (_files.count) {
[description appendString:@"\n"];
for (GCDWebServerMultiPartFile* file in _files) {
[description appendFormat:@"\n%@ (%@): %@\n{%@}", file.controlName, file.contentType, file.fileName, file.temporaryPath];
}
}
return description;
}
@end

View File

@@ -0,0 +1,51 @@
/*
Copyright (c) 2012-2014, 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 "GCDWebServerDataRequest.h"
/**
* The GCDWebServerURLEncodedFormRequest subclass of GCDWebServerRequest
* parses the body of the HTTP request as a URL encoded form using
* GCDWebServerParseURLEncodedForm().
*/
@interface GCDWebServerURLEncodedFormRequest : GCDWebServerDataRequest
/**
* 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* arguments;
/**
* Returns the MIME type for URL encoded forms
* i.e. "application/x-www-form-urlencoded".
*/
+ (NSString*)mimeType;
@end

View File

@@ -0,0 +1,70 @@
/*
Copyright (c) 2012-2014, 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.
*/
#if !__has_feature(objc_arc)
#error GCDWebServer requires ARC
#endif
#import "GCDWebServerPrivate.h"
@interface GCDWebServerURLEncodedFormRequest () {
@private
NSDictionary* _arguments;
}
@end
@implementation GCDWebServerURLEncodedFormRequest
@synthesize arguments=_arguments;
+ (NSString*)mimeType {
return @"application/x-www-form-urlencoded";
}
- (BOOL)close:(NSError**)error {
if (![super close:error]) {
return NO;
}
NSString* charset = GCDWebServerExtractHeaderValueParameter(self.contentType, @"charset");
NSString* string = [[NSString alloc] initWithData:self.data encoding:GCDWebServerStringEncodingFromCharset(charset)];
_arguments = GCDWebServerParseURLEncodedForm(string);
GWS_DCHECK(_arguments);
return YES;
}
- (NSString*)description {
NSMutableString* description = [NSMutableString stringWithString:[super description]];
[description appendString:@"\n"];
for (NSString* argument in [[_arguments allKeys] sortedArrayUsingSelector:@selector(compare:)]) {
[description appendFormat:@"\n%@ = %@", argument, [_arguments objectForKey:argument]];
}
return description;
}
@end

View File

@@ -0,0 +1,108 @@
/*
Copyright (c) 2012-2014, 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"
/**
* The GCDWebServerDataResponse subclass of GCDWebServerResponse reads the body
* of the HTTP response from memory.
*/
@interface GCDWebServerDataResponse : GCDWebServerResponse
/**
* Creates a response with data in memory and a given content type.
*/
+ (instancetype)responseWithData:(NSData*)data contentType:(NSString*)type;
/**
* This method is the designated initializer for the class.
*/
- (instancetype)initWithData:(NSData*)data contentType:(NSString*)type;
@end
@interface GCDWebServerDataResponse (Extensions)
/**
* Creates a data response from text encoded using UTF-8.
*/
+ (instancetype)responseWithText:(NSString*)text;
/**
* Creates a data response from HTML encoded using UTF-8.
*/
+ (instancetype)responseWithHTML:(NSString*)html;
/**
* Creates a data response from an HTML template encoded using UTF-8.
* See -initWithHTMLTemplate:variables: for details.
*/
+ (instancetype)responseWithHTMLTemplate:(NSString*)path variables:(NSDictionary*)variables;
/**
* Creates a data response from a serialized JSON object and the default
* "application/json" content type.
*/
+ (instancetype)responseWithJSONObject:(id)object;
/**
* Creates a data response from a serialized JSON object and a custom
* content type.
*/
+ (instancetype)responseWithJSONObject:(id)object contentType:(NSString*)type;
/**
* Initializes a data response from text encoded using UTF-8.
*/
- (instancetype)initWithText:(NSString*)text;
/**
* Initializes a data response from HTML encoded using UTF-8.
*/
- (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.
*/
- (instancetype)initWithHTMLTemplate:(NSString*)path variables:(NSDictionary*)variables;
/**
* Initializes a data response from a serialized JSON object and the default
* "application/json" content type.
*/
- (instancetype)initWithJSONObject:(id)object;
/**
* Initializes a data response from a serialized JSON object and a custom
* content type.
*/
- (instancetype)initWithJSONObject:(id)object contentType:(NSString*)type;
@end

View File

@@ -0,0 +1,143 @@
/*
Copyright (c) 2012-2014, 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.
*/
#if !__has_feature(objc_arc)
#error GCDWebServer requires ARC
#endif
#import "GCDWebServerPrivate.h"
@interface GCDWebServerDataResponse () {
@private
NSData* _data;
BOOL _done;
}
@end
@implementation GCDWebServerDataResponse
+ (instancetype)responseWithData:(NSData*)data contentType:(NSString*)type {
return [[[self class] alloc] initWithData:data contentType:type];
}
- (instancetype)initWithData:(NSData*)data contentType:(NSString*)type {
if (data == nil) {
GWS_DNOT_REACHED();
return nil;
}
if ((self = [super init])) {
_data = data;
self.contentType = type;
self.contentLength = data.length;
}
return self;
}
- (NSData*)readData:(NSError**)error {
NSData* data;
if (_done) {
data = [NSData data];
} else {
data = _data;
_done = YES;
}
return data;
}
- (NSString*)description {
NSMutableString* description = [NSMutableString stringWithString:[super description]];
[description appendString:@"\n\n"];
[description appendString:GCDWebServerDescribeData(_data, self.contentType)];
return description;
}
@end
@implementation GCDWebServerDataResponse (Extensions)
+ (instancetype)responseWithText:(NSString*)text {
return [[self alloc] initWithText:text];
}
+ (instancetype)responseWithHTML:(NSString*)html {
return [[self alloc] initWithHTML:html];
}
+ (instancetype)responseWithHTMLTemplate:(NSString*)path variables:(NSDictionary*)variables {
return [[self alloc] initWithHTMLTemplate:path variables:variables];
}
+ (instancetype)responseWithJSONObject:(id)object {
return [[self alloc] initWithJSONObject:object];
}
+ (instancetype)responseWithJSONObject:(id)object contentType:(NSString*)type {
return [[self alloc] initWithJSONObject:object contentType:type];
}
- (instancetype)initWithText:(NSString*)text {
NSData* data = [text dataUsingEncoding:NSUTF8StringEncoding];
if (data == nil) {
GWS_DNOT_REACHED();
return nil;
}
return [self initWithData:data contentType:@"text/plain; charset=utf-8"];
}
- (instancetype)initWithHTML:(NSString*)html {
NSData* data = [html dataUsingEncoding:NSUTF8StringEncoding];
if (data == nil) {
GWS_DNOT_REACHED();
return nil;
}
return [self initWithData:data contentType:@"text/html; charset=utf-8"];
}
- (instancetype)initWithHTMLTemplate:(NSString*)path variables:(NSDictionary*)variables {
NSMutableString* html = [[NSMutableString alloc] initWithContentsOfFile:path encoding:NSUTF8StringEncoding error:NULL];
[variables enumerateKeysAndObjectsUsingBlock:^(NSString* key, NSString* value, BOOL* stop) {
[html replaceOccurrencesOfString:[NSString stringWithFormat:@"%%%@%%", key] withString:value options:0 range:NSMakeRange(0, html.length)];
}];
id response = [self initWithHTML:html];
return response;
}
- (instancetype)initWithJSONObject:(id)object {
return [self initWithJSONObject:object contentType:@"application/json"];
}
- (instancetype)initWithJSONObject:(id)object contentType:(NSString*)type {
NSData* data = [NSJSONSerialization dataWithJSONObject:object options:0 error:NULL];
if (data == nil) {
return nil;
}
return [self initWithData:data contentType:type];
}
@end

View File

@@ -0,0 +1,81 @@
/*
Copyright (c) 2012-2014, 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 "GCDWebServerDataResponse.h"
#import "GCDWebServerHTTPStatusCodes.h"
/**
* The GCDWebServerDataResponse subclass of GCDWebServerDataResponse generates
* an HTML body from an HTTP status code and an error message.
*/
@interface GCDWebServerErrorResponse : GCDWebServerDataResponse
/**
* Creates a client error response with the corresponding HTTP status code.
*/
+ (instancetype)responseWithClientError:(GCDWebServerClientErrorHTTPStatusCode)errorCode message:(NSString*)format, ... NS_FORMAT_FUNCTION(2,3);
/**
* 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:(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:(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:(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:(NSError*)underlyingError message:(NSString*)format, ... NS_FORMAT_FUNCTION(3,4);
@end

View File

@@ -0,0 +1,128 @@
/*
Copyright (c) 2012-2014, 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.
*/
#if !__has_feature(objc_arc)
#error GCDWebServer requires ARC
#endif
#import "GCDWebServerPrivate.h"
@interface GCDWebServerErrorResponse ()
- (instancetype)initWithStatusCode:(NSInteger)statusCode underlyingError:(NSError*)underlyingError messageFormat:(NSString*)format arguments:(va_list)arguments;
@end
@implementation GCDWebServerErrorResponse
+ (instancetype)responseWithClientError:(GCDWebServerClientErrorHTTPStatusCode)errorCode message:(NSString*)format, ... {
GWS_DCHECK(((NSInteger)errorCode >= 400) && ((NSInteger)errorCode < 500));
va_list arguments;
va_start(arguments, format);
GCDWebServerErrorResponse* response = [[self alloc] initWithStatusCode:errorCode underlyingError:nil messageFormat:format arguments:arguments];
va_end(arguments);
return response;
}
+ (instancetype)responseWithServerError:(GCDWebServerServerErrorHTTPStatusCode)errorCode message:(NSString*)format, ... {
GWS_DCHECK(((NSInteger)errorCode >= 500) && ((NSInteger)errorCode < 600));
va_list arguments;
va_start(arguments, format);
GCDWebServerErrorResponse* response = [[self alloc] initWithStatusCode:errorCode underlyingError:nil messageFormat:format arguments:arguments];
va_end(arguments);
return response;
}
+ (instancetype)responseWithClientError:(GCDWebServerClientErrorHTTPStatusCode)errorCode underlyingError:(NSError*)underlyingError message:(NSString*)format, ... {
GWS_DCHECK(((NSInteger)errorCode >= 400) && ((NSInteger)errorCode < 500));
va_list arguments;
va_start(arguments, format);
GCDWebServerErrorResponse* response = [[self alloc] initWithStatusCode:errorCode underlyingError:underlyingError messageFormat:format arguments:arguments];
va_end(arguments);
return response;
}
+ (instancetype)responseWithServerError:(GCDWebServerServerErrorHTTPStatusCode)errorCode underlyingError:(NSError*)underlyingError message:(NSString*)format, ... {
GWS_DCHECK(((NSInteger)errorCode >= 500) && ((NSInteger)errorCode < 600));
va_list arguments;
va_start(arguments, format);
GCDWebServerErrorResponse* response = [[self alloc] initWithStatusCode:errorCode underlyingError:underlyingError messageFormat:format arguments:arguments];
va_end(arguments);
return response;
}
static inline NSString* _EscapeHTMLString(NSString* string) {
return [string stringByReplacingOccurrencesOfString:@"\"" withString:@"&quot;"];
}
- (instancetype)initWithStatusCode:(NSInteger)statusCode underlyingError:(NSError*)underlyingError messageFormat:(NSString*)format arguments:(va_list)arguments {
NSString* message = [[NSString alloc] initWithFormat:format arguments:arguments];
NSString* title = [NSString stringWithFormat:@"HTTP Error %i", (int)statusCode];
NSString* error = underlyingError ? [NSString stringWithFormat:@"[%@] %@ (%li)", underlyingError.domain, _EscapeHTMLString(underlyingError.localizedDescription), (long)underlyingError.code] : @"";
NSString* html = [NSString stringWithFormat:@"<!DOCTYPE html><html lang=\"en\"><head><meta charset=\"utf-8\"><title>%@</title></head><body><h1>%@: %@</h1><h3>%@</h3></body></html>",
title, title, _EscapeHTMLString(message), error];
if ((self = [self initWithHTML:html])) {
self.statusCode = statusCode;
}
return self;
}
- (instancetype)initWithClientError:(GCDWebServerClientErrorHTTPStatusCode)errorCode message:(NSString*)format, ... {
GWS_DCHECK(((NSInteger)errorCode >= 400) && ((NSInteger)errorCode < 500));
va_list arguments;
va_start(arguments, format);
self = [self initWithStatusCode:errorCode underlyingError:nil messageFormat:format arguments:arguments];
va_end(arguments);
return self;
}
- (instancetype)initWithServerError:(GCDWebServerServerErrorHTTPStatusCode)errorCode message:(NSString*)format, ... {
GWS_DCHECK(((NSInteger)errorCode >= 500) && ((NSInteger)errorCode < 600));
va_list arguments;
va_start(arguments, format);
self = [self initWithStatusCode:errorCode underlyingError:nil messageFormat:format arguments:arguments];
va_end(arguments);
return self;
}
- (instancetype)initWithClientError:(GCDWebServerClientErrorHTTPStatusCode)errorCode underlyingError:(NSError*)underlyingError message:(NSString*)format, ... {
GWS_DCHECK(((NSInteger)errorCode >= 400) && ((NSInteger)errorCode < 500));
va_list arguments;
va_start(arguments, format);
self = [self initWithStatusCode:errorCode underlyingError:underlyingError messageFormat:format arguments:arguments];
va_end(arguments);
return self;
}
- (instancetype)initWithServerError:(GCDWebServerServerErrorHTTPStatusCode)errorCode underlyingError:(NSError*)underlyingError message:(NSString*)format, ... {
GWS_DCHECK(((NSInteger)errorCode >= 500) && ((NSInteger)errorCode < 600));
va_list arguments;
va_start(arguments, format);
self = [self initWithStatusCode:errorCode underlyingError:underlyingError messageFormat:format arguments:arguments];
va_end(arguments);
return self;
}
@end

View File

@@ -0,0 +1,96 @@
/*
Copyright (c) 2012-2014, 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"
/**
* 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
/**
* Creates a response with the contents of a file.
*/
+ (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.
*/
+ (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.
*/
+ (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.
*/
+ (instancetype)responseWithFile:(NSString*)path byteRange:(NSRange)range isAttachment:(BOOL)attachment;
/**
* Initializes a response with the contents of a file.
*/
- (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.
*/
- (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.
*/
- (instancetype)initWithFile:(NSString*)path byteRange:(NSRange)range;
/**
* This method is the designated initializer for the class.
*/
- (instancetype)initWithFile:(NSString*)path byteRange:(NSRange)range isAttachment:(BOOL)attachment;
@end

View File

@@ -0,0 +1,181 @@
/*
Copyright (c) 2012-2014, 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.
*/
#if !__has_feature(objc_arc)
#error GCDWebServer requires ARC
#endif
#import <sys/stat.h>
#import "GCDWebServerPrivate.h"
#define kFileReadBufferSize (32 * 1024)
@interface GCDWebServerFileResponse () {
@private
NSString* _path;
NSUInteger _offset;
NSUInteger _size;
int _file;
}
@end
@implementation GCDWebServerFileResponse
+ (instancetype)responseWithFile:(NSString*)path {
return [[[self class] alloc] initWithFile:path];
}
+ (instancetype)responseWithFile:(NSString*)path isAttachment:(BOOL)attachment {
return [[[self class] alloc] initWithFile:path isAttachment:attachment];
}
+ (instancetype)responseWithFile:(NSString*)path byteRange:(NSRange)range {
return [[[self class] alloc] initWithFile:path byteRange:range];
}
+ (instancetype)responseWithFile:(NSString*)path byteRange:(NSRange)range isAttachment:(BOOL)attachment {
return [[[self class] alloc] initWithFile:path byteRange:range isAttachment:attachment];
}
- (instancetype)initWithFile:(NSString*)path {
return [self initWithFile:path byteRange:NSMakeRange(NSUIntegerMax, 0) isAttachment:NO];
}
- (instancetype)initWithFile:(NSString*)path isAttachment:(BOOL)attachment {
return [self initWithFile:path byteRange:NSMakeRange(NSUIntegerMax, 0) isAttachment:attachment];
}
- (instancetype)initWithFile:(NSString*)path byteRange:(NSRange)range {
return [self initWithFile:path byteRange:range isAttachment:NO];
}
static inline NSDate* _NSDateFromTimeSpec(const struct timespec* t) {
return [NSDate dateWithTimeIntervalSince1970:((NSTimeInterval)t->tv_sec + (NSTimeInterval)t->tv_nsec / 1000000000.0)];
}
- (instancetype)initWithFile:(NSString*)path byteRange:(NSRange)range isAttachment:(BOOL)attachment {
struct stat info;
if (lstat([path fileSystemRepresentation], &info) || !(info.st_mode & S_IFREG)) {
GWS_DNOT_REACHED();
return nil;
}
#ifndef __LP64__
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)
GWS_DNOT_REACHED();
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 {
range.length = MIN(range.length, fileSize);
range.location = fileSize - range.length;
}
if (range.length == 0) {
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])) {
_path = [path copy];
_offset = range.location;
_size = range.length;
if (hasByteRange) {
[self setStatusCode:kGCDWebServerHTTPStatusCode_PartialContent];
[self setValue:[NSString stringWithFormat:@"bytes %lu-%lu/%lu", (unsigned long)_offset, (unsigned long)(_offset + _size - 1), (unsigned long)fileSize] forAdditionalHeader:@"Content-Range"];
GWS_LOG_DEBUG(@"Using content bytes range [%lu-%lu] for file \"%@\"", (unsigned long)_offset, (unsigned long)(_offset + _size - 1), path);
}
if (attachment) {
NSString* fileName = [path lastPathComponent];
NSData* data = [[fileName stringByReplacingOccurrencesOfString:@"\"" withString:@""] dataUsingEncoding:NSISOLatin1StringEncoding allowLossyConversion:YES];
NSString* lossyFileName = data ? [[NSString alloc] initWithData:data encoding:NSISOLatin1StringEncoding] : nil;
if (lossyFileName) {
NSString* value = [NSString stringWithFormat:@"attachment; filename=\"%@\"; filename*=UTF-8''%@", lossyFileName, GCDWebServerEscapeURLString(fileName)];
[self setValue:value forAdditionalHeader:@"Content-Disposition"];
} else {
GWS_DNOT_REACHED();
}
}
self.contentType = GCDWebServerGetMimeTypeForExtension([_path pathExtension]);
self.contentLength = _size;
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];
}
return self;
}
- (BOOL)open:(NSError**)error {
_file = open([_path fileSystemRepresentation], O_NOFOLLOW | O_RDONLY);
if (_file <= 0) {
*error = GCDWebServerMakePosixError(errno);
return NO;
}
if (lseek(_file, _offset, SEEK_SET) != (off_t)_offset) {
*error = GCDWebServerMakePosixError(errno);
close(_file);
return NO;
}
return YES;
}
- (NSData*)readData:(NSError**)error {
size_t length = MIN((NSUInteger)kFileReadBufferSize, _size);
NSMutableData* data = [[NSMutableData alloc] initWithLength:length];
ssize_t result = read(_file, data.mutableBytes, length);
if (result < 0) {
*error = GCDWebServerMakePosixError(errno);
return nil;
}
if (result > 0) {
[data setLength:result];
_size -= result;
}
return data;
}
- (void)close {
close(_file);
}
- (NSString*)description {
NSMutableString* description = [NSMutableString stringWithString:[super description]];
[description appendFormat:@"\n\n{%@}", _path];
return description;
}
@end

View File

@@ -0,0 +1,78 @@
/*
Copyright (c) 2012-2014, 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"
/**
* The GCDWebServerStreamBlock is called to stream the data for the HTTP body.
* The block must return empty NSData when done or nil on error and set the
* "error" argument which is guaranteed to be non-NULL.
*/
typedef NSData* (^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 return empty NSData when done or nil on error and set the
* "error" argument which is guaranteed to be non-NULL.
*
* The block must regularly call "completionBlock" passing new streamed data.
* Eventually it must call "completionBlock" passing an empty NSData indicating
* the end of the stream has been reached, or pass nil and an NSError in case of
* error.
*/
typedef void (^GCDWebServerAsyncStreamBlock)(GCDWebServerBodyReaderCompletionBlock completionBlock);
/**
* The GCDWebServerStreamedResponse subclass of GCDWebServerResponse streams
* the body of the HTTP response using a GCD block.
*/
@interface GCDWebServerStreamedResponse : GCDWebServerResponse
/**
* 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

View File

@@ -0,0 +1,79 @@
/*
Copyright (c) 2012-2014, 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.
*/
#if !__has_feature(objc_arc)
#error GCDWebServer requires ARC
#endif
#import "GCDWebServerPrivate.h"
@interface GCDWebServerStreamedResponse () {
@private
GCDWebServerAsyncStreamBlock _block;
}
@end
@implementation GCDWebServerStreamedResponse
+ (instancetype)responseWithContentType:(NSString*)type streamBlock:(GCDWebServerStreamBlock)block {
return [[[self class] alloc] initWithContentType:type streamBlock:block];
}
+ (instancetype)responseWithContentType:(NSString*)type asyncStreamBlock:(GCDWebServerAsyncStreamBlock)block {
return [[[self class] alloc] initWithContentType:type asyncStreamBlock: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])) {
_block = [block copy];
self.contentType = type;
}
return self;
}
- (void)asyncReadDataWithCompletion:(GCDWebServerBodyReaderCompletionBlock)block {
_block(block);
}
- (NSString*)description {
NSMutableString* description = [NSMutableString stringWithString:[super description]];
[description appendString:@"\n\n<STREAM>"];
return description;
}
@end

View File

@@ -42,6 +42,11 @@
text-align: right;
}
.column-move {
width: 40px;
text-align: center;
}
.column-delete {
width: 40px;
text-align: center;
@@ -64,6 +69,16 @@
float: right;
}
#create-input {
width: 50%;
height: 20px;
}
#move-input {
width: 80%;
height: 20px;
}
/* Bootstrap overrides */
.btn:focus {

View File

@@ -65,7 +65,7 @@
<div id="alerts"></div>
<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&hellip;
<input id="fileupload" type="file" name="files[]" multiple>
</button>
@@ -95,12 +95,54 @@
</div>
<div class="modal fade" id="create-modal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal">&times;</button>
<h4 class="modal-title">Create Folder</h4>
</div>
<div class="modal-body">
<p>Please enter the name of the folder to be created:</p>
<form onsubmit="return false">
<input type="text" autocomplete="off" id="create-input">
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-default" data-dismiss="modal">Cancel</button>
<button type="button" class="btn btn-primary" id="create-confirm">Create Folder</button>
</div>
</div>
</div>
</div>
<div class="modal fade" id="move-modal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal">&times;</button>
<h4 class="modal-title">Move Item</h4>
</div>
<div class="modal-body">
<p>Please enter the new location for this item:</p>
<form onsubmit="return false">
<input type="text" autocomplete="off" id="move-input">
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-default" data-dismiss="modal">Cancel</button>
<button type="button" class="btn btn-primary" id="move-confirm">Move Item</button>
</div>
</div>
</div>
</div>
<script type="text/x-tmpl" id="template-listing">
<tr class="row-file" data-path="{%=o.path%}" data-name="{%=o.name%}">
<tr class="row-file">
<td class="column-icon">
{% if (o.size != null) { %}
<button type="button" class="btn btn-default btn-xs button-download">
<span class="glyphicon glyphicon-download"></span>
<span class="glyphicon glyphicon-download-alt"></span>
</button>
{% } else { %}
<button type="button" class="btn btn-default btn-xs button-open">
@@ -114,6 +156,11 @@
<p>{%=formatFileSize(o.size)%}</p>
{% } %}
</td>
<td class="column-move">
<button type="button" class="btn btn-default btn-xs button-move">
<span class="glyphicon glyphicon glyphicon-share-alt"></span>
</button>
</td>
<td class="column-delete">
<button type="button" class="btn btn-danger btn-xs button-delete">
<span class="glyphicon glyphicon-trash"></span>
@@ -132,7 +179,7 @@
<td class="column-path"><p>{%=o.path%}</p></td>
<td class="column-progress">
<div class="progress">
<div class="progress-bar" aria-valuenow="60" aria-valuemin="0" aria-valuemax="100" id="progress-bar"></div>
<div class="progress-bar" id="progress-bar"></div>
</div>
</ts>
</tr>
@@ -140,7 +187,7 @@
<script type="text/x-tmpl" id="template-alert">
<div class="alert alert-{%=o.level%} alert-dismissable">
<button type="button" class="close" data-dismiss="alert" aria-hidden="true">&times;</button>
<button type="button" class="close" data-dismiss="alert">&times;</button>
<strong>{%=o.title%}</strong>{%=o.description%}
</div>
</script>

View File

@@ -25,9 +25,11 @@
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
var ENTER_KEYCODE = 13;
var _path = null;
var _reloading = false;
var _pendingReloads = [];
var _reloadingDisabled = 0;
function formatFileSize(bytes) {
if (bytes >= 1000000000) {
@@ -47,15 +49,27 @@ function _showError(message, textStatus, errorThrown) {
}));
}
function _disableReloads() {
_reloadingDisabled += 1;
}
function _enableReloads() {
_reloadingDisabled -= 1;
if (_pendingReloads.length > 0) {
_reload(_pendingReloads.shift());
}
}
function _reload(path) {
if (_reloading) {
if (_reloadingDisabled) {
if ($.inArray(path, _pendingReloads) < 0) {
_pendingReloads.push(path);
}
return;
}
_reloading = true;
_disableReloads();
$.ajax({
url: 'list',
type: 'GET',
@@ -64,6 +78,7 @@ function _reload(path) {
}).fail(function(jqXHR, textStatus, errorThrown) {
_showError("Failed retrieving contents of \"" + path + "\"", textStatus, errorThrown);
}).done(function(data, textStatus, jqXHR) {
var scrollPosition = $(document).scrollTop();
if (path != _path) {
$("#path").empty();
@@ -77,7 +92,7 @@ function _reload(path) {
$("#path").append('<li data-path="' + subpath + '"><a>' + components[i] + '</a></li>');
}
$("#path > li").click(function(event) {
_reload($(this).attr("data-path"));
_reload($(this).data("path"));
event.preventDefault();
});
$("#path").append('<li class="active">' + components[components.length - 1] + '</li>');
@@ -87,13 +102,13 @@ function _reload(path) {
$("#listing").empty();
for (var i = 0, file; file = data[i]; ++i) {
$("#listing").append(tmpl("template-listing", file));
$(tmpl("template-listing", file)).data(file).appendTo("#listing");
}
$(".edit").editable(function(value, settings) {
var name = $(this).parent().parent().attr("data-name");
var name = $(this).parent().parent().data("name");
if (value != name) {
var path = $(this).parent().parent().attr("data-path");
var path = $(this).parent().parent().data("path");
$.ajax({
url: 'move',
type: 'POST',
@@ -107,23 +122,42 @@ function _reload(path) {
}
return value;
}, {
onedit: function(settings, original) {
_disableReloads();
},
onsubmit: function(settings, original) {
_enableReloads();
},
onreset: function(settings, original) {
_enableReloads();
},
tooltip: 'Click to rename...'
});
$(".button-download").click(function(event) {
var path = $(this).parent().parent().attr("data-path");
var path = $(this).parent().parent().data("path");
setTimeout(function() {
window.location = "download?path=" + encodeURIComponent(path);
}, 0);
});
$(".button-open").click(function(event) {
var path = $(this).parent().parent().attr("data-path");
var path = $(this).parent().parent().data("path");
_reload(path);
});
$(".button-move").click(function(event) {
var path = $(this).parent().parent().data("path");
if (path[path.length - 1] == "/") {
path = path.slice(0, path.length - 1);
}
$("#move-input").data("path", path);
$("#move-input").val(path);
$("#move-modal").modal("show");
});
$(".button-delete").click(function(event) {
var path = $(this).parent().parent().attr("data-path");
var path = $(this).parent().parent().data("path");
$.ajax({
url: 'delete',
type: 'POST',
@@ -136,16 +170,25 @@ function _reload(path) {
});
});
$(document).scrollTop(scrollPosition);
}).always(function() {
_reloading = false;
if (_pendingReloads.length > 0) {
_reload(_pendingReloads.shift());
}
_enableReloads();
});
}
$(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({
dropZone: $(document),
pasteZone: null,
@@ -202,9 +245,26 @@ $(document).ready(function() {
});
$("#create-input").keypress(function(event) {
if (event.keyCode == ENTER_KEYCODE) {
$("#create-confirm").click();
};
});
$("#create-modal").on("shown.bs.modal", function(event) {
$("#create-input").focus();
$("#create-input").select();
});
$("#create-folder").click(function(event) {
var name = prompt("Please enter folder name:", "Untitled folder");
if ((name != null) && (name != "")) {
$("#create-input").val("Untitled folder");
$("#create-modal").modal("show");
});
$("#create-confirm").click(function(event) {
$("#create-modal").modal("hide");
var name = $("#create-input").val();
if (name != "") {
$.ajax({
url: 'create',
type: 'POST',
@@ -218,6 +278,35 @@ $(document).ready(function() {
}
});
$("#move-input").keypress(function(event) {
if (event.keyCode == ENTER_KEYCODE) {
$("#move-confirm").click();
};
});
$("#move-modal").on("shown.bs.modal", function(event) {
$("#move-input").focus();
$("#move-input").select();
})
$("#move-confirm").click(function(event) {
$("#move-modal").modal("hide");
var oldPath = $("#move-input").data("path");
var newPath = $("#move-input").val();
if ((newPath != "") && (newPath[0] == "/") && (newPath != oldPath)) {
$.ajax({
url: 'move',
type: 'POST',
data: {oldPath: oldPath, newPath: newPath},
dataType: 'json'
}).fail(function(jqXHR, textStatus, errorThrown) {
_showError("Failed moving \"" + oldPath + "\" to \"" + newPath + "\"", textStatus, errorThrown);
}).always(function() {
_reload(_path);
});
}
});
$("#reload").click(function(event) {
_reload(_path);
});

View File

@@ -29,29 +29,169 @@
@class GCDWebUploader;
@protocol GCDWebUploaderDelegate <NSObject>
/**
* Delegate methods for GCDWebUploader.
*
* @warning These methods are always called on the main thread in a serialized way.
*/
@protocol GCDWebUploaderDelegate <GCDWebServerDelegate>
@optional
/**
* This method is called whenever a file has been downloaded.
*/
- (void)webUploader:(GCDWebUploader*)uploader didDownloadFileAtPath:(NSString*)path;
/**
* This method is called whenever a file has been uploaded.
*/
- (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;
/**
* This method is called whenever a file or directory has been deleted.
*/
- (void)webUploader:(GCDWebUploader*)uploader didDeleteItemAtPath:(NSString*)path;
/**
* This method is called whenever a directory has been created.
*/
- (void)webUploader:(GCDWebUploader*)uploader didCreateDirectoryAtPath:(NSString*)path;
@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
/**
* Returns the upload directory as specified when the uploader was initialized.
*/
@property(nonatomic, readonly) NSString* uploadDirectory;
/**
* Sets the delegate for the uploader.
*/
@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
@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, copy) NSString* prologue; // Default is mini help (must be raw HTML)
@property(nonatomic, copy) NSString* epilogue; // Default is mini help (must be raw HTML)
@property(nonatomic, copy) NSString* footer; // Default is application name and version (must be HTML escaped)
- (id)initWithUploadDirectory:(NSString*)path;
/**
* 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* 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 "&amp;".
*/
@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 "&amp;".
*/
@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 "&amp;".
*/
@property(nonatomic, copy) NSString* footer;
/**
* This method is the designated initializer for the class.
*/
- (instancetype)initWithUploadDirectory:(NSString*)path;
@end
/**
* Hooks to customize the behavior of GCDWebUploader.
*
* @warning These methods can be called on any GCD thread.
*/
@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
/**
* This method is called to check if a file upload is allowed to complete.
* 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

View File

@@ -25,6 +25,10 @@
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
#if !__has_feature(objc_arc)
#error GCDWebUploader requires ARC
#endif
#import <TargetConditionals.h>
#if TARGET_OS_IPHONE
#import <UIKit/UIKit.h>
@@ -34,12 +38,19 @@
#import "GCDWebUploader.h"
#import "GCDWebServerDataRequest.h"
#import "GCDWebServerMultiPartFormRequest.h"
#import "GCDWebServerURLEncodedFormRequest.h"
#import "GCDWebServerDataResponse.h"
#import "GCDWebServerErrorResponse.h"
#import "GCDWebServerFileResponse.h"
@interface GCDWebUploader () {
@private
NSString* _uploadDirectory;
id<GCDWebUploaderDelegate> delegate;
NSArray* _allowedExtensions;
BOOL _showHidden;
BOOL _allowHidden;
NSString* _title;
NSString* _header;
NSString* _prologue;
@@ -48,10 +59,12 @@
}
@end
@implementation GCDWebUploader
@implementation GCDWebUploader (Methods)
@synthesize uploadDirectory=_uploadDirectory, delegate=_delegate, allowedFileExtensions=_allowedExtensions, showHiddenFiles=_showHidden,
title=_title, header=_header, prologue=_prologue, epilogue=_epilogue, footer=_footer;
// Must match implementation in GCDWebDAVServer
- (BOOL)_checkSandboxedPath:(NSString*)path {
return [[path stringByStandardizingPath] hasPrefix:_uploadDirectory];
}
- (BOOL)_checkFileExtension:(NSString*)fileName {
if (_allowedExtensions && ![_allowedExtensions containsObject:[[fileName pathExtension] lowercaseString]]) {
@@ -78,17 +91,215 @@
return path;
}
- (id)initWithUploadDirectory:(NSString*)path {
- (GCDWebServerResponse*)listDirectory:(GCDWebServerRequest*)request {
NSString* relativePath = [[request query] objectForKey:@"path"];
NSString* absolutePath = [_uploadDirectory stringByAppendingPathComponent:relativePath];
BOOL isDirectory = NO;
if (![self _checkSandboxedPath:absolutePath] || ![[NSFileManager defaultManager] fileExistsAtPath:absolutePath isDirectory:&isDirectory]) {
return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_NotFound message:@"\"%@\" does not exist", relativePath];
}
if (!isDirectory) {
return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_BadRequest message:@"\"%@\" is not a directory", relativePath];
}
NSString* directoryName = [absolutePath lastPathComponent];
if (!_allowHidden && [directoryName hasPrefix:@"."]) {
return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_Forbidden message:@"Listing directory name \"%@\" is not allowed", directoryName];
}
NSError* error = nil;
NSArray* contents = [[NSFileManager defaultManager] contentsOfDirectoryAtPath:absolutePath error:&error];
if (contents == nil) {
return [GCDWebServerErrorResponse responseWithServerError:kGCDWebServerHTTPStatusCode_InternalServerError underlyingError:error message:@"Failed listing directory \"%@\"", relativePath];
}
NSMutableArray* array = [NSMutableArray array];
for (NSString* item in [contents sortedArrayUsingSelector:@selector(localizedStandardCompare:)]) {
if (_allowHidden || ![item hasPrefix:@"."]) {
NSDictionary* attributes = [[NSFileManager defaultManager] attributesOfItemAtPath:[absolutePath stringByAppendingPathComponent:item] error:NULL];
NSString* type = [attributes objectForKey:NSFileType];
if ([type isEqualToString:NSFileTypeRegular] && [self _checkFileExtension:item]) {
[array addObject:@{
@"path": [relativePath stringByAppendingPathComponent:item],
@"name": item,
@"size": [attributes objectForKey:NSFileSize]
}];
} else if ([type isEqualToString:NSFileTypeDirectory]) {
[array addObject:@{
@"path": [[relativePath stringByAppendingPathComponent:item] stringByAppendingString:@"/"],
@"name": item
}];
}
}
}
return [GCDWebServerDataResponse responseWithJSONObject:array];
}
- (GCDWebServerResponse*)downloadFile:(GCDWebServerRequest*)request {
NSString* relativePath = [[request query] objectForKey:@"path"];
NSString* absolutePath = [_uploadDirectory stringByAppendingPathComponent:relativePath];
BOOL isDirectory = NO;
if (![self _checkSandboxedPath:absolutePath] || ![[NSFileManager defaultManager] fileExistsAtPath:absolutePath isDirectory:&isDirectory]) {
return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_NotFound message:@"\"%@\" does not exist", relativePath];
}
if (isDirectory) {
return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_BadRequest message:@"\"%@\" is a directory", relativePath];
}
NSString* fileName = [absolutePath lastPathComponent];
if (([fileName hasPrefix:@"."] && !_allowHidden) || ![self _checkFileExtension:fileName]) {
return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_Forbidden message:@"Downlading file name \"%@\" is not allowed", fileName];
}
if ([self.delegate respondsToSelector:@selector(webUploader:didDownloadFileAtPath: )]) {
dispatch_async(dispatch_get_main_queue(), ^{
[self.delegate webUploader:self didDownloadFileAtPath:absolutePath];
});
}
return [GCDWebServerFileResponse responseWithFile:absolutePath isAttachment:YES];
}
- (GCDWebServerResponse*)uploadFile:(GCDWebServerMultiPartFormRequest*)request {
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)
GCDWebServerMultiPartFile* file = [request firstFileForControlName:@"files[]"];
if ((!_allowHidden && [file.fileName hasPrefix:@"."]) || ![self _checkFileExtension:file.fileName]) {
return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_Forbidden message:@"Uploaded file name \"%@\" is not allowed", file.fileName];
}
NSString* relativePath = [[request firstArgumentForControlName:@"path"] string];
NSString* absolutePath = [self _uniquePathForPath:[[_uploadDirectory stringByAppendingPathComponent:relativePath] stringByAppendingPathComponent:file.fileName]];
if (![self _checkSandboxedPath:absolutePath]) {
return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_NotFound message:@"\"%@\" does not exist", relativePath];
}
if (![self shouldUploadFileAtPath:absolutePath withTemporaryFile:file.temporaryPath]) {
return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_Forbidden message:@"Uploading file \"%@\" to \"%@\" is not permitted", file.fileName, relativePath];
}
NSError* error = nil;
if (![[NSFileManager defaultManager] moveItemAtPath:file.temporaryPath toPath:absolutePath error:&error]) {
return [GCDWebServerErrorResponse responseWithServerError:kGCDWebServerHTTPStatusCode_InternalServerError underlyingError:error message:@"Failed moving uploaded file to \"%@\"", relativePath];
}
if ([self.delegate respondsToSelector:@selector(webUploader:didUploadFileAtPath:)]) {
dispatch_async(dispatch_get_main_queue(), ^{
[self.delegate webUploader:self didUploadFileAtPath:absolutePath];
});
}
return [GCDWebServerDataResponse responseWithJSONObject:@{} contentType:contentType];
}
- (GCDWebServerResponse*)moveItem:(GCDWebServerURLEncodedFormRequest*)request {
NSString* oldRelativePath = [request.arguments objectForKey:@"oldPath"];
NSString* oldAbsolutePath = [_uploadDirectory stringByAppendingPathComponent:oldRelativePath];
BOOL isDirectory = NO;
if (![self _checkSandboxedPath:oldAbsolutePath] || ![[NSFileManager defaultManager] fileExistsAtPath:oldAbsolutePath isDirectory:&isDirectory]) {
return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_NotFound message:@"\"%@\" does not exist", oldRelativePath];
}
NSString* newRelativePath = [request.arguments objectForKey:@"newPath"];
NSString* newAbsolutePath = [self _uniquePathForPath:[_uploadDirectory stringByAppendingPathComponent:newRelativePath]];
if (![self _checkSandboxedPath:newAbsolutePath]) {
return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_NotFound message:@"\"%@\" does not exist", newRelativePath];
}
NSString* itemName = [newAbsolutePath lastPathComponent];
if ((!_allowHidden && [itemName hasPrefix:@"."]) || (!isDirectory && ![self _checkFileExtension:itemName])) {
return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_Forbidden message:@"Moving to item name \"%@\" is not allowed", itemName];
}
if (![self shouldMoveItemFromPath:oldAbsolutePath toPath:newAbsolutePath]) {
return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_Forbidden message:@"Moving \"%@\" to \"%@\" is not permitted", oldRelativePath, newRelativePath];
}
NSError* error = nil;
if (![[NSFileManager defaultManager] moveItemAtPath:oldAbsolutePath toPath:newAbsolutePath error:&error]) {
return [GCDWebServerErrorResponse responseWithServerError:kGCDWebServerHTTPStatusCode_InternalServerError underlyingError:error message:@"Failed moving \"%@\" to \"%@\"", oldRelativePath, newRelativePath];
}
if ([self.delegate respondsToSelector:@selector(webUploader:didMoveItemFromPath:toPath:)]) {
dispatch_async(dispatch_get_main_queue(), ^{
[self.delegate webUploader:self didMoveItemFromPath:oldAbsolutePath toPath:newAbsolutePath];
});
}
return [GCDWebServerDataResponse responseWithJSONObject:@{}];
}
- (GCDWebServerResponse*)deleteItem:(GCDWebServerURLEncodedFormRequest*)request {
NSString* relativePath = [request.arguments objectForKey:@"path"];
NSString* absolutePath = [_uploadDirectory stringByAppendingPathComponent:relativePath];
BOOL isDirectory = NO;
if (![self _checkSandboxedPath:absolutePath] || ![[NSFileManager defaultManager] fileExistsAtPath:absolutePath isDirectory:&isDirectory]) {
return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_NotFound message:@"\"%@\" does not exist", relativePath];
}
NSString* itemName = [absolutePath lastPathComponent];
if (([itemName hasPrefix:@"."] && !_allowHidden) || (!isDirectory && ![self _checkFileExtension:itemName])) {
return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_Forbidden message:@"Deleting item name \"%@\" is not allowed", itemName];
}
if (![self shouldDeleteItemAtPath:absolutePath]) {
return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_Forbidden message:@"Deleting \"%@\" is not permitted", relativePath];
}
NSError* error = nil;
if (![[NSFileManager defaultManager] removeItemAtPath:absolutePath error:&error]) {
return [GCDWebServerErrorResponse responseWithServerError:kGCDWebServerHTTPStatusCode_InternalServerError underlyingError:error message:@"Failed deleting \"%@\"", relativePath];
}
if ([self.delegate respondsToSelector:@selector(webUploader:didDeleteItemAtPath:)]) {
dispatch_async(dispatch_get_main_queue(), ^{
[self.delegate webUploader:self didDeleteItemAtPath:absolutePath];
});
}
return [GCDWebServerDataResponse responseWithJSONObject:@{}];
}
- (GCDWebServerResponse*)createDirectory:(GCDWebServerURLEncodedFormRequest*)request {
NSString* relativePath = [request.arguments objectForKey:@"path"];
NSString* absolutePath = [self _uniquePathForPath:[_uploadDirectory stringByAppendingPathComponent:relativePath]];
if (![self _checkSandboxedPath:absolutePath]) {
return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_NotFound message:@"\"%@\" does not exist", relativePath];
}
NSString* directoryName = [absolutePath lastPathComponent];
if (!_allowHidden && [directoryName hasPrefix:@"."]) {
return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_Forbidden message:@"Creating directory name \"%@\" is not allowed", directoryName];
}
if (![self shouldCreateDirectoryAtPath:absolutePath]) {
return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_Forbidden message:@"Creating directory \"%@\" is not permitted", relativePath];
}
NSError* error = nil;
if (![[NSFileManager defaultManager] createDirectoryAtPath:absolutePath withIntermediateDirectories:NO attributes:nil error:&error]) {
return [GCDWebServerErrorResponse responseWithServerError:kGCDWebServerHTTPStatusCode_InternalServerError underlyingError:error message:@"Failed creating directory \"%@\"", relativePath];
}
if ([self.delegate respondsToSelector:@selector(webUploader:didCreateDirectoryAtPath:)]) {
dispatch_async(dispatch_get_main_queue(), ^{
[self.delegate webUploader:self didCreateDirectoryAtPath:absolutePath];
});
}
return [GCDWebServerDataResponse responseWithJSONObject:@{}];
}
@end
@implementation GCDWebUploader
@synthesize uploadDirectory=_uploadDirectory, allowedFileExtensions=_allowedExtensions, allowHiddenItems=_allowHidden,
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];
GCDWebUploader* __unsafe_unretained uploader = self; // Avoid retain-cycles with self
GCDWebUploader* __unsafe_unretained server = self;
// Resource files
[self addGETHandlerForBasePath:@"/" directoryPath:[siteBundle resourcePath] indexFilename:nil cacheAge:3600 allowRangeRequests:NO];
@@ -99,33 +310,43 @@
#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 = uploader.title;
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 = uploader.header;
NSString* header = server.header;
if (header == nil) {
header = title;
}
NSString* prologue = uploader.prologue;
NSString* prologue = server.prologue;
if (prologue == nil) {
prologue = [siteBundle localizedStringForKey:@"PROLOGUE" value:@"" table:nil];
}
NSString* epilogue = uploader.epilogue;
NSString* epilogue = server.epilogue;
if (epilogue == nil) {
epilogue = [siteBundle localizedStringForKey:@"EPILOGUE" value:@"" table:nil];
}
NSString* footer = uploader.footer;
NSString* footer = server.footer;
if (footer == nil) {
footer = [NSString stringWithFormat:[siteBundle localizedStringForKey:@"FOOTER_FORMAT" value:@"" table:nil],
[[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleDisplayName"],
[[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleShortVersionString"]];
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:@{
@@ -141,208 +362,38 @@
// File listing
[self addHandlerForMethod:@"GET" path:@"/list" requestClass:[GCDWebServerRequest class] processBlock:^GCDWebServerResponse *(GCDWebServerRequest* request) {
NSString* relativePath = [[request query] objectForKey:@"path"];
NSString* absolutePath = [uploader.uploadDirectory stringByAppendingPathComponent:relativePath];
BOOL isDirectory;
if ([[NSFileManager defaultManager] fileExistsAtPath:absolutePath isDirectory:&isDirectory]) {
if (isDirectory) {
BOOL showHidden = uploader.showHiddenFiles;
NSArray* contents = [[NSFileManager defaultManager] contentsOfDirectoryAtPath:absolutePath error:NULL];
if (contents) {
NSMutableArray* array = [NSMutableArray array];
for (NSString* item in [contents sortedArrayUsingSelector:@selector(localizedStandardCompare:)]) {
if (showHidden || ![item hasPrefix:@"."]) {
NSDictionary* attributes = [[NSFileManager defaultManager] attributesOfItemAtPath:[absolutePath stringByAppendingPathComponent:item] error:NULL];
NSString* type = [attributes objectForKey:NSFileType];
if ([type isEqualToString:NSFileTypeRegular] && [uploader _checkFileExtension:item]) {
[array addObject:@{
@"path": [relativePath stringByAppendingPathComponent:item],
@"name": item,
@"size": [attributes objectForKey:NSFileSize]
}];
} else if ([type isEqualToString:NSFileTypeDirectory]) {
[array addObject:@{
@"path": [[relativePath stringByAppendingPathComponent:item] stringByAppendingString:@"/"],
@"name": item
}];
}
}
}
return [GCDWebServerDataResponse responseWithJSONObject:array];
} else {
return [GCDWebServerResponse responseWithStatusCode:500];
}
} else {
return [GCDWebServerResponse responseWithStatusCode:400];
}
} else {
return [GCDWebServerResponse responseWithStatusCode:404];
}
return [server listDirectory:request];
}];
// File download
[self addHandlerForMethod:@"GET" path:@"/download" requestClass:[GCDWebServerRequest class] processBlock:^GCDWebServerResponse *(GCDWebServerRequest* request) {
NSString* relativePath = [[request query] objectForKey:@"path"];
NSString* absolutePath = [uploader.uploadDirectory stringByAppendingPathComponent:relativePath];
BOOL isDirectory;
if ([[NSFileManager defaultManager] fileExistsAtPath:absolutePath isDirectory:&isDirectory]) {
if (isDirectory) {
return [GCDWebServerResponse responseWithStatusCode:400];
} else {
if ([uploader.delegate respondsToSelector:@selector(webUploader:didDownloadFileAtPath: )]) {
dispatch_async(dispatch_get_main_queue(), ^{
[uploader.delegate webUploader:uploader didDownloadFileAtPath:absolutePath];
});
}
return [GCDWebServerFileResponse responseWithFile:absolutePath isAttachment:YES];
}
} else {
return [GCDWebServerResponse responseWithStatusCode:404];
}
return [server downloadFile:request];
}];
// File upload
[self addHandlerForMethod:@"POST" path:@"/upload" requestClass:[GCDWebServerMultiPartFormRequest class] processBlock:^GCDWebServerResponse *(GCDWebServerRequest* request) {
// Required when using iFrame transport (see https://github.com/blueimp/jQuery-File-Upload/wiki/Setup)
NSRange range = [[request.headers objectForKey:@"Accept"] rangeOfString:@"application/json" options:NSCaseInsensitiveSearch];
NSString* contentType = (range.location != NSNotFound ? @"application/json" : @"text/plain; charset=utf-8");
GCDWebServerMultiPartFile* file = [[(GCDWebServerMultiPartFormRequest*)request files] objectForKey:@"files[]"];
if ((![file.fileName hasPrefix:@"."] || uploader.showHiddenFiles) && [uploader _checkFileExtension:file.fileName]) {
NSString* relativePath = [(GCDWebServerMultiPartArgument*)[[(GCDWebServerURLEncodedFormRequest*)request arguments] objectForKey:@"path"] string];
NSString* absolutePath = [uploader _uniquePathForPath:[[uploader.uploadDirectory stringByAppendingPathComponent:relativePath] stringByAppendingPathComponent:file.fileName]];
if ([uploader shouldUploadFileAtPath:absolutePath withTemporaryFile:file.temporaryPath]) {
NSError* error = nil;
if ([[NSFileManager defaultManager] moveItemAtPath:file.temporaryPath toPath:absolutePath error:&error]) {
if ([uploader.delegate respondsToSelector:@selector(webUploader:didUploadFileAtPath:)]) {
dispatch_async(dispatch_get_main_queue(), ^{
[uploader.delegate webUploader:uploader didUploadFileAtPath:absolutePath];
});
}
return [GCDWebServerDataResponse responseWithJSONObject:@{} contentType:contentType];
} else {
return [GCDWebServerResponse responseWithStatusCode:500];
}
} else {
return [GCDWebServerResponse responseWithStatusCode:403];
}
} else {
return [GCDWebServerResponse responseWithStatusCode:400];
}
return [server uploadFile:(GCDWebServerMultiPartFormRequest*)request];
}];
// File and folder moving
[self addHandlerForMethod:@"POST" path:@"/move" requestClass:[GCDWebServerURLEncodedFormRequest class] processBlock:^GCDWebServerResponse *(GCDWebServerRequest* request) {
NSString* oldRelativePath = [[(GCDWebServerURLEncodedFormRequest*)request arguments] objectForKey:@"oldPath"];
NSString* oldAbsolutePath = [uploader.uploadDirectory stringByAppendingPathComponent:oldRelativePath];
BOOL isDirectory;
if ([[NSFileManager defaultManager] fileExistsAtPath:oldAbsolutePath isDirectory:&isDirectory]) {
NSString* newRelativePath = [[(GCDWebServerURLEncodedFormRequest*)request arguments] objectForKey:@"newPath"];
if (!uploader.showHiddenFiles) {
for (NSString* component in [newRelativePath pathComponents]) {
if ([component hasPrefix:@"."]) {
return [GCDWebServerResponse responseWithStatusCode:400];
}
}
}
if (!isDirectory && ![uploader _checkFileExtension:newRelativePath]) {
return [GCDWebServerResponse responseWithStatusCode:400];
}
NSString* newAbsolutePath = [uploader _uniquePathForPath:[uploader.uploadDirectory stringByAppendingPathComponent:newRelativePath]];
if ([uploader shouldMoveItemFromPath:oldAbsolutePath toPath:newAbsolutePath]) {
if ([[NSFileManager defaultManager] moveItemAtPath:oldAbsolutePath toPath:newAbsolutePath error:NULL]) {
if ([uploader.delegate respondsToSelector:@selector(webUploader:didMoveItemFromPath:toPath:)]) {
dispatch_async(dispatch_get_main_queue(), ^{
[uploader.delegate webUploader:uploader didMoveItemFromPath:oldAbsolutePath toPath:newAbsolutePath];
});
}
return [GCDWebServerDataResponse responseWithJSONObject:@{}];
} else {
return [GCDWebServerResponse responseWithStatusCode:500];
}
} else {
return [GCDWebServerResponse responseWithStatusCode:403];
}
} else {
return [GCDWebServerResponse responseWithStatusCode:404];
}
return [server moveItem:(GCDWebServerURLEncodedFormRequest*)request];
}];
// File deletion
// File and folder deletion
[self addHandlerForMethod:@"POST" path:@"/delete" requestClass:[GCDWebServerURLEncodedFormRequest class] processBlock:^GCDWebServerResponse *(GCDWebServerRequest* request) {
NSString* relativePath = [[(GCDWebServerURLEncodedFormRequest*)request arguments] objectForKey:@"path"];
NSString* absolutePath = [uploader.uploadDirectory stringByAppendingPathComponent:relativePath];
if ([[NSFileManager defaultManager] fileExistsAtPath:absolutePath]) {
if ([[NSFileManager defaultManager] removeItemAtPath:absolutePath error:NULL]) {
if ([uploader.delegate respondsToSelector:@selector(webUploader:didDeleteItemAtPath:)]) {
dispatch_async(dispatch_get_main_queue(), ^{
[uploader.delegate webUploader:uploader didDeleteItemAtPath:absolutePath];
});
}
return [GCDWebServerDataResponse responseWithJSONObject:@{}];
} else {
return [GCDWebServerResponse responseWithStatusCode:500];
}
} else {
return [GCDWebServerResponse responseWithStatusCode:404];
}
return [server deleteItem:(GCDWebServerURLEncodedFormRequest*)request];
}];
// Directory creation
[self addHandlerForMethod:@"POST" path:@"/create" requestClass:[GCDWebServerURLEncodedFormRequest class] processBlock:^GCDWebServerResponse *(GCDWebServerRequest* request) {
NSString* relativePath = [[(GCDWebServerURLEncodedFormRequest*)request arguments] objectForKey:@"path"];
if (!uploader.showHiddenFiles) {
for (NSString* component in [relativePath pathComponents]) {
if ([component hasPrefix:@"."]) {
return [GCDWebServerResponse responseWithStatusCode:400];
}
}
}
NSString* absolutePath = [uploader _uniquePathForPath:[uploader.uploadDirectory stringByAppendingPathComponent:relativePath]];
if ([[NSFileManager defaultManager] createDirectoryAtPath:absolutePath withIntermediateDirectories:YES attributes:nil error:NULL]) {
if ([uploader.delegate respondsToSelector:@selector(webUploader:didCreateDirectoryAtPath:)]) {
dispatch_async(dispatch_get_main_queue(), ^{
[uploader.delegate webUploader:uploader didCreateDirectoryAtPath:absolutePath];
});
}
return [GCDWebServerDataResponse responseWithJSONObject:@{}];
} else {
return [GCDWebServerResponse responseWithStatusCode:500];
}
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)
@@ -355,4 +406,12 @@
return YES;
}
- (BOOL)shouldDeleteItemAtPath:(NSString*)path {
return YES;
}
- (BOOL)shouldCreateDirectoryAtPath:(NSString*)path {
return YES;
}
@end

View File

@@ -25,24 +25,187 @@
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
#import <libgen.h>
#import "GCDWebServer.h"
#import "GCDWebServerDataRequest.h"
#import "GCDWebServerURLEncodedFormRequest.h"
#import "GCDWebServerMultiPartFormRequest.h"
#import "GCDWebServerDataResponse.h"
#import "GCDWebServerStreamedResponse.h"
#import "GCDWebDAVServer.h"
#import "GCDWebUploader.h"
#ifndef __GCDWEBSERVER_ENABLE_TESTING__
#error __GCDWEBSERVER_ENABLE_TESTING__ must be defined
#endif
typedef enum {
kMode_WebServer = 0,
kMode_HTMLPage,
kMode_HTMLForm,
kMode_HTMLFileUpload,
kMode_WebDAV,
kMode_WebUploader,
kMode_StreamingResponse,
kMode_AsyncResponse
} Mode;
@interface Delegate : NSObject <GCDWebServerDelegate, GCDWebDAVServerDelegate, GCDWebUploaderDelegate>
@end
@implementation Delegate
- (void)_logDelegateCall:(SEL)selector {
fprintf(stdout, "<DELEGATE METHOD \"%s\" CALLED>\n", [NSStringFromSelector(selector) UTF8String]);
}
- (void)webServerDidStart:(GCDWebServer*)server {
[self _logDelegateCall:_cmd];
}
- (void)webServerDidCompleteBonjourRegistration:(GCDWebServer*)server {
[self _logDelegateCall:_cmd];
}
- (void)webServerDidConnect:(GCDWebServer*)server {
[self _logDelegateCall:_cmd];
}
- (void)webServerDidDisconnect:(GCDWebServer*)server {
[self _logDelegateCall:_cmd];
}
- (void)webServerDidStop:(GCDWebServer*)server {
[self _logDelegateCall:_cmd];
}
- (void)davServer:(GCDWebDAVServer*)server didDownloadFileAtPath:(NSString*)path {
[self _logDelegateCall:_cmd];
}
- (void)davServer:(GCDWebDAVServer*)server didUploadFileAtPath:(NSString*)path {
[self _logDelegateCall:_cmd];
}
- (void)davServer:(GCDWebDAVServer*)server didMoveItemFromPath:(NSString*)fromPath toPath:(NSString*)toPath {
[self _logDelegateCall:_cmd];
}
- (void)davServer:(GCDWebDAVServer*)server didCopyItemFromPath:(NSString*)fromPath toPath:(NSString*)toPath {
[self _logDelegateCall:_cmd];
}
- (void)davServer:(GCDWebDAVServer*)server didDeleteItemAtPath:(NSString*)path {
[self _logDelegateCall:_cmd];
}
- (void)davServer:(GCDWebDAVServer*)server didCreateDirectoryAtPath:(NSString*)path {
[self _logDelegateCall:_cmd];
}
- (void)webUploader:(GCDWebUploader*)uploader didDownloadFileAtPath:(NSString*)path {
[self _logDelegateCall:_cmd];
}
- (void)webUploader:(GCDWebUploader*)uploader didUploadFileAtPath:(NSString*)path {
[self _logDelegateCall:_cmd];
}
- (void)webUploader:(GCDWebUploader*)uploader didMoveItemFromPath:(NSString*)fromPath toPath:(NSString*)toPath {
[self _logDelegateCall:_cmd];
}
- (void)webUploader:(GCDWebUploader*)uploader didDeleteItemAtPath:(NSString*)path {
[self _logDelegateCall:_cmd];
}
- (void)webUploader:(GCDWebUploader*)uploader didCreateDirectoryAtPath:(NSString*)path {
[self _logDelegateCall:_cmd];
}
@end
int main(int argc, const char* argv[]) {
BOOL success = NO;
int mode = (argc == 2 ? MIN(MAX(atoi(argv[1]), 0), 3) : 0);
int result = -1;
@autoreleasepool {
Mode mode = kMode_WebServer;
BOOL recording = NO;
NSString* rootDirectory = NSHomeDirectory();
NSString* testDirectory = nil;
NSString* authenticationMethod = nil;
NSString* authenticationRealm = nil;
NSString* authenticationUser = nil;
NSString* authenticationPassword = nil;
if (argc == 1) {
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]\n\n", basename((char*)argv[0]));
} else {
for (int i = 1; i < argc; ++i) {
if (argv[i][0] != '-') {
continue;
}
if (!strcmp(argv[i], "-mode") && (i + 1 < argc)) {
++i;
if (!strcmp(argv[i], "webServer")) {
mode = kMode_WebServer;
} else if (!strcmp(argv[i], "htmlPage")) {
mode = kMode_HTMLPage;
} else if (!strcmp(argv[i], "htmlForm")) {
mode = kMode_HTMLForm;
} else if (!strcmp(argv[i], "htmlFileUpload")) {
mode = kMode_HTMLFileUpload;
} else if (!strcmp(argv[i], "webDAV")) {
mode = kMode_WebDAV;
} else if (!strcmp(argv[i], "webUploader")) {
mode = kMode_WebUploader;
} else if (!strcmp(argv[i], "streamingResponse")) {
mode = kMode_StreamingResponse;
} else if (!strcmp(argv[i], "asyncResponse")) {
mode = kMode_AsyncResponse;
}
} else if (!strcmp(argv[i], "-record")) {
recording = YES;
} else if (!strcmp(argv[i], "-root") && (i + 1 < argc)) {
++i;
rootDirectory = [[[NSFileManager defaultManager] stringWithFileSystemRepresentation:argv[i] length:strlen(argv[i])] stringByStandardizingPath];
} else if (!strcmp(argv[i], "-tests") && (i + 1 < argc)) {
++i;
testDirectory = [[[NSFileManager defaultManager] stringWithFileSystemRepresentation:argv[i] length:strlen(argv[i])] stringByStandardizingPath];
} 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]];
}
}
}
GCDWebServer* webServer = nil;
switch (mode) {
// Simply serve contents of home directory
case 0: {
case kMode_WebServer: {
fprintf(stdout, "Running in Web Server mode from \"%s\"", [rootDirectory UTF8String]);
webServer = [[GCDWebServer alloc] init];
[webServer addGETHandlerForBasePath:@"/" directoryPath:NSHomeDirectory() indexFilename:nil cacheAge:0 allowRangeRequests:YES];
[webServer addGETHandlerForBasePath:@"/" directoryPath:rootDirectory indexFilename:nil cacheAge:0 allowRangeRequests:YES];
break;
}
// Renders a HTML page
case 1: {
case kMode_HTMLPage: {
fprintf(stdout, "Running in HTML Page mode");
webServer = [[GCDWebServer alloc] init];
[webServer addDefaultHandlerForMethod:@"GET"
requestClass:[GCDWebServerRequest class]
@@ -55,7 +218,8 @@ int main(int argc, const char* argv[]) {
}
// Implements an HTML form
case 2: {
case kMode_HTMLForm: {
fprintf(stdout, "Running in HTML Form mode");
webServer = [[GCDWebServer alloc] init];
[webServer addHandlerForMethod:@"GET"
path:@"/"
@@ -86,16 +250,158 @@ int main(int argc, const char* argv[]) {
break;
}
case 3: {
webServer = [[GCDWebUploader alloc] initWithUploadDirectory:@"/tmp"];
// Implements HTML file upload
case kMode_HTMLFileUpload: {
fprintf(stdout, "Running in HTML File Upload mode");
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"
path:@"/"
requestClass:[GCDWebServerRequest class]
processBlock:^GCDWebServerResponse *(GCDWebServerRequest* request) {
NSString* html = [NSString stringWithFormat:@"<html><body>%@</body></html>", formHTML];
return [GCDWebServerDataResponse responseWithHTML:html];
}];
[webServer addHandlerForMethod:@"POST"
path:@"/"
requestClass:[GCDWebServerMultiPartFormRequest class]
processBlock:^GCDWebServerResponse *(GCDWebServerRequest* request) {
NSMutableString* string = [NSMutableString string];
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:@"%@ = &quot;%@&quot; (%@ | %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\"", [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\"", [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 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");
webServer = [[GCDWebServer alloc] init];
[webServer addHandlerForMethod:@"GET"
path:@"/"
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:[@"Hello World!" dataUsingEncoding:NSUTF8StringEncoding] contentType:@"text/plain"];
completionBlock(response);
});
}];
break;
}
}
success = [webServer runWithPort:8080];
#if !__has_feature(objc_arc)
[webServer release];
if (webServer) {
Delegate* delegate = [[Delegate alloc] init];
if (testDirectory) {
#if DEBUG
webServer.delegate = delegate;
#endif
fprintf(stdout, "<RUNNING TESTS FROM \"%s\">\n\n", [testDirectory UTF8String]);
result = (int)[webServer runTestsWithOptions:@{GCDWebServerOption_Port: @8080} inDirectory:testDirectory];
} else {
webServer.delegate = delegate;
if (recording) {
fprintf(stdout, "<RECORDING ENABLED>\n");
webServer.recordingEnabled = YES;
}
fprintf(stdout, "\n");
NSMutableDictionary* options = [NSMutableDictionary dictionary];
[options setObject:@8080 forKey:GCDWebServerOption_Port];
[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;
}
}
webServer.delegate = nil;
}
}
return success ? 0 : -1;
return result;
}

301
README.md
View File

@@ -1,18 +1,35 @@
Overview
========
GCDWebServer is a lightweight GCD based HTTP 1.1 server designed to be embedded in Mac & iOS apps. It was written from scratch with the following goals in mind:
* Easy to use and understand: only 4 main classes and less than 10 source code files
* Well designed API 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 maximum performance and concurrency
* Support for streaming large HTTP bodies for requests and responses to minimize memory usage
* Built-in parser for web forms submitted using "application/x-www-form-urlencoded" or "multipart/form-data" encodings (including file uploads)
[![Build Status](https://travis-ci.org/swisspol/GCDWebServer.svg?branch=master)](https://travis-ci.org/swisspol/GCDWebServer)
[![Version](http://cocoapod-badges.herokuapp.com/v/GCDWebServer/badge.png)](http://cocoadocs.org/docsets/GCDWebServer)
[![Platform](http://cocoapod-badges.herokuapp.com/p/GCDWebServer/badge.png)](https://github.com/swisspol/GCDWebServer)
[![License](http://img.shields.io/cocoapods/l/GCDWebServer.svg)](LICENSE)
*ANNOUNCEMENT: If you like GCDWebServer, check out [XLFacility](https://github.com/swisspol/XLFacility), an elegant and powerful logging facility for OS X & iOS by the same author and also open-source. XLFacility can be used seemlessly to handle logging from GCDWebServer (see "Logging in GCDWebServer" below).*
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:
* 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 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 best performance and concurrency
* No dependencies on third-party source code
* Available under a friendly [New BSD License](LICENSE)
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"
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
* 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
* [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 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
Included extensions:
* [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)
What's not supported (but not really required from an embedded HTTP server):
* Keep-alive connections
@@ -21,14 +38,35 @@ What's not supported (but not really required from an embedded HTTP server):
Requirements:
* OS X 10.7 or later (x86_64)
* iOS 5.0 or later (armv7, armv7s or arm64)
* ARC memory management only (if you need MRC support use GCDWebServer 3.1 and earlier)
Getting Started
===============
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.
Alternatively, you can install GCDWebServer using [CocoaPods](http://cocoapods.org/) by simply adding this line to your Xcode project's Podfile:
```
pod "GCDWebServer", "~> 3.0"
```
If you want to use GCDWebUploader, use this line instead:
```
pod "GCDWebServer/WebUploader", "~> 3.0"
```
Or this line for GCDWebDAVServer:
```
pod "GCDWebServer/WebDAV", "~> 3.0"
```
Hello World
===========
This code snippet shows how to implement a custom HTTP server that runs on port 8080 and returns a "Hello World" HTML page to any request &mdash; since GCDWebServer uses GCD blocks to handle requests, no subclassing or delegates are needed:
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):**
```objectivec
#import "GCDWebServer.h"
#import "GCDWebServerDataResponse.h"
int main(int argc, const char* argv[]) {
@autoreleasepool {
@@ -36,7 +74,7 @@ int main(int argc, const char* argv[]) {
// Create server
GCDWebServer* webServer = [[GCDWebServer alloc] init];
// Add a handler to respond to requests on any URL
// Add a handler to respond to GET requests on any URL
[webServer addDefaultHandlerForMethod:@"GET"
requestClass:[GCDWebServerRequest class]
processBlock:^GCDWebServerResponse *(GCDWebServerRequest* request) {
@@ -45,39 +83,196 @@ int main(int argc, const char* argv[]) {
}];
// Use convenience method that runs server on port 8080 until SIGINT received
[webServer runWithPort:8080];
// Destroy server
[webServer release];
// Use convenience method that runs server on port 8080
// until SIGINT (Ctrl-C in Terminal) or SIGTERM is received
[webServer runWithPort:8080 bonjourName:nil];
NSLog(@"Visit %@ in your web browser", webServer.serverURL);
}
return 0;
}
```
Adding File Upload to iOS Apps
==============================
**iOS version:**
```objectivec
#import "GCDWebServer.h"
#import "GCDWebServerDataResponse.h"
GCDWebUploader is a subclass of GCDWebServer that provides a ready-to-use HTML 5 file uploader & downloader. This lets users upload, download and delete files from a directory inside your iOS app's sandbox using a clean user interface in their web browser.
@interface AppDelegate : NSObject <UIApplicationDelegate> {
GCDWebServer* _webServer;
}
@end
Simply instantiate and run a GCDWebUploader instance then visit http://{YOUR-IOS-DEVICE-IP-ADDRESS}/ from your web browser:
@implementation AppDelegate
- (BOOL)application:(UIApplication*)application didFinishLaunchingWithOptions:(NSDictionary*)launchOptions {
// Create server
_webServer = [[GCDWebServer alloc] init];
// Add a handler to respond to GET requests on any URL
[_webServer addDefaultHandlerForMethod:@"GET"
requestClass:[GCDWebServerRequest class]
processBlock:^GCDWebServerResponse *(GCDWebServerRequest* request) {
return [GCDWebServerDataResponse responseWithHTML:@"<html><body><p>Hello World</p></body></html>"];
}];
// Start server on port 8080
[_webServer startWithPort:8080 bonjourName:nil];
NSLog(@"Visit %@ in your web browser", _webServer.serverURL);
return YES;
}
@end
```
**OS X Swift version (command line tool):**
***webServer.swift***
```swift
import Foundation
let webServer = GCDWebServer()
webServer.addDefaultHandlerForMethod("GET", requestClass: GCDWebServerRequest.self) { request in
return GCDWebServerDataResponse(HTML:"<html><body><p>Hello World</p></body></html>")
}
webServer.runWithPort(8080, bonjourName: nil)
println("Visit \(webServer.serverURL) in your web browser")
```
***WebServer-Bridging-Header.h***
```objectivec
#import "GCDWebServer.h"
#import "GCDWebServerDataResponse.h"
```
Asynchronous HTTP Responses
===========================
New in GCDWebServer 3.0 is the ability to process HTTP requests aysnchronously 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]
asyncProcessBlock:^(GCDWebServerRequest* request, GCDWebServerCompletionBlock completionBlock) {
GCDWebServerStreamedResponse* response = [GCDWebServerStreamedResponse responseWithContentType:@"text/html" asyncStreamBlock:^(GCDWebServerBodyReaderCompletionBlock completionBlock) {
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1 * NSEC_PER_SEC)), dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
completionBlock([@"<html><body><p>Hello" dataUsingEncoding:NSUTF8StringEncoding], nil); // Generate the 1st part of the stream data
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1 * NSEC_PER_SEC)), dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
completionBlock([@"World</p></body></html>" dataUsingEncoding:NSUTF8StringEncoding], nil); // Generate the 2nd part of the stream data
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!*
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.
Simply instantiate and run a ```GCDWebUploader``` instance then visit ```http://{YOUR-IOS-DEVICE-IP-ADDRESS}/``` from your web browser:
```objectivec
#import "GCDWebUploader.h"
@interface AppDelegate : NSObject <UIApplicationDelegate> {
GCDWebUploader* _webUploader;
}
@end
@implementation AppDelegate
- (BOOL)application:(UIApplication*)application didFinishLaunchingWithOptions:(NSDictionary*)launchOptions {
NSString* documentsPath = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) firstObject];
GCDWebUploader* webUploader = [[GCDWebUploader alloc] initWithUploadDirectory:documentsPath];
[webUploader start];
_webUploader = [[GCDWebUploader alloc] initWithUploadDirectory:documentsPath];
[_webUploader start];
NSLog(@"Visit %@ in your web browser", _webUploader.serverURL);
return YES;
}
@end
```
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 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).
Simply instantiate and run a ```GCDWebDAVServer``` instance then connect to ```http://{YOUR-IOS-DEVICE-IP-ADDRESS}/``` using a WebDAV client:
```objectivec
#import "GCDWebDAVServer.h"
@interface AppDelegate : NSObject <UIApplicationDelegate> {
GCDWebDAVServer* _davServer;
}
@end
@implementation AppDelegate
- (BOOL)application:(UIApplication*)application didFinishLaunchingWithOptions:(NSDictionary*)launchOptions {
NSString* documentsPath = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) firstObject];
_davServer = [[GCDWebDAVServer alloc] initWithUploadDirectory:documentsPath];
[_davServer start];
NSLog(@"Visit %@ in your WebDAV client", _davServer.serverURL);
return YES;
}
@end
```
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" 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):
```objectivec
#import "GCDWebServer.h"
@@ -88,7 +283,6 @@ int main(int argc, const char* argv[]) {
GCDWebServer* webServer = [[GCDWebServer alloc] init];
[webServer addGETHandlerForBasePath:@"/" directoryPath:NSHomeDirectory() indexFilename:nil cacheAge:3600 allowRangeRequests:YES];
[webServer runWithPort:8080];
[webServer release];
}
return 0;
@@ -98,20 +292,20 @@ int main(int argc, const char* argv[]) {
Using GCDWebServer
==================
You start by creating an instance of the 'GCDWebServer' class. Note that you can have multiple web servers running in the same app as long as they listen on different ports.
You start by creating an instance of the ```GCDWebServer``` class. Note that you can have multiple web servers running in the same app as long as they listen on different ports.
Then you add one or more "handlers" to the server: each handler gets a chance to handle an incoming web request and provide a response. Handlers are called in a LIFO queue, so the latest added handler overrides any previously added ones.
Finally you start the server on a given port.
Understanding GCDWebServer Architecture
=======================================
Understanding GCDWebServer's Architecture
=========================================
GCDWebServer is made of only 4 core classes:
* 'GCDWebServer' manages the socket that listens for new HTTP connections and the list of handlers used by the server.
* 'GCDWebServerConnection' 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 exposed so you can subclass it to override some hooks.
* 'GCDWebServerRequest' is created by the 'GCDWebServerConnection' instance after HTTP headers have been received. It wraps the request and handles the HTTP body if any. GCDWebServer comes with several subclasses of 'GCDWebServerRequest' to handle common cases like storing the body in memory or stream it to a file on disk. See [GCDWebServerRequest.h](CGDWebServer/GCDWebServerRequest.h) for the full list.
* 'GCDWebServerResponse' is created by the request handler and wraps the response HTTP headers and optional body. GCDWebServer provides several subclasses of 'GCDWebServerResponse' to handle common cases like HTML text in memory or streaming a file from disk. See [GCDWebServerResponse.h](CGDWebServer/GCDWebServerResponse.h) for the full list.
GCDWebServer's architecture consists of only 4 core classes:
* [GCDWebServer](GCDWebServer/Core/GCDWebServer.h) manages the socket that listens for new HTTP connections and the list of handlers used by the server.
* [GCDWebServerConnection](GCDWebServer/Core/GCDWebServerConnection.h) 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 exposed so you can subclass it to override some hooks.
* [GCDWebServerRequest](GCDWebServer/Core/GCDWebServerRequest.h) is created by the ```GCDWebServerConnection``` instance after HTTP headers have been received. It wraps the request and handles the HTTP body if any. GCDWebServer comes with [several subclasses](GCDWebServer/Requests) of ```GCDWebServerRequest``` to handle common cases like storing the body in memory or stream it to a file on disk.
* [GCDWebServerResponse](GCDWebServer/Core/GCDWebServerResponse.h) is created by the request handler and wraps the response HTTP headers and optional body. GCDWebServer comes with [several subclasses](GCDWebServer/Responses) of ```GCDWebServerResponse``` to handle common cases like HTML text in memory or streaming a file from disk.
Implementing Handlers
=====================
@@ -119,15 +313,40 @@ 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__.
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 'GCDWebServerRequest' instance (see above). 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.
* 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``` 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.
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 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:
- 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.
- 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).
- 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 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.
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) and [CocoaLumberjack](https://github.com/CocoaLumberjack/CocoaLumberjack). If either of them 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
===============================================
Here's an example handler that redirects "/" to "/index.html" using the convenience method on 'GCDWebServerResponse' (it sets the HTTP status code and 'Location' header automatically):
Here's an example handler that redirects "/" to "/index.html" using the convenience method on ```GCDWebServerResponse``` (it sets the HTTP status code and "Location" header automatically):
```objectivec
[self addHandlerForMethod:@"GET"
@@ -145,8 +364,8 @@ Advanced Example 2: Implementing Forms
======================================
To implement an HTTP form, you need a pair of handlers:
* The GET handler does not expect any body in the HTTP request and therefore uses the 'GCDWebServerRequest' class. The handler generates a response containing a simple HTML form.
* The POST handler expects the form values to be in the body of the HTTP request and percent-encoded. Fortunately, GCDWebServer provides the request class 'GCDWebServerURLEncodedFormRequest' which can automatically parse such bodies. The handler simply echoes back the value from the user submitted form.
* The GET handler does not expect any body in the HTTP request and therefore uses the ```GCDWebServerRequest``` class. The handler generates a response containing a simple HTML form.
* The POST handler expects the form values to be in the body of the HTTP request and percent-encoded. Fortunately, GCDWebServer provides the request class ```GCDWebServerURLEncodedFormRequest``` which can automatically parse such bodies. The handler simply echoes back the value from the user submitted form.
```objectivec
[webServer addHandlerForMethod:@"GET"
@@ -181,7 +400,7 @@ To implement an HTTP form, you need a pair of handlers:
Advanced Example 3: Serving a Dynamic Website
=============================================
GCDWebServer provides an extension to the 'GCDWebServerDataResponse' class that can return HTML content generated from a template and a set of variables (using the format '%variable%'). It is a very basic template system and is really intended as a starting point to building more advanced template systems by subclassing 'GCDWebServerResponse'.
GCDWebServer provides an extension to the ```GCDWebServerDataResponse``` class that can return HTML content generated from a template and a set of variables (using the format ```%variable%```). It is a very basic template system and is really intended as a starting point to building more advanced template systems by subclassing ```GCDWebServerResponse```.
Assuming you have a website directory in your app containing HTML template files along with the corresponding CSS, scripts and images, it's pretty easy to turn it into a dynamic website:
@@ -220,6 +439,6 @@ NSString* websitePath = [[NSBundle mainBundle] pathForResource:@"Website" ofType
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 uses it to provide a web server for people to upload and download comic files directly over WiFi to and from 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 GCDWebServer 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.

60
Run-Tests.sh Executable file
View File

@@ -0,0 +1,60 @@
#!/bin/bash -ex
OSX_SDK="macosx"
if [ -z "$TRAVIS" ]; then
IOS_SDK="iphoneos"
else
IOS_SDK="iphonesimulator"
fi
OSX_TARGET="GCDWebServer (Mac)"
IOS_TARGET="GCDWebServer (iOS)"
CONFIGURATION="Release"
BUILD_DIR="/tmp/GCDWebServer-Build"
PRODUCT="$BUILD_DIR/$CONFIGURATION/GCDWebServer"
PAYLOAD_ZIP="Tests/Payload.zip"
PAYLOAD_DIR="/tmp/GCDWebServer-Payload"
function runTests {
rm -rf "$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
if [ "$4" != "" ]; then
cp -f "$4" "$PAYLOAD_DIR/Payload"
pushd "$PAYLOAD_DIR/Payload"
SetFile -d "1/1/2014 00:00:00" -m "1/1/2014 00:00:00" `basename "$4"`
popd
fi
logLevel=2 $1 -mode "$2" -root "$PAYLOAD_DIR/Payload" -tests "$3"
}
# Build for iOS for oldest deployment target (TODO: run tests on iOS)
rm -rf "$BUILD_DIR"
xcodebuild -sdk "$IOS_SDK" -target "$IOS_TARGET" -configuration "$CONFIGURATION" build "SYMROOT=$BUILD_DIR" "IPHONEOS_DEPLOYMENT_TARGET=5.1.1" > /dev/null
# Build for iOS for default deployment target (TODO: run tests on iOS)
rm -rf "$BUILD_DIR"
xcodebuild -sdk "$IOS_SDK" -target "$IOS_TARGET" -configuration "$CONFIGURATION" build "SYMROOT=$BUILD_DIR" > /dev/null
# Build for OS X for oldest deployment target
rm -rf "$BUILD_DIR"
xcodebuild -sdk "$OSX_SDK" -target "$OSX_TARGET" -configuration "$CONFIGURATION" build "SYMROOT=$BUILD_DIR" "MACOSX_DEPLOYMENT_TARGET=10.7" > /dev/null
# Build for OS X for default deployment target
rm -rf "$BUILD_DIR"
xcodebuild -sdk "$OSX_SDK" -target "$OSX_TARGET" -configuration "$CONFIGURATION" build "SYMROOT=$BUILD_DIR" > /dev/null
# Run tests
runTests $PRODUCT "htmlForm" "Tests/HTMLForm"
runTests $PRODUCT "htmlFileUpload" "Tests/HTMLFileUpload"
runTests $PRODUCT "webServer" "Tests/WebServer"
runTests $PRODUCT "webDAV" "Tests/WebDAV-Transmit"
runTests $PRODUCT "webDAV" "Tests/WebDAV-Cyberduck"
runTests $PRODUCT "webDAV" "Tests/WebDAV-Finder"
runTests $PRODUCT "webUploader" "Tests/WebUploader"
runTests $PRODUCT "webServer" "Tests/WebServer-Sample-Movie" "Tests/Sample-Movie.mp4"
# Done
echo "\nAll tests completed successfully!"

View 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>

View 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

View 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 = &quot;hero_mba_11.jpg&quot; (image/jpeg | 106 KB)<br>files = &quot;Test File.txt&quot; (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>

Binary file not shown.

View 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>

View 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

View 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>

View 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/Payload.zip Normal file

Binary file not shown.

BIN
Tests/Sample-Movie.mp4 Normal file

Binary file not shown.

View File

@@ -0,0 +1,6 @@
HTTP/1.1 200 OK
Cache-Control: no-cache
Connection: Close
Server: GCDWebDAVServer
Date: Sat, 12 Apr 2014 04:52:42 GMT

View File

@@ -0,0 +1,6 @@
HEAD / HTTP/1.1
Host: localhost:8080
Connection: Keep-Alive
User-Agent: Cyberduck/4.4.3 (Mac OS X/10.9.2) (x86_64)
Accept-Encoding: gzip,deflate

View File

@@ -0,0 +1,14 @@
HTTP/1.1 207 Multi-Status
Cache-Control: no-cache
Content-Length: 1106
Content-Type: application/xml; charset="utf-8"
Connection: Close
Server: GCDWebDAVServer
Date: Sat, 12 Apr 2014 04:52:42 GMT
<?xml version="1.0" encoding="utf-8" ?><D:multistatus xmlns:D="DAV:">
<D:response><D:href>/</D:href><D:propstat><D:prop><D:resourcetype><D:collection/></D:resourcetype><D:creationdate>2014-01-01T00:00:00+00:00</D:creationdate></D:prop><D:status>HTTP/1.1 200 OK</D:status></D:propstat></D:response>
<D:response><D:href>/Copy.txt</D:href><D:propstat><D:prop><D:resourcetype/><D:creationdate>2014-04-10T11:10:14+00:00</D:creationdate><D:getlastmodified>Thu, 10 Apr 2014 11:10:14 GMT</D:getlastmodified><D:getcontentlength>271</D:getcontentlength></D:prop><D:status>HTTP/1.1 200 OK</D:status></D:propstat></D:response>
<D:response><D:href>/images</D:href><D:propstat><D:prop><D:resourcetype><D:collection/></D:resourcetype><D:creationdate>2014-01-01T00:00:00+00:00</D:creationdate></D:prop><D:status>HTTP/1.1 200 OK</D:status></D:propstat></D:response>
<D:response><D:href>/PDF%20Reports</D:href><D:propstat><D:prop><D:resourcetype><D:collection/></D:resourcetype><D:creationdate>2014-01-01T00:00:00+00:00</D:creationdate></D:prop><D:status>HTTP/1.1 200 OK</D:status></D:propstat></D:response>
</D:multistatus>

View File

@@ -0,0 +1,10 @@
PROPFIND / HTTP/1.1
Depth: 1
Content-Type: text/xml; charset=utf-8
Content-Length: 99
Host: localhost:8080
Connection: Keep-Alive
User-Agent: Cyberduck/4.4.3 (Mac OS X/10.9.2) (x86_64)
Accept-Encoding: gzip,deflate
<?xml version="1.0" encoding="UTF-8" standalone="yes"?><propfind xmlns="DAV:"><allprop/></propfind>

View File

@@ -0,0 +1,12 @@
HTTP/1.1 207 Multi-Status
Cache-Control: no-cache
Content-Length: 700
Content-Type: application/xml; charset="utf-8"
Connection: Close
Server: GCDWebDAVServer
Date: Sat, 12 Apr 2014 04:52:47 GMT
<?xml version="1.0" encoding="utf-8" ?><D:multistatus xmlns:D="DAV:">
<D:response><D:href>/PDF%20Reports/</D:href><D:propstat><D:prop><D:resourcetype><D:collection/></D:resourcetype><D:creationdate>2014-01-01T00:00:00+00:00</D:creationdate></D:prop><D:status>HTTP/1.1 200 OK</D:status></D:propstat></D:response>
<D:response><D:href>/PDF%20Reports/Apple%20Economic%20Impact%20on%20Cupertino.pdf</D:href><D:propstat><D:prop><D:resourcetype/><D:creationdate>2013-05-01T12:01:13+00:00</D:creationdate><D:getlastmodified>Wed, 01 May 2013 12:01:13 GMT</D:getlastmodified><D:getcontentlength>181952</D:getcontentlength></D:prop><D:status>HTTP/1.1 200 OK</D:status></D:propstat></D:response>
</D:multistatus>

View File

@@ -0,0 +1,10 @@
PROPFIND /PDF%20Reports/ HTTP/1.1
Depth: 1
Content-Type: text/xml; charset=utf-8
Content-Length: 99
Host: localhost:8080
Connection: Keep-Alive
User-Agent: Cyberduck/4.4.3 (Mac OS X/10.9.2) (x86_64)
Accept-Encoding: gzip,deflate
<?xml version="1.0" encoding="UTF-8" standalone="yes"?><propfind xmlns="DAV:"><allprop/></propfind>

View File

@@ -0,0 +1,13 @@
HTTP/1.1 207 Multi-Status
Cache-Control: no-cache
Content-Length: 998
Content-Type: application/xml; charset="utf-8"
Connection: Close
Server: GCDWebDAVServer
Date: Sat, 12 Apr 2014 04:52:47 GMT
<?xml version="1.0" encoding="utf-8" ?><D:multistatus xmlns:D="DAV:">
<D:response><D:href>/images/</D:href><D:propstat><D:prop><D:resourcetype><D:collection/></D:resourcetype><D:creationdate>2014-01-01T00:00:00+00:00</D:creationdate></D:prop><D:status>HTTP/1.1 200 OK</D:status></D:propstat></D:response>
<D:response><D:href>/images/capable_green_ipad_l.png</D:href><D:propstat><D:prop><D:resourcetype/><D:creationdate>2014-04-10T21:46:56+00:00</D:creationdate><D:getlastmodified>Thu, 10 Apr 2014 21:46:56 GMT</D:getlastmodified><D:getcontentlength>116066</D:getcontentlength></D:prop><D:status>HTTP/1.1 200 OK</D:status></D:propstat></D:response>
<D:response><D:href>/images/hero_mba_11.jpg</D:href><D:propstat><D:prop><D:resourcetype/><D:creationdate>2014-04-10T21:51:14+00:00</D:creationdate><D:getlastmodified>Thu, 10 Apr 2014 21:51:14 GMT</D:getlastmodified><D:getcontentlength>106799</D:getcontentlength></D:prop><D:status>HTTP/1.1 200 OK</D:status></D:propstat></D:response>
</D:multistatus>

View File

@@ -0,0 +1,10 @@
PROPFIND /images/ HTTP/1.1
Depth: 1
Content-Type: text/xml; charset=utf-8
Content-Length: 99
Host: localhost:8080
Connection: Keep-Alive
User-Agent: Cyberduck/4.4.3 (Mac OS X/10.9.2) (x86_64)
Accept-Encoding: gzip,deflate
<?xml version="1.0" encoding="UTF-8" standalone="yes"?><propfind xmlns="DAV:"><allprop/></propfind>

View File

@@ -0,0 +1,6 @@
HTTP/1.1 200 OK
Cache-Control: no-cache
Connection: Close
Server: GCDWebDAVServer
Date: Sat, 12 Apr 2014 04:52:51 GMT

View File

@@ -0,0 +1,6 @@
HEAD / HTTP/1.1
Host: localhost:8080
Connection: Keep-Alive
User-Agent: Cyberduck/4.4.3 (Mac OS X/10.9.2) (x86_64)
Accept-Encoding: gzip,deflate

View File

@@ -0,0 +1,7 @@
HTTP/1.1 404 Not Found
Content-Length: 204
Content-Type: text/html; charset=utf-8
Connection: Close
Server: GCDWebDAVServer
Date: Sat, 12 Apr 2014 04:52:51 GMT

View File

@@ -0,0 +1,6 @@
HEAD /Copy%20%284%3A11%3A14%2C%209%3A52%20PM%29.txt HTTP/1.1
Host: localhost:8080
Connection: Keep-Alive
User-Agent: Cyberduck/4.4.3 (Mac OS X/10.9.2) (x86_64)
Accept-Encoding: gzip,deflate

View File

@@ -0,0 +1,6 @@
HTTP/1.1 201 Created
Cache-Control: no-cache
Connection: Close
Server: GCDWebDAVServer
Date: Sat, 12 Apr 2014 04:52:51 GMT

View File

@@ -0,0 +1,8 @@
COPY /Copy.txt HTTP/1.1
Destination: http://localhost:8080/Copy%20%284%3A11%3A14%2C%209%3A52%20PM%29.txt
Overwrite: T
Host: localhost:8080
Connection: Keep-Alive
User-Agent: Cyberduck/4.4.3 (Mac OS X/10.9.2) (x86_64)
Accept-Encoding: gzip,deflate

View File

@@ -0,0 +1,15 @@
HTTP/1.1 207 Multi-Status
Cache-Control: no-cache
Content-Length: 1448
Content-Type: application/xml; charset="utf-8"
Connection: Close
Server: GCDWebDAVServer
Date: Sat, 12 Apr 2014 04:52:51 GMT
<?xml version="1.0" encoding="utf-8" ?><D:multistatus xmlns:D="DAV:">
<D:response><D:href>/</D:href><D:propstat><D:prop><D:resourcetype><D:collection/></D:resourcetype><D:creationdate>2014-01-01T00:00:00+00:00</D:creationdate></D:prop><D:status>HTTP/1.1 200 OK</D:status></D:propstat></D:response>
<D:response><D:href>/Copy%20(4:11:14,%209:52%20PM).txt</D:href><D:propstat><D:prop><D:resourcetype/><D:creationdate>2014-04-10T11:10:14+00:00</D:creationdate><D:getlastmodified>Thu, 10 Apr 2014 11:10:14 GMT</D:getlastmodified><D:getcontentlength>271</D:getcontentlength></D:prop><D:status>HTTP/1.1 200 OK</D:status></D:propstat></D:response>
<D:response><D:href>/Copy.txt</D:href><D:propstat><D:prop><D:resourcetype/><D:creationdate>2014-04-10T11:10:14+00:00</D:creationdate><D:getlastmodified>Thu, 10 Apr 2014 11:10:14 GMT</D:getlastmodified><D:getcontentlength>271</D:getcontentlength></D:prop><D:status>HTTP/1.1 200 OK</D:status></D:propstat></D:response>
<D:response><D:href>/images</D:href><D:propstat><D:prop><D:resourcetype><D:collection/></D:resourcetype><D:creationdate>2014-01-01T00:00:00+00:00</D:creationdate></D:prop><D:status>HTTP/1.1 200 OK</D:status></D:propstat></D:response>
<D:response><D:href>/PDF%20Reports</D:href><D:propstat><D:prop><D:resourcetype><D:collection/></D:resourcetype><D:creationdate>2014-01-01T00:00:00+00:00</D:creationdate></D:prop><D:status>HTTP/1.1 200 OK</D:status></D:propstat></D:response>
</D:multistatus>

View File

@@ -0,0 +1,10 @@
PROPFIND / HTTP/1.1
Depth: 1
Content-Type: text/xml; charset=utf-8
Content-Length: 99
Host: localhost:8080
Connection: Keep-Alive
User-Agent: Cyberduck/4.4.3 (Mac OS X/10.9.2) (x86_64)
Accept-Encoding: gzip,deflate
<?xml version="1.0" encoding="UTF-8" standalone="yes"?><propfind xmlns="DAV:"><allprop/></propfind>

View File

@@ -0,0 +1,6 @@
HTTP/1.1 200 OK
Cache-Control: no-cache
Connection: Close
Server: GCDWebDAVServer
Date: Sat, 12 Apr 2014 04:52:59 GMT

View File

@@ -0,0 +1,6 @@
HEAD / HTTP/1.1
Host: localhost:8080
Connection: Keep-Alive
User-Agent: Cyberduck/4.4.3 (Mac OS X/10.9.2) (x86_64)
Accept-Encoding: gzip,deflate

Binary file not shown.

View File

@@ -0,0 +1,6 @@
GET /images/capable_green_ipad_l.png HTTP/1.1
Host: localhost:8080
Connection: Keep-Alive
User-Agent: Cyberduck/4.4.3 (Mac OS X/10.9.2) (x86_64)
Accept-Encoding: gzip,deflate

View File

@@ -0,0 +1,13 @@
HTTP/1.1 207 Multi-Status
Cache-Control: no-cache
Content-Length: 998
Content-Type: application/xml; charset="utf-8"
Connection: Close
Server: GCDWebDAVServer
Date: Sat, 12 Apr 2014 04:53:07 GMT
<?xml version="1.0" encoding="utf-8" ?><D:multistatus xmlns:D="DAV:">
<D:response><D:href>/images/</D:href><D:propstat><D:prop><D:resourcetype><D:collection/></D:resourcetype><D:creationdate>2014-01-01T00:00:00+00:00</D:creationdate></D:prop><D:status>HTTP/1.1 200 OK</D:status></D:propstat></D:response>
<D:response><D:href>/images/capable_green_ipad_l.png</D:href><D:propstat><D:prop><D:resourcetype/><D:creationdate>2014-04-10T21:46:56+00:00</D:creationdate><D:getlastmodified>Thu, 10 Apr 2014 21:46:56 GMT</D:getlastmodified><D:getcontentlength>116066</D:getcontentlength></D:prop><D:status>HTTP/1.1 200 OK</D:status></D:propstat></D:response>
<D:response><D:href>/images/hero_mba_11.jpg</D:href><D:propstat><D:prop><D:resourcetype/><D:creationdate>2014-04-10T21:51:14+00:00</D:creationdate><D:getlastmodified>Thu, 10 Apr 2014 21:51:14 GMT</D:getlastmodified><D:getcontentlength>106799</D:getcontentlength></D:prop><D:status>HTTP/1.1 200 OK</D:status></D:propstat></D:response>
</D:multistatus>

View File

@@ -0,0 +1,10 @@
PROPFIND /images/ HTTP/1.1
Depth: 1
Content-Type: text/xml; charset=utf-8
Content-Length: 99
Host: localhost:8080
Connection: Keep-Alive
User-Agent: Cyberduck/4.4.3 (Mac OS X/10.9.2) (x86_64)
Accept-Encoding: gzip,deflate
<?xml version="1.0" encoding="UTF-8" standalone="yes"?><propfind xmlns="DAV:"><allprop/></propfind>

View File

@@ -0,0 +1,6 @@
HTTP/1.1 204 No Content
Cache-Control: no-cache
Connection: Close
Server: GCDWebDAVServer
Date: Sat, 12 Apr 2014 04:53:07 GMT

View File

@@ -0,0 +1,6 @@
DELETE /images/capable_green_ipad_l.png HTTP/1.1
Host: localhost:8080
Connection: Keep-Alive
User-Agent: Cyberduck/4.4.3 (Mac OS X/10.9.2) (x86_64)
Accept-Encoding: gzip,deflate

View File

@@ -0,0 +1,6 @@
HTTP/1.1 204 No Content
Cache-Control: no-cache
Connection: Close
Server: GCDWebDAVServer
Date: Sat, 12 Apr 2014 04:53:07 GMT

View File

@@ -0,0 +1,6 @@
DELETE /images/hero_mba_11.jpg HTTP/1.1
Host: localhost:8080
Connection: Keep-Alive
User-Agent: Cyberduck/4.4.3 (Mac OS X/10.9.2) (x86_64)
Accept-Encoding: gzip,deflate

View File

@@ -0,0 +1,6 @@
HTTP/1.1 204 No Content
Cache-Control: no-cache
Connection: Close
Server: GCDWebDAVServer
Date: Sat, 12 Apr 2014 04:53:07 GMT

View File

@@ -0,0 +1,6 @@
DELETE /images/ HTTP/1.1
Host: localhost:8080
Connection: Keep-Alive
User-Agent: Cyberduck/4.4.3 (Mac OS X/10.9.2) (x86_64)
Accept-Encoding: gzip,deflate

View File

@@ -0,0 +1,14 @@
HTTP/1.1 207 Multi-Status
Cache-Control: no-cache
Content-Length: 1214
Content-Type: application/xml; charset="utf-8"
Connection: Close
Server: GCDWebDAVServer
Date: Sat, 12 Apr 2014 04:53:07 GMT
<?xml version="1.0" encoding="utf-8" ?><D:multistatus xmlns:D="DAV:">
<D:response><D:href>/</D:href><D:propstat><D:prop><D:resourcetype><D:collection/></D:resourcetype><D:creationdate>2014-01-01T00:00:00+00:00</D:creationdate></D:prop><D:status>HTTP/1.1 200 OK</D:status></D:propstat></D:response>
<D:response><D:href>/Copy%20(4:11:14,%209:52%20PM).txt</D:href><D:propstat><D:prop><D:resourcetype/><D:creationdate>2014-04-10T11:10:14+00:00</D:creationdate><D:getlastmodified>Thu, 10 Apr 2014 11:10:14 GMT</D:getlastmodified><D:getcontentlength>271</D:getcontentlength></D:prop><D:status>HTTP/1.1 200 OK</D:status></D:propstat></D:response>
<D:response><D:href>/Copy.txt</D:href><D:propstat><D:prop><D:resourcetype/><D:creationdate>2014-04-10T11:10:14+00:00</D:creationdate><D:getlastmodified>Thu, 10 Apr 2014 11:10:14 GMT</D:getlastmodified><D:getcontentlength>271</D:getcontentlength></D:prop><D:status>HTTP/1.1 200 OK</D:status></D:propstat></D:response>
<D:response><D:href>/PDF%20Reports</D:href><D:propstat><D:prop><D:resourcetype><D:collection/></D:resourcetype><D:creationdate>2014-01-01T00:00:00+00:00</D:creationdate></D:prop><D:status>HTTP/1.1 200 OK</D:status></D:propstat></D:response>
</D:multistatus>

View File

@@ -0,0 +1,10 @@
PROPFIND / HTTP/1.1
Depth: 1
Content-Type: text/xml; charset=utf-8
Content-Length: 99
Host: localhost:8080
Connection: Keep-Alive
User-Agent: Cyberduck/4.4.3 (Mac OS X/10.9.2) (x86_64)
Accept-Encoding: gzip,deflate
<?xml version="1.0" encoding="UTF-8" standalone="yes"?><propfind xmlns="DAV:"><allprop/></propfind>

View File

@@ -0,0 +1,6 @@
HTTP/1.1 201 Created
Cache-Control: no-cache
Connection: Close
Server: GCDWebDAVServer
Date: Sat, 12 Apr 2014 04:53:13 GMT

View File

@@ -0,0 +1,8 @@
MOVE /Copy%20%284%3A11%3A14%2C%209%3A52%20PM%29.txt HTTP/1.1
Destination: http://localhost:8080/Test.txt
Overwrite: T
Host: localhost:8080
Connection: Keep-Alive
User-Agent: Cyberduck/4.4.3 (Mac OS X/10.9.2) (x86_64)
Accept-Encoding: gzip,deflate

View File

@@ -0,0 +1,14 @@
HTTP/1.1 207 Multi-Status
Cache-Control: no-cache
Content-Length: 1189
Content-Type: application/xml; charset="utf-8"
Connection: Close
Server: GCDWebDAVServer
Date: Sat, 12 Apr 2014 04:53:13 GMT
<?xml version="1.0" encoding="utf-8" ?><D:multistatus xmlns:D="DAV:">
<D:response><D:href>/</D:href><D:propstat><D:prop><D:resourcetype><D:collection/></D:resourcetype><D:creationdate>2014-01-01T00:00:00+00:00</D:creationdate></D:prop><D:status>HTTP/1.1 200 OK</D:status></D:propstat></D:response>
<D:response><D:href>/Copy.txt</D:href><D:propstat><D:prop><D:resourcetype/><D:creationdate>2014-04-10T11:10:14+00:00</D:creationdate><D:getlastmodified>Thu, 10 Apr 2014 11:10:14 GMT</D:getlastmodified><D:getcontentlength>271</D:getcontentlength></D:prop><D:status>HTTP/1.1 200 OK</D:status></D:propstat></D:response>
<D:response><D:href>/PDF%20Reports</D:href><D:propstat><D:prop><D:resourcetype><D:collection/></D:resourcetype><D:creationdate>2014-01-01T00:00:00+00:00</D:creationdate></D:prop><D:status>HTTP/1.1 200 OK</D:status></D:propstat></D:response>
<D:response><D:href>/Test.txt</D:href><D:propstat><D:prop><D:resourcetype/><D:creationdate>2014-04-10T11:10:14+00:00</D:creationdate><D:getlastmodified>Thu, 10 Apr 2014 11:10:14 GMT</D:getlastmodified><D:getcontentlength>271</D:getcontentlength></D:prop><D:status>HTTP/1.1 200 OK</D:status></D:propstat></D:response>
</D:multistatus>

View File

@@ -0,0 +1,10 @@
PROPFIND / HTTP/1.1
Depth: 1
Content-Type: text/xml; charset=utf-8
Content-Length: 99
Host: localhost:8080
Connection: Keep-Alive
User-Agent: Cyberduck/4.4.3 (Mac OS X/10.9.2) (x86_64)
Accept-Encoding: gzip,deflate
<?xml version="1.0" encoding="UTF-8" standalone="yes"?><propfind xmlns="DAV:"><allprop/></propfind>

View File

@@ -0,0 +1,6 @@
HTTP/1.1 201 Created
Cache-Control: no-cache
Connection: Close
Server: GCDWebDAVServer
Date: Sat, 12 Apr 2014 04:53:14 GMT

View File

@@ -0,0 +1,8 @@
MOVE /Test.txt HTTP/1.1
Destination: http://localhost:8080/PDF%20Reports/Test.txt
Overwrite: T
Host: localhost:8080
Connection: Keep-Alive
User-Agent: Cyberduck/4.4.3 (Mac OS X/10.9.2) (x86_64)
Accept-Encoding: gzip,deflate

View File

@@ -0,0 +1,13 @@
HTTP/1.1 207 Multi-Status
Cache-Control: no-cache
Content-Length: 872
Content-Type: application/xml; charset="utf-8"
Connection: Close
Server: GCDWebDAVServer
Date: Sat, 12 Apr 2014 04:53:14 GMT
<?xml version="1.0" encoding="utf-8" ?><D:multistatus xmlns:D="DAV:">
<D:response><D:href>/</D:href><D:propstat><D:prop><D:resourcetype><D:collection/></D:resourcetype><D:creationdate>2014-01-01T00:00:00+00:00</D:creationdate></D:prop><D:status>HTTP/1.1 200 OK</D:status></D:propstat></D:response>
<D:response><D:href>/Copy.txt</D:href><D:propstat><D:prop><D:resourcetype/><D:creationdate>2014-04-10T11:10:14+00:00</D:creationdate><D:getlastmodified>Thu, 10 Apr 2014 11:10:14 GMT</D:getlastmodified><D:getcontentlength>271</D:getcontentlength></D:prop><D:status>HTTP/1.1 200 OK</D:status></D:propstat></D:response>
<D:response><D:href>/PDF%20Reports</D:href><D:propstat><D:prop><D:resourcetype><D:collection/></D:resourcetype><D:creationdate>2014-01-01T00:00:00+00:00</D:creationdate></D:prop><D:status>HTTP/1.1 200 OK</D:status></D:propstat></D:response>
</D:multistatus>

View File

@@ -0,0 +1,10 @@
PROPFIND / HTTP/1.1
Depth: 1
Content-Type: text/xml; charset=utf-8
Content-Length: 99
Host: localhost:8080
Connection: Keep-Alive
User-Agent: Cyberduck/4.4.3 (Mac OS X/10.9.2) (x86_64)
Accept-Encoding: gzip,deflate
<?xml version="1.0" encoding="UTF-8" standalone="yes"?><propfind xmlns="DAV:"><allprop/></propfind>

View File

@@ -0,0 +1,13 @@
HTTP/1.1 207 Multi-Status
Cache-Control: no-cache
Content-Length: 1031
Content-Type: application/xml; charset="utf-8"
Connection: Close
Server: GCDWebDAVServer
Date: Sat, 12 Apr 2014 04:53:14 GMT
<?xml version="1.0" encoding="utf-8" ?><D:multistatus xmlns:D="DAV:">
<D:response><D:href>/PDF%20Reports/</D:href><D:propstat><D:prop><D:resourcetype><D:collection/></D:resourcetype><D:creationdate>2014-01-01T00:00:00+00:00</D:creationdate></D:prop><D:status>HTTP/1.1 200 OK</D:status></D:propstat></D:response>
<D:response><D:href>/PDF%20Reports/Apple%20Economic%20Impact%20on%20Cupertino.pdf</D:href><D:propstat><D:prop><D:resourcetype/><D:creationdate>2013-05-01T12:01:13+00:00</D:creationdate><D:getlastmodified>Wed, 01 May 2013 12:01:13 GMT</D:getlastmodified><D:getcontentlength>181952</D:getcontentlength></D:prop><D:status>HTTP/1.1 200 OK</D:status></D:propstat></D:response>
<D:response><D:href>/PDF%20Reports/Test.txt</D:href><D:propstat><D:prop><D:resourcetype/><D:creationdate>2014-04-10T11:10:14+00:00</D:creationdate><D:getlastmodified>Thu, 10 Apr 2014 11:10:14 GMT</D:getlastmodified><D:getcontentlength>271</D:getcontentlength></D:prop><D:status>HTTP/1.1 200 OK</D:status></D:propstat></D:response>
</D:multistatus>

View File

@@ -0,0 +1,10 @@
PROPFIND /PDF%20Reports/ HTTP/1.1
Depth: 1
Content-Type: text/xml; charset=utf-8
Content-Length: 99
Host: localhost:8080
Connection: Keep-Alive
User-Agent: Cyberduck/4.4.3 (Mac OS X/10.9.2) (x86_64)
Accept-Encoding: gzip,deflate
<?xml version="1.0" encoding="UTF-8" standalone="yes"?><propfind xmlns="DAV:"><allprop/></propfind>

Some files were not shown because too many files have changed in this diff Show More