iOS module, fixes

This commit is contained in:
alwx 2017-02-07 14:03:16 +03:00
parent 5161252b4f
commit 9bbbd0fe6a
40 changed files with 7300 additions and 359 deletions

21
LICENSE
View File

@ -1,21 +0,0 @@
The MIT License (MIT)
Copyright (c) 2016 James Strain
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

130
README.md
View File

@ -1,15 +1,13 @@
# react-native-http-server
# react-native-http-bridge
HTTP Server for [React Native](https://github.com/facebook/react-native)
Android only for now.
Built on top of the NanoHttpd library: https://github.com/NanoHttpd/nanohttpd
Supports only POST-requests and one-way communication. Created for [Status.im](https://github.com/status-im)
## Install
```shell
npm install --save react-native-http-server
npm install --save react-native-http-bridge
```
## Automatically link
@ -17,137 +15,31 @@ npm install --save react-native-http-server
#### With React Native 0.27+
```shell
react-native link react-native-http-server
react-native link react-native-http-bridge
```
#### With older versions of React Native
You need [`rnpm`](https://github.com/rnpm/rnpm) (`npm install -g rnpm`)
```shell
rnpm link react-native-http-server
```
## Manually link
- in `android/app/build.gradle`:
```diff
dependencies {
...
compile "com.facebook.react:react-native:+" // From node_modules
+ compile project(':react-native-http-server')
}
```
- in `android/settings.gradle`:
```diff
...
include ':app'
+ include ':react-native-http-server'
+ project(':react-native-http-server').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-http-server/android')
```
#### With React Native 0.29+
- in `MainApplication.java`:
```diff
+ import com.strainy.RNHttpServer.RNHttpServer;
public class MainApplication extends Application implements ReactApplication {
//......
@Override
protected List<ReactPackage> getPackages() {
return Arrays.<ReactPackage>asList(
+ new RNHttpServer(),
new MainReactPackage()
);
}
......
}
```
#### With older versions of React Native:
- in `MainActivity.java`:
```diff
+ import com.strainy.RNHttpServer.RNHttpServer;
public class MainActivity extends ReactActivity {
......
@Override
protected List<ReactPackage> getPackages() {
return Arrays.<ReactPackage>asList(
+ new RNHttpServer(),
new MainReactPackage()
);
}
}
```
## Release Notes
See [CHANGELOG.md](https://github.com/strainy/react-native-http-server/blob/master/CHANGELOG.md)
## Example
First import/require react-native-http-server:
```js
var httpServer = require('react-native-http-server');
var httpBridge = require('react-native-http-bridge');
```
Initalise the server in the `componentWillMount` lifecycle method. When initalising, you'll provide an options object (only the `port` property is accepted for now) and a callback where requests will be captured and responses returned.
Initalise the server in the `componentWillMount` lifecycle method. You need to provide a `port` and a callback where requests will be captured. Currently there is no way to return responses.
```js
componentWillMount(){
var options = {
port: 1234, // note that 80 is reserved on Android - an exception will be thrown
};
// initalize the server (now accessible via localhost:1234)
httpBridge.start(5561, function(request) {
// initalise the server (now accessible via localhost:1234)
httpServer.create(options, function(request, send){
// interpret the url
let url = request.url.split('/');
let ext = url[1];
let data = JSON.stringify({data: "hello world!", extension: ext});
//Build our response object (you can specify status, mime_type (type), data, and response headers)
let res = {};
res.status = "OK";
res.type = "application/json";
res.data = data;
res.headers = {
"Access-Control-Allow-Credentials": "true",
"Access-Control-Allow-Headers": "Authorization, Content-Type, Accept, Origin, User-Agent, Cache-Control, Keep-Alive, If-Modified-Since, If-None-Match",
"Access-Control-Allow-Methods": "GET, HEAD",
"Access-Control-Allow-Origin": "*",
"Access-Control-Expose-Headers": "Content-Type, Cache-Control, ETag, Expires, Last-Modified, Content-Length",
"Access-Control-Max-Age": "3000",
"Cache-Control": "max-age=300",
"Connection": "keep-alive",
"Content-Encoding": "gzip",
"Content-Length": data.length.toString(),
"Date": (new Date()).toUTCString(),
"Last-Modified": (new Date()).toUTCString(),
"Server": "Fastly",
"Vary": "Accept-Encoding"
};
send(res);
// request.url
// request.postData
});
@ -160,7 +52,7 @@ Finally, ensure that you disable the server when your component is being unmount
```js
componentWillUnmount() {
httpServer.stop();
httpBridge.stop();
}
```

View File

@ -4,30 +4,18 @@ import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.ReactContextBaseJavaModule;
import com.facebook.react.bridge.LifecycleEventListener;
import com.facebook.react.bridge.ReactMethod;
import com.facebook.react.bridge.Callback;
import com.facebook.react.bridge.ReadableMap;
import me.alwx.HttpServer.Server;
import java.util.Map;
import java.util.HashMap;
import java.io.IOException;
import android.support.annotation.Nullable;
import android.util.Log;
public class HttpServerModule extends ReactContextBaseJavaModule implements LifecycleEventListener {
ReactApplicationContext reactContext;
private static final String TAG = "HttpServer";
private static final String DEFAULT_PORT_KEY = "DEFAULT_PORT";
private static final String DEFAULT_TIMEOUT_KEY = "DEFAULT_TIMEOUT";
private static final String SERVER_EVENT_ID_KEY = "SERVER_EVENT";
private static final String MODULE_NAME = "HttpServer";
private static final int DEFAULT_PORT = 5561;
private static final int DEFAULT_TIMEOUT = 5000;
private static final String SERVER_EVENT_ID = "ReactNativeHttpServerResponse";
private int port;
private int timeout;
private Server server = null;
public HttpServerModule(ReactApplicationContext reactContext) {
@ -35,88 +23,46 @@ public class HttpServerModule extends ReactContextBaseJavaModule implements Life
this.reactContext = reactContext;
port = DEFAULT_PORT;
timeout = DEFAULT_TIMEOUT;
reactContext.addLifecycleEventListener(this);
}
@Override
public String getName() {
return "HttpServer";
}
@Override
public Map<String, Object> getConstants() {
final Map<String, Object> constants = new HashMap<>();
constants.put(DEFAULT_PORT_KEY, DEFAULT_PORT);
constants.put(DEFAULT_TIMEOUT_KEY, DEFAULT_TIMEOUT);
constants.put(SERVER_EVENT_ID_KEY, SERVER_EVENT_ID);
return constants;
return MODULE_NAME;
}
@ReactMethod
public void init(ReadableMap options, Callback success, @Nullable Callback failure) {
Log.d(TAG, "Initializing server...");
public void start(int port) {
Log.d(MODULE_NAME, "Initializing server...");
this.port = port;
if (options.hasKey("port")) {
port = options.getInt("port");
}
if (options.hasKey("timeout")) {
timeout = options.getInt("timeout");
}
start(success, failure);
start();
}
@ReactMethod
public void start(@Nullable Callback success, @Nullable Callback failure) {
private void start() {
if (port == 0) {
return;
}
server = new Server(reactContext, port);
try {
server = new Server(reactContext, port, timeout);
server.start();
if (success != null) {
success.invoke();
}
} catch (Exception e) {
Log.e(TAG, e.getMessage());
if (failure != null) {
failure.invoke(e.getMessage());
}
} catch (IOException e) {
Log.e(MODULE_NAME, e.getMessage());
}
}
@ReactMethod
public void stop() {
Log.d(TAG, "Server Stopped.");
Log.d(MODULE_NAME, "Server stopped");
server.stop();
server = null;
}
@ReactMethod
public void setResponse(String uri, ReadableMap response) {
if (server != null) {
server.setResponse(uri, response);
}
}
@ReactMethod
public String getHostName() {
if (server != null) {
Log.d(TAG, server.getHostname());
return server.getHostname();
} else {
return "not defined";
}
}
/* Shut down the server if app is destroyed or paused */
@Override
public void onHostResume() {
//we can restart the server here as the success callback is not needed since an event is registered
start(null, null);
start();
}
@Override

View File

@ -22,21 +22,14 @@ public class Server extends NanoHTTPD {
private Map<String, ReadableMap> response;
private ReactContext reactContext;
private int timeout;
public Server(ReactContext context, int port, int timeout) {
public Server(ReactContext context, int port) {
super(port);
reactContext = context;
this.timeout = timeout;
response = new HashMap<>();
Log.d(TAG, "Server started");
}
public void setResponse(String uri, ReadableMap response) {
this.response.put(uri, response);
}
@Override
public Response serve(IHTTPSession session) {
Log.d(TAG, "Request received!");
@ -54,15 +47,13 @@ public class Server extends NanoHTTPD {
}
this.sendEvent(reactContext, SERVER_EVENT_ID, request);
return waitForResponse(session);
return newFixedLengthResponse(Response.Status.OK, MIME_PLAINTEXT, "OK");
}
private WritableMap fillRequestMap(IHTTPSession session) throws IOException, ResponseException {
Method method = session.getMethod();
WritableMap request = Arguments.createMap();
request.putString("url", session.getUri());
request.putString("method", method.toString());
request.putMap("headers", this.convertToWritableMap(session.getHeaders()));
Map<String, String> files = new HashMap<>();
if (Method.POST.equals(method)) {
@ -74,84 +65,7 @@ public class Server extends NanoHTTPD {
return request;
}
private Response waitForResponse(IHTTPSession session) {
Response.Status errorStatus = null;
String errorText = "";
int timer = 0;
int interval = 500;
ReadableMap response;
while (!this.response.containsKey(session.getUri()) && (timer < timeout)) {
try {
Thread.sleep(interval);
} catch (InterruptedException e) {
Log.e(TAG, e.getMessage());
errorStatus = Response.Status.INTERNAL_ERROR;
errorText = e.getMessage();
break;
}
timer = timer + interval;
}
if (!this.response.containsKey(session.getUri())) {
errorStatus = Response.Status.NOT_FOUND;
errorText = "Resource not found";
}
if (errorStatus != null) {
return newFixedLengthResponse(errorStatus, MIME_PLAINTEXT, errorText);
} else {
response = this.response.get(session.getUri());
this.response.remove(session.getUri()); // clear responses
}
Log.d(TAG, "Sending response for " + session.getUri());
Response res = newFixedLengthResponse(
Response.Status.valueOf(response.getString("status")),
response.getString("type"),
response.getString("data"));
ReadableMap headers = response.getMap("headers");
com.facebook.react.bridge.ReadableMapKeySetIterator iterator = headers.keySetIterator();
if (!iterator.hasNextKey()) {
return null;
}
while (iterator.hasNextKey()) {
String key = iterator.nextKey();
Log.d(TAG, key + "=" + headers.getString(key));
res.addHeader(key, headers.getString(key));
}
return res;
}
private void sendEvent(ReactContext reactContext, String eventName, @Nullable WritableMap params) {
reactContext.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class).emit(eventName, params);
}
private WritableMap convertToWritableMap(Map map) {
WritableMap request = Arguments.createMap();
Set<Map.Entry<String, String>> entrySet = map.entrySet();
for (Map.Entry entry : entrySet) {
switch (entry.getValue().getClass().getName()) {
case "java.lang.Boolean":
request.putBoolean(entry.getKey().toString(), (Boolean) entry.getValue());
break;
case "java.lang.Integer":
request.putInt(entry.getKey().toString(), (Integer) entry.getValue());
break;
case "java.lang.Double":
request.putDouble(entry.getKey().toString(), (Double) entry.getValue());
break;
case "java.lang.String":
request.putString(entry.getKey().toString(), (String) entry.getValue());
break;
}
}
return request;
}
}

View File

@ -7,71 +7,15 @@ import {DeviceEventEmitter} from 'react-native';
import {NativeModules} from 'react-native';
var Server = NativeModules.HttpServer;
var validStatusCodes = ['ACCEPTED',
'BAD_REQUEST',
'CREATED',
'FORBIDDEN',
'INTERNAL_ERROR',
'METHOD_NOT_ALLOWED',
'NO_CONTENT',
'NOT_FOUND',
'NOT_MODIFIED',
'OK',
'PARTIAL_CONTENT',
'RANGE_NOT_SATISFIABLE',
'REDIRECT',
'UNAUTHORIZED'
];
module.exports = {
create: function (options, callback) {
if (options.port == 80) {
start: function (port, callback) {
if (port == 80) {
throw "Invalid server port specified. Port 80 is reserved.";
}
Server.init(options, function () {
DeviceEventEmitter.addListener('httpServerResponseReceived', function (request) {
var success = true;
var promise = new Promise(function (resolve, reject) {
callback(request, resolve);
}).then(function (response) {
if (validStatusCodes.indexOf(response.status) === 0) {
success = false;
throw "Invalid response status code specified in HttpServer options.";
}
if (response.type === null) {
response.type = "text/plain";
}
if (response.data === null) {
response.data = "";
}
if (response.headers === null) {
response.headers = {};
}
if (success) {
Server.setResponse(request.url, response);
}
});
});
}, function (e) {
throw "Could not initialize server: " + e;
Server.start(port, function () {
DeviceEventEmitter.addListener('httpServerResponseReceived', callback);
});
},
// attempt to start the instance of the server - returns a promise object that will be rejected or approved
start: function () {
var promise = new Promise(function (resolve, reject) {
Server.start(function () {
resolve();
}, function () {
reject();
});
});
return promise;
},
// effectively pause the instance of the server

1
ios/RCTHttpServer.h Normal file
View File

@ -0,0 +1 @@
#import "RCTBridgeModule.h"

58
ios/RCTHttpServer.m Normal file
View File

@ -0,0 +1,58 @@
#import "RCTHttpServer.h"
#import "RCTBridge.h"
#import "RCTLog.h"
#import "RCTEventDispatcher.h"
#import "WGCDWebServer.h"
#import "WGCDWebServerDataResponse.h"
#import "WGCDWebServerDataRequest.h"
@interface RCTHttpServer : NSObject <RCTBridgeModule> {
WGCDWebServer* _webServer;
}
@end
static RCTBridge *bridge;
@implementation RCTHttpServer
@synthesize bridge = _bridge;
RCT_EXPORT_MODULE();
RCT_EXPORT_METHOD(start:(NSInteger) port)
{
RCTLogInfo(@"Running HTTP bridge server: %d", port);
dispatch_sync(dispatch_get_main_queue(), ^{
_webServer = [[WGCDWebServer alloc] init];
[_webServer addDefaultHandlerForMethod:@"POST"
requestClass:[WGCDWebServerDataRequest class]
processBlock:^WGCDWebServerResponse *(WGCDWebServerRequest* request) {
WGCDWebServerDataRequest* dataRequest = (WGCDWebServerDataRequest*)request;
[self.bridge.eventDispatcher sendAppEventWithName:@"httpServerResponseReceived"
body:@{@"postData": dataRequest.jsonObject,
@"url": dataRequest.URL.relativeString}];
return [WGCDWebServerDataResponse responseWithStatusCode:200];
}];
[_webServer startWithPort:port bonjourName:nil];
});
}
RCT_EXPORT_METHOD(stop)
{
RCTLogInfo(@"Stopping HTTP bridge server");
dispatch_sync(dispatch_get_main_queue(), ^{
if (_webServer) {
[_webServer stop];
}
});
}
@end

View File

@ -0,0 +1,396 @@
// !$*UTF8*$!
{
archiveVersion = 1;
classes = {
};
objectVersion = 46;
objects = {
/* Begin PBXBuildFile section */
B232F4021E49DE0C00C8AEE0 /* WGCDWebServer.m in Sources */ = {isa = PBXBuildFile; fileRef = B232F3E51E49DE0C00C8AEE0 /* WGCDWebServer.m */; };
B232F4031E49DE0C00C8AEE0 /* WGCDWebServerConnection.m in Sources */ = {isa = PBXBuildFile; fileRef = B232F3E71E49DE0C00C8AEE0 /* WGCDWebServerConnection.m */; };
B232F4041E49DE0C00C8AEE0 /* WGCDWebServerFunctions.m in Sources */ = {isa = PBXBuildFile; fileRef = B232F3E91E49DE0C00C8AEE0 /* WGCDWebServerFunctions.m */; };
B232F4051E49DE0C00C8AEE0 /* WGCDWebServerRequest.m in Sources */ = {isa = PBXBuildFile; fileRef = B232F3ED1E49DE0C00C8AEE0 /* WGCDWebServerRequest.m */; };
B232F4061E49DE0C00C8AEE0 /* WGCDWebServerResponse.m in Sources */ = {isa = PBXBuildFile; fileRef = B232F3EF1E49DE0C00C8AEE0 /* WGCDWebServerResponse.m */; };
B232F4071E49DE0C00C8AEE0 /* WGCDWebServerDataRequest.m in Sources */ = {isa = PBXBuildFile; fileRef = B232F3F21E49DE0C00C8AEE0 /* WGCDWebServerDataRequest.m */; };
B232F4081E49DE0C00C8AEE0 /* WGCDWebServerFileRequest.m in Sources */ = {isa = PBXBuildFile; fileRef = B232F3F41E49DE0C00C8AEE0 /* WGCDWebServerFileRequest.m */; };
B232F4091E49DE0C00C8AEE0 /* WGCDWebServerMultiPartFormRequest.m in Sources */ = {isa = PBXBuildFile; fileRef = B232F3F61E49DE0C00C8AEE0 /* WGCDWebServerMultiPartFormRequest.m */; };
B232F40A1E49DE0C00C8AEE0 /* WGCDWebServerURLEncodedFormRequest.m in Sources */ = {isa = PBXBuildFile; fileRef = B232F3F81E49DE0C00C8AEE0 /* WGCDWebServerURLEncodedFormRequest.m */; };
B232F40B1E49DE0C00C8AEE0 /* WGCDWebServerDataResponse.m in Sources */ = {isa = PBXBuildFile; fileRef = B232F3FB1E49DE0C00C8AEE0 /* WGCDWebServerDataResponse.m */; };
B232F40C1E49DE0C00C8AEE0 /* WGCDWebServerErrorResponse.m in Sources */ = {isa = PBXBuildFile; fileRef = B232F3FD1E49DE0C00C8AEE0 /* WGCDWebServerErrorResponse.m */; };
B232F40D1E49DE0C00C8AEE0 /* WGCDWebServerFileResponse.m in Sources */ = {isa = PBXBuildFile; fileRef = B232F3FF1E49DE0C00C8AEE0 /* WGCDWebServerFileResponse.m */; };
B232F40E1E49DE0C00C8AEE0 /* WGCDWebServerStreamedResponse.m in Sources */ = {isa = PBXBuildFile; fileRef = B232F4011E49DE0C00C8AEE0 /* WGCDWebServerStreamedResponse.m */; };
B2340C241E48BA3E0024C045 /* RCTHttpServer.m in Sources */ = {isa = PBXBuildFile; fileRef = B2340C231E48BA3E0024C045 /* RCTHttpServer.m */; };
B29ECA281E48CE1C00704A36 /* libz.1.dylib in Frameworks */ = {isa = PBXBuildFile; fileRef = B29ECA271E48CE1C00704A36 /* libz.1.dylib */; };
/* End PBXBuildFile section */
/* Begin PBXCopyFilesBuildPhase section */
1441618C1BD0A79300FA4F59 /* CopyFiles */ = {
isa = PBXCopyFilesBuildPhase;
buildActionMask = 2147483647;
dstPath = "include/$(PRODUCT_NAME)";
dstSubfolderSpec = 16;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXCopyFilesBuildPhase section */
/* Begin PBXFileReference section */
B232F3E41E49DE0C00C8AEE0 /* WGCDWebServer.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = WGCDWebServer.h; sourceTree = "<group>"; };
B232F3E51E49DE0C00C8AEE0 /* WGCDWebServer.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = WGCDWebServer.m; sourceTree = "<group>"; };
B232F3E61E49DE0C00C8AEE0 /* WGCDWebServerConnection.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = WGCDWebServerConnection.h; sourceTree = "<group>"; };
B232F3E71E49DE0C00C8AEE0 /* WGCDWebServerConnection.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = WGCDWebServerConnection.m; sourceTree = "<group>"; };
B232F3E81E49DE0C00C8AEE0 /* WGCDWebServerFunctions.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = WGCDWebServerFunctions.h; sourceTree = "<group>"; };
B232F3E91E49DE0C00C8AEE0 /* WGCDWebServerFunctions.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = WGCDWebServerFunctions.m; sourceTree = "<group>"; };
B232F3EA1E49DE0C00C8AEE0 /* WGCDWebServerHTTPStatusCodes.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = WGCDWebServerHTTPStatusCodes.h; sourceTree = "<group>"; };
B232F3EB1E49DE0C00C8AEE0 /* WGCDWebServerPrivate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = WGCDWebServerPrivate.h; sourceTree = "<group>"; };
B232F3EC1E49DE0C00C8AEE0 /* WGCDWebServerRequest.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = WGCDWebServerRequest.h; sourceTree = "<group>"; };
B232F3ED1E49DE0C00C8AEE0 /* WGCDWebServerRequest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = WGCDWebServerRequest.m; sourceTree = "<group>"; };
B232F3EE1E49DE0C00C8AEE0 /* WGCDWebServerResponse.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = WGCDWebServerResponse.h; sourceTree = "<group>"; };
B232F3EF1E49DE0C00C8AEE0 /* WGCDWebServerResponse.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = WGCDWebServerResponse.m; sourceTree = "<group>"; };
B232F3F11E49DE0C00C8AEE0 /* WGCDWebServerDataRequest.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = WGCDWebServerDataRequest.h; sourceTree = "<group>"; };
B232F3F21E49DE0C00C8AEE0 /* WGCDWebServerDataRequest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = WGCDWebServerDataRequest.m; sourceTree = "<group>"; };
B232F3F31E49DE0C00C8AEE0 /* WGCDWebServerFileRequest.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = WGCDWebServerFileRequest.h; sourceTree = "<group>"; };
B232F3F41E49DE0C00C8AEE0 /* WGCDWebServerFileRequest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = WGCDWebServerFileRequest.m; sourceTree = "<group>"; };
B232F3F51E49DE0C00C8AEE0 /* WGCDWebServerMultiPartFormRequest.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = WGCDWebServerMultiPartFormRequest.h; sourceTree = "<group>"; };
B232F3F61E49DE0C00C8AEE0 /* WGCDWebServerMultiPartFormRequest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = WGCDWebServerMultiPartFormRequest.m; sourceTree = "<group>"; };
B232F3F71E49DE0C00C8AEE0 /* WGCDWebServerURLEncodedFormRequest.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = WGCDWebServerURLEncodedFormRequest.h; sourceTree = "<group>"; };
B232F3F81E49DE0C00C8AEE0 /* WGCDWebServerURLEncodedFormRequest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = WGCDWebServerURLEncodedFormRequest.m; sourceTree = "<group>"; };
B232F3FA1E49DE0C00C8AEE0 /* WGCDWebServerDataResponse.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = WGCDWebServerDataResponse.h; sourceTree = "<group>"; };
B232F3FB1E49DE0C00C8AEE0 /* WGCDWebServerDataResponse.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = WGCDWebServerDataResponse.m; sourceTree = "<group>"; };
B232F3FC1E49DE0C00C8AEE0 /* WGCDWebServerErrorResponse.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = WGCDWebServerErrorResponse.h; sourceTree = "<group>"; };
B232F3FD1E49DE0C00C8AEE0 /* WGCDWebServerErrorResponse.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = WGCDWebServerErrorResponse.m; sourceTree = "<group>"; };
B232F3FE1E49DE0C00C8AEE0 /* WGCDWebServerFileResponse.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = WGCDWebServerFileResponse.h; sourceTree = "<group>"; };
B232F3FF1E49DE0C00C8AEE0 /* WGCDWebServerFileResponse.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = WGCDWebServerFileResponse.m; sourceTree = "<group>"; };
B232F4001E49DE0C00C8AEE0 /* WGCDWebServerStreamedResponse.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = WGCDWebServerStreamedResponse.h; sourceTree = "<group>"; };
B232F4011E49DE0C00C8AEE0 /* WGCDWebServerStreamedResponse.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = WGCDWebServerStreamedResponse.m; sourceTree = "<group>"; };
B2340C221E48BA3E0024C045 /* RCTHttpServer.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTHttpServer.h; sourceTree = "<group>"; };
B2340C231E48BA3E0024C045 /* RCTHttpServer.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTHttpServer.m; sourceTree = "<group>"; };
B29EC9CC1E48BED600704A36 /* libRCTHttpServer.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = libRCTHttpServer.a; sourceTree = BUILT_PRODUCTS_DIR; };
B29EC9CE1E48BF1900704A36 /* libz.tbd */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.text-based-dylib-definition"; name = libz.tbd; path = usr/lib/libz.tbd; sourceTree = SDKROOT; };
B29ECA231E48CDCB00704A36 /* libz.1.tbd */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.text-based-dylib-definition"; name = libz.1.tbd; path = usr/lib/libz.1.tbd; sourceTree = SDKROOT; };
B29ECA251E48CDE300704A36 /* libz.1.2.8.tbd */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.text-based-dylib-definition"; name = libz.1.2.8.tbd; path = usr/lib/libz.1.2.8.tbd; sourceTree = SDKROOT; };
B29ECA271E48CE1C00704A36 /* libz.1.dylib */ = {isa = PBXFileReference; lastKnownFileType = "compiled.mach-o.dylib"; name = libz.1.dylib; path = ../../../../../../../../../usr/lib/libz.1.dylib; sourceTree = "<group>"; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
1441618B1BD0A79300FA4F59 /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
B29ECA281E48CE1C00704A36 /* libz.1.dylib in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */
145CC5671AED80100006342E = {
isa = PBXGroup;
children = (
B232F3E21E49DE0C00C8AEE0 /* WGCDWebServer */,
B2340C221E48BA3E0024C045 /* RCTHttpServer.h */,
B2340C231E48BA3E0024C045 /* RCTHttpServer.m */,
B29EC9CC1E48BED600704A36 /* libRCTHttpServer.a */,
B29EC9CD1E48BF1800704A36 /* Frameworks */,
);
sourceTree = "<group>";
};
B232F3E21E49DE0C00C8AEE0 /* WGCDWebServer */ = {
isa = PBXGroup;
children = (
B232F3E31E49DE0C00C8AEE0 /* Core */,
B232F3F01E49DE0C00C8AEE0 /* Requests */,
B232F3F91E49DE0C00C8AEE0 /* Responses */,
);
path = WGCDWebServer;
sourceTree = "<group>";
};
B232F3E31E49DE0C00C8AEE0 /* Core */ = {
isa = PBXGroup;
children = (
B232F3E41E49DE0C00C8AEE0 /* WGCDWebServer.h */,
B232F3E51E49DE0C00C8AEE0 /* WGCDWebServer.m */,
B232F3E61E49DE0C00C8AEE0 /* WGCDWebServerConnection.h */,
B232F3E71E49DE0C00C8AEE0 /* WGCDWebServerConnection.m */,
B232F3E81E49DE0C00C8AEE0 /* WGCDWebServerFunctions.h */,
B232F3E91E49DE0C00C8AEE0 /* WGCDWebServerFunctions.m */,
B232F3EA1E49DE0C00C8AEE0 /* WGCDWebServerHTTPStatusCodes.h */,
B232F3EB1E49DE0C00C8AEE0 /* WGCDWebServerPrivate.h */,
B232F3EC1E49DE0C00C8AEE0 /* WGCDWebServerRequest.h */,
B232F3ED1E49DE0C00C8AEE0 /* WGCDWebServerRequest.m */,
B232F3EE1E49DE0C00C8AEE0 /* WGCDWebServerResponse.h */,
B232F3EF1E49DE0C00C8AEE0 /* WGCDWebServerResponse.m */,
);
path = Core;
sourceTree = "<group>";
};
B232F3F01E49DE0C00C8AEE0 /* Requests */ = {
isa = PBXGroup;
children = (
B232F3F11E49DE0C00C8AEE0 /* WGCDWebServerDataRequest.h */,
B232F3F21E49DE0C00C8AEE0 /* WGCDWebServerDataRequest.m */,
B232F3F31E49DE0C00C8AEE0 /* WGCDWebServerFileRequest.h */,
B232F3F41E49DE0C00C8AEE0 /* WGCDWebServerFileRequest.m */,
B232F3F51E49DE0C00C8AEE0 /* WGCDWebServerMultiPartFormRequest.h */,
B232F3F61E49DE0C00C8AEE0 /* WGCDWebServerMultiPartFormRequest.m */,
B232F3F71E49DE0C00C8AEE0 /* WGCDWebServerURLEncodedFormRequest.h */,
B232F3F81E49DE0C00C8AEE0 /* WGCDWebServerURLEncodedFormRequest.m */,
);
path = Requests;
sourceTree = "<group>";
};
B232F3F91E49DE0C00C8AEE0 /* Responses */ = {
isa = PBXGroup;
children = (
B232F3FA1E49DE0C00C8AEE0 /* WGCDWebServerDataResponse.h */,
B232F3FB1E49DE0C00C8AEE0 /* WGCDWebServerDataResponse.m */,
B232F3FC1E49DE0C00C8AEE0 /* WGCDWebServerErrorResponse.h */,
B232F3FD1E49DE0C00C8AEE0 /* WGCDWebServerErrorResponse.m */,
B232F3FE1E49DE0C00C8AEE0 /* WGCDWebServerFileResponse.h */,
B232F3FF1E49DE0C00C8AEE0 /* WGCDWebServerFileResponse.m */,
B232F4001E49DE0C00C8AEE0 /* WGCDWebServerStreamedResponse.h */,
B232F4011E49DE0C00C8AEE0 /* WGCDWebServerStreamedResponse.m */,
);
path = Responses;
sourceTree = "<group>";
};
B29EC9CD1E48BF1800704A36 /* Frameworks */ = {
isa = PBXGroup;
children = (
B29ECA271E48CE1C00704A36 /* libz.1.dylib */,
B29ECA251E48CDE300704A36 /* libz.1.2.8.tbd */,
B29ECA231E48CDCB00704A36 /* libz.1.tbd */,
B29EC9CE1E48BF1900704A36 /* libz.tbd */,
);
name = Frameworks;
sourceTree = "<group>";
};
/* End PBXGroup section */
/* Begin PBXNativeTarget section */
1441618D1BD0A79300FA4F59 /* RCTHttpServer */ = {
isa = PBXNativeTarget;
buildConfigurationList = 144161941BD0A79300FA4F59 /* Build configuration list for PBXNativeTarget "RCTHttpServer" */;
buildPhases = (
1441618A1BD0A79300FA4F59 /* Sources */,
1441618B1BD0A79300FA4F59 /* Frameworks */,
1441618C1BD0A79300FA4F59 /* CopyFiles */,
);
buildRules = (
);
dependencies = (
);
name = RCTHttpServer;
productName = RCTHttpServer;
productReference = B29EC9CC1E48BED600704A36 /* libRCTHttpServer.a */;
productType = "com.apple.product-type.library.static";
};
/* End PBXNativeTarget section */
/* Begin PBXProject section */
145CC5681AED80100006342E /* Project object */ = {
isa = PBXProject;
attributes = {
LastUpgradeCheck = 0630;
ORGANIZATIONNAME = rt2zz;
TargetAttributes = {
1441618D1BD0A79300FA4F59 = {
CreatedOnToolsVersion = 7.0.1;
};
};
};
buildConfigurationList = 145CC56B1AED80100006342E /* Build configuration list for PBXProject "RCTHttpServer" */;
compatibilityVersion = "Xcode 3.2";
developmentRegion = English;
hasScannedForEncodings = 0;
knownRegions = (
en,
);
mainGroup = 145CC5671AED80100006342E;
productRefGroup = 145CC5671AED80100006342E;
projectDirPath = "";
projectRoot = "";
targets = (
1441618D1BD0A79300FA4F59 /* RCTHttpServer */,
);
};
/* End PBXProject section */
/* Begin PBXSourcesBuildPhase section */
1441618A1BD0A79300FA4F59 /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
B232F4081E49DE0C00C8AEE0 /* WGCDWebServerFileRequest.m in Sources */,
B232F4071E49DE0C00C8AEE0 /* WGCDWebServerDataRequest.m in Sources */,
B232F40D1E49DE0C00C8AEE0 /* WGCDWebServerFileResponse.m in Sources */,
B232F40C1E49DE0C00C8AEE0 /* WGCDWebServerErrorResponse.m in Sources */,
B232F40E1E49DE0C00C8AEE0 /* WGCDWebServerStreamedResponse.m in Sources */,
B232F40B1E49DE0C00C8AEE0 /* WGCDWebServerDataResponse.m in Sources */,
B232F4021E49DE0C00C8AEE0 /* WGCDWebServer.m in Sources */,
B232F4051E49DE0C00C8AEE0 /* WGCDWebServerRequest.m in Sources */,
B232F4031E49DE0C00C8AEE0 /* WGCDWebServerConnection.m in Sources */,
B232F40A1E49DE0C00C8AEE0 /* WGCDWebServerURLEncodedFormRequest.m in Sources */,
B232F4061E49DE0C00C8AEE0 /* WGCDWebServerResponse.m in Sources */,
B232F4041E49DE0C00C8AEE0 /* WGCDWebServerFunctions.m in Sources */,
B232F4091E49DE0C00C8AEE0 /* WGCDWebServerMultiPartFormRequest.m in Sources */,
B2340C241E48BA3E0024C045 /* RCTHttpServer.m in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXSourcesBuildPhase section */
/* Begin XCBuildConfiguration section */
144161951BD0A79300FA4F59 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
GCC_NO_COMMON_BLOCKS = NO;
HEADER_SEARCH_PATHS = (
"$(inherited)",
/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/include,
"$(SRCROOT)/../../../React/**",
"$(SRCROOT)/../../react-native/React/**",
"$(SDKROOT)/usr/include/libxml2/**",
);
OTHER_LDFLAGS = "-ObjC";
PRODUCT_NAME = "$(TARGET_NAME)";
SKIP_INSTALL = YES;
STRIP_INSTALLED_PRODUCT = NO;
};
name = Debug;
};
144161961BD0A79300FA4F59 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
GCC_NO_COMMON_BLOCKS = NO;
HEADER_SEARCH_PATHS = (
"$(inherited)",
/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/include,
"$(SRCROOT)/../../../React/**",
"$(SRCROOT)/../../react-native/React/**",
"$(SDKROOT)/usr/include/libxml2/**",
);
OTHER_LDFLAGS = "-ObjC";
PRODUCT_NAME = "$(TARGET_NAME)";
SKIP_INSTALL = YES;
STRIP_INSTALLED_PRODUCT = NO;
};
name = Release;
};
145CC5821AED80100006342E /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
ENABLE_STRICT_OBJC_MSGSEND = YES;
GCC_C_LANGUAGE_STANDARD = gnu99;
GCC_DYNAMIC_NO_PIC = NO;
GCC_NO_COMMON_BLOCKS = YES;
GCC_OPTIMIZATION_LEVEL = 0;
GCC_PREPROCESSOR_DEFINITIONS = (
"DEBUG=1",
"$(inherited)",
);
GCC_SYMBOLS_PRIVATE_EXTERN = NO;
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
HEADER_SEARCH_PATHS = (
"$(inherited)",
"$(SRCROOT)/../../../React/**",
"$(SRCROOT)/../../react-native/React/**",
);
IPHONEOS_DEPLOYMENT_TARGET = 7.0;
MTL_ENABLE_DEBUG_INFO = YES;
ONLY_ACTIVE_ARCH = YES;
SDKROOT = iphoneos;
USER_HEADER_SEARCH_PATHS = "";
};
name = Debug;
};
145CC5831AED80100006342E /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
GCC_C_LANGUAGE_STANDARD = gnu99;
GCC_NO_COMMON_BLOCKS = YES;
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
HEADER_SEARCH_PATHS = (
"$(inherited)",
"$(SRCROOT)/../../../React/**",
"$(SRCROOT)/../../react-native/React/**",
);
IPHONEOS_DEPLOYMENT_TARGET = 7.0;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos;
USER_HEADER_SEARCH_PATHS = "";
VALIDATE_PRODUCT = YES;
};
name = Release;
};
/* End XCBuildConfiguration section */
/* Begin XCConfigurationList section */
144161941BD0A79300FA4F59 /* Build configuration list for PBXNativeTarget "RCTHttpServer" */ = {
isa = XCConfigurationList;
buildConfigurations = (
144161951BD0A79300FA4F59 /* Debug */,
144161961BD0A79300FA4F59 /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
145CC56B1AED80100006342E /* Build configuration list for PBXProject "RCTHttpServer" */ = {
isa = XCConfigurationList;
buildConfigurations = (
145CC5821AED80100006342E /* Debug */,
145CC5831AED80100006342E /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
/* End XCConfigurationList section */
};
rootObject = 145CC5681AED80100006342E /* Project object */;
}

View File

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<Workspace
version = "1.0">
<FileRef
location = "self:/Users/alwx/Documents/Dev/2016/status-react/node_modules/react-native-http-server/ios/RCTHttpServer.xcodeproj">
</FileRef>
</Workspace>

View File

@ -0,0 +1,89 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "0820"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "1441618D1BD0A79300FA4F59"
BuildableName = "libRCTHttpServer.a"
BlueprintName = "RCTHttpServer"
ReferencedContainer = "container:RCTHttpServer.xcodeproj">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES">
<Testables>
</Testables>
<MacroExpansion>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "1441618D1BD0A79300FA4F59"
BuildableName = "libRCTHttpServer.a"
BlueprintName = "RCTHttpServer"
ReferencedContainer = "container:RCTHttpServer.xcodeproj">
</BuildableReference>
</MacroExpansion>
<AdditionalOptions>
</AdditionalOptions>
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
allowLocationSimulation = "YES">
<MacroExpansion>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "1441618D1BD0A79300FA4F59"
BuildableName = "libRCTHttpServer.a"
BlueprintName = "RCTHttpServer"
ReferencedContainer = "container:RCTHttpServer.xcodeproj">
</BuildableReference>
</MacroExpansion>
<AdditionalOptions>
</AdditionalOptions>
</LaunchAction>
<ProfileAction
buildConfiguration = "Release"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES">
<MacroExpansion>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "1441618D1BD0A79300FA4F59"
BuildableName = "libRCTHttpServer.a"
BlueprintName = "RCTHttpServer"
ReferencedContainer = "container:RCTHttpServer.xcodeproj">
</BuildableReference>
</MacroExpansion>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>

View File

@ -0,0 +1,22 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>SchemeUserState</key>
<dict>
<key>RCTHttpServer.xcscheme</key>
<dict>
<key>orderHint</key>
<integer>0</integer>
</dict>
</dict>
<key>SuppressBuildableAutocreation</key>
<dict>
<key>1441618D1BD0A79300FA4F59</key>
<dict>
<key>primary</key>
<true/>
</dict>
</dict>
</dict>
</plist>

View File

@ -0,0 +1,619 @@
/*
Copyright (c) 2012-2015, 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 "WGCDWebServerRequest.h"
#import "WGCDWebServerResponse.h"
/**
* The WGCDWebServerMatchBlock is called for every handler added to the
* WGCDWebServer 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
* WGCDWebServerRequest instance created with the same basic info.
* Otherwise, it simply returns nil.
*/
typedef WGCDWebServerRequest* (^WGCDWebServerMatchBlock)(NSString* requestMethod, NSURL* requestURL, NSDictionary* requestHeaders, NSString* urlPath, NSDictionary* urlQuery);
/**
* The WGCDWebServerProcessBlock is called after the HTTP request has been fully
* received (i.e. the entire HTTP body has been read). The block is passed the
* WGCDWebServerRequest created at the previous step by the WGCDWebServerMatchBlock.
*
* The block must return a WGCDWebServerResponse or nil on error, which will
* result in a 500 HTTP status code returned to the client. It's however
* recommended to return a WGCDWebServerErrorResponse on error so more useful
* information can be returned to the client.
*/
typedef WGCDWebServerResponse* (^WGCDWebServerProcessBlock)(__kindof WGCDWebServerRequest* request);
/**
* The WGCDWebServerAsynchronousProcessBlock works like the WGCDWebServerProcessBlock
* except the WGCDWebServerResponse 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 WGCDWebServerResponse
* or nil on error, which will result in a 500 HTTP status code returned to the client.
* It's however recommended to return a WGCDWebServerErrorResponse on error so more
* useful information can be returned to the client.
*/
typedef void (^WGCDWebServerCompletionBlock)(WGCDWebServerResponse* response);
typedef void (^WGCDWebServerAsyncProcessBlock)(__kindof WGCDWebServerRequest* request, WGCDWebServerCompletionBlock completionBlock);
/**
* The port used by the WGCDWebServer (NSNumber / NSUInteger).
*
* The default value is 0 i.e. let the OS pick a random port.
*/
extern NSString* const WGCDWebServerOption_Port;
/**
* The Bonjour name used by the WGCDWebServer (NSString). If set to an empty string,
* the name will automatically take the value of the WGCDWebServerOption_ServerName
* option. If this option is set to nil, Bonjour will be disabled.
*
* The default value is nil.
*/
extern NSString* const WGCDWebServerOption_BonjourName;
/**
* The Bonjour service type used by the WGCDWebServer (NSString).
*
* The default value is "_http._tcp", the service type for HTTP web servers.
*/
extern NSString* const WGCDWebServerOption_BonjourType;
/**
* Request a port mapping in the NAT gateway (NSNumber / BOOL).
*
* This uses the DNSService API under the hood which supports IPv4 mappings only.
*
* The default value is NO.
*
* @warning The external port set up by the NAT gateway may be different than
* the one used by the WGCDWebServer.
*/
extern NSString* const WGCDWebServerOption_RequestNATPortMapping;
/**
* Only accept HTTP requests coming from localhost i.e. not from the outside
* network (NSNumber / BOOL).
*
* The default value is NO.
*
* @warning Bonjour and NAT port mapping should be disabled if using this option
* since the server will not be reachable from the outside network anyway.
*/
extern NSString* const WGCDWebServerOption_BindToLocalhost;
/**
* The maximum number of incoming HTTP requests that can be queued waiting to
* be handled before new ones are dropped (NSNumber / NSUInteger).
*
* The default value is 16.
*/
extern NSString* const WGCDWebServerOption_MaxPendingConnections;
/**
* The value for "Server" HTTP header used by the WGCDWebServer (NSString).
*
* The default value is the WGCDWebServer class name.
*/
extern NSString* const WGCDWebServerOption_ServerName;
/**
* The authentication method used by the WGCDWebServer
* (one of "WGCDWebServerAuthenticationMethod_...").
*
* The default value is nil i.e. authentication is disabled.
*/
extern NSString* const WGCDWebServerOption_AuthenticationMethod;
/**
* The authentication realm used by the WGCDWebServer (NSString).
*
* The default value is the same as the WGCDWebServerOption_ServerName option.
*/
extern NSString* const WGCDWebServerOption_AuthenticationRealm;
/**
* The authentication accounts used by the WGCDWebServer
* (NSDictionary of username / password pairs).
*
* The default value is nil i.e. no accounts.
*/
extern NSString* const WGCDWebServerOption_AuthenticationAccounts;
/**
* The class used by the WGCDWebServer when instantiating WGCDWebServerConnection
* (subclass of WGCDWebServerConnection).
*
* The default value is the WGCDWebServerConnection class.
*/
extern NSString* const WGCDWebServerOption_ConnectionClass;
/**
* Allow the WGCDWebServer 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 WGCDWebServerOption_AutomaticallyMapHEADToGET;
/**
* The interval expressed in seconds used by the WGCDWebServer 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 WGCDWebServerOption_ConnectedStateCoalescingInterval;
/**
* Set the dispatch queue priority on which server connection will be
* run (NSNumber / long).
*
*
* The default value is DISPATCH_QUEUE_PRIORITY_DEFAULT.
*/
extern NSString* const WGCDWebServerOption_DispatchQueuePriority;
#if TARGET_OS_IPHONE
/**
* Enables the WGCDWebServer to automatically suspend itself (as if -stop was
* called) when the iOS app goes into the background and the last
* WGCDWebServerConnection 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 WGCDWebServer is suspended.
*/
extern NSString* const WGCDWebServerOption_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 WGCDWebServerAuthenticationMethod_Basic;
/**
* HTTP Digest Access Authentication scheme (see https://tools.ietf.org/html/rfc2617).
*/
extern NSString* const WGCDWebServerAuthenticationMethod_DigestAccess;
@class WGCDWebServer;
/**
* Delegate methods for WGCDWebServer.
*
* @warning These methods are always called on the main thread in a serialized way.
*/
@protocol WGCDWebServerDelegate <NSObject>
@optional
/**
* This method is called after the server has successfully started.
*/
- (void)webServerDidStart:(WGCDWebServer*)server;
/**
* This method is called after the Bonjour registration for the server has
* successfully completed.
*
* Use the "bonjourServerURL" property to retrieve the Bonjour address of the
* server.
*/
- (void)webServerDidCompleteBonjourRegistration:(WGCDWebServer*)server;
/**
* This method is called after the NAT port mapping for the server has been
* updated.
*
* Use the "publicServerURL" property to retrieve the public address of the
* server.
*/
- (void)webServerDidUpdateNATPortMapping:(WGCDWebServer*)server;
/**
* This method is called when the first WGCDWebServerConnection 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 WGCDWebServerConnection instances keep being opened),
* until before the last HTTP request has been responded to (and the
* corresponding last WGCDWebServerConnection closed).
*/
- (void)webServerDidConnect:(WGCDWebServer*)server;
/**
* This method is called when the last WGCDWebServerConnection is closed after
* the server has served a series of HTTP requests.
*
* The WGCDWebServerOption_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:(WGCDWebServer*)server;
/**
* This method is called after the server has stopped.
*/
- (void)webServerDidStop:(WGCDWebServer*)server;
@end
/**
* The WGCDWebServer 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.
*
* WGCDWebServer 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 WGCDWebServer.
*/
@interface WGCDWebServer : NSObject
/**
* Sets the delegate for the server.
*/
@property(nonatomic, assign) id<WGCDWebServerDelegate> 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:(WGCDWebServerMatchBlock)matchBlock processBlock:(WGCDWebServerProcessBlock)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:(WGCDWebServerMatchBlock)matchBlock asyncProcessBlock:(WGCDWebServerAsyncProcessBlock)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 WGCDWebServerConnection instances
* currently handling already received HTTP requests. These connections will
* continue to execute normally until completion.
*/
- (void)stop;
@end
@interface WGCDWebServer (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;
/**
* Returns the server's public URL.
*
* @warning This property is only valid if the server is running and NAT port
* mapping is active.
*/
@property(nonatomic, readonly) NSURL* publicServerURL;
/**
* Starts the server on port 8080 (OS X & iOS Simulator) or port 80 (iOS)
* using the default Bonjour name.
*
* Returns NO if the server failed to start.
*/
- (BOOL)start;
/**
* Starts the server on a given port and with a specific Bonjour name.
* Pass a nil Bonjour name to disable Bonjour entirely or an empty string to
* use the default name.
*
* Returns NO if the server failed to start.
*/
- (BOOL)startWithPort:(NSUInteger)port bonjourName:(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 WGCDWebServer (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:(WGCDWebServerProcessBlock)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:(WGCDWebServerAsyncProcessBlock)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:(WGCDWebServerProcessBlock)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:(WGCDWebServerAsyncProcessBlock)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:(WGCDWebServerProcessBlock)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:(WGCDWebServerAsyncProcessBlock)block;
@end
@interface WGCDWebServer (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
/**
* WGCDWebServer 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.
*
* WGCDWebServer is also compatible with a limited set of third-party logging
* facilities. If one of them is available at compile time, WGCDWebServer will
* automatically use it in place of the built-in one.
*
* Currently supported third-party logging facilities are:
* - XLFacility (by the same author as WGCDWebServer): 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 WGCDWebServer use a custom logging facility by defining
* the "__WGCDWEBSERVER_LOGGING_HEADER__" preprocessor constant in Xcode build
* settings to the name of a custom header file (escaped like \"MyLogging.h\").
* This header file must define the following set of macros:
*
* GWS_LOG_DEBUG(...)
* GWS_LOG_VERBOSE(...)
* GWS_LOG_INFO(...)
* GWS_LOG_WARNING(...)
* GWS_LOG_ERROR(...)
*
* IMPORTANT: These macros must behave like NSLog(). Furthermore the GWS_LOG_DEBUG()
* macro should not do anything unless the preprocessor constant "DEBUG" evaluates
* to non-zero.
*
* The logging methods below send log messages to the same logging facility
* used by WGCDWebServer. They can be used for consistency wherever you interact
* with WGCDWebServer in your code (e.g. in the implementation of handlers).
*/
@interface WGCDWebServer (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.
*
* If using the built-in logging facility, the log levels are as follow:
* DEBUG = 0
* VERBOSE = 1
* INFO = 2
* WARNING = 3
* ERROR = 4
*/
+ (void)setLogLevel:(int)level;
/**
* Logs a message to the logging facility at the VERBOSE level.
*/
- (void)logVerbose:(NSString*)format, ... NS_FORMAT_FUNCTION(1,2);
/**
* Logs a message to the logging facility at the INFO level.
*/
- (void)logInfo:(NSString*)format, ... NS_FORMAT_FUNCTION(1,2);
/**
* Logs a message to the logging facility at the WARNING level.
*/
- (void)logWarning:(NSString*)format, ... NS_FORMAT_FUNCTION(1,2);
/**
* Logs a message to the logging facility at the ERROR level.
*/
- (void)logError:(NSString*)format, ... NS_FORMAT_FUNCTION(1,2);
@end
#ifdef __WGCDWEBSERVER_ENABLE_TESTING__
@interface WGCDWebServer (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-2015, 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 "WGCDWebServer.h"
@class WGCDWebServerHandler;
/**
* The WGCDWebServerConnection class is instantiated by WGCDWebServer 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 WGCDWebServerOption_ConnectionClass
* option for WGCDWebServer to install your custom subclass.
*
* @warning The WGCDWebServerConnection retains the WGCDWebServer until the
* connection is closed.
*/
@interface WGCDWebServerConnection : NSObject
/**
* Returns the WGCDWebServer that owns the connection.
*/
@property(nonatomic, readonly) WGCDWebServer* 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 WGCDWebServer HTTP connections.
*
* @warning These methods can be called on any WGCD thread.
* Be sure to also call "super" when overriding them.
*/
@interface WGCDWebServerConnection (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 WGCDWebServerResponse 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.
*/
- (WGCDWebServerResponse*)preflightRequest:(WGCDWebServerRequest*)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:(WGCDWebServerRequest*)request completion:(WGCDWebServerCompletionBlock)completion;
/**
* Assuming a valid HTTP request was received and either -preflightRequest:
* or -processRequest:completion: returned a non-nil WGCDWebServerResponse,
* 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.
*/
- (WGCDWebServerResponse*)overrideResponse:(WGCDWebServerResponse*)response forRequest:(WGCDWebServerRequest*)request;
/**
* This method is called if any error happens while validing or processing
* the request or if no WGCDWebServerResponse 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:(WGCDWebServerRequest*)request withStatusCode:(NSInteger)statusCode;
/**
* Called when the connection is closed.
*/
- (void)close;
@end

View File

@ -0,0 +1,846 @@
/*
Copyright (c) 2012-2015, 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 WGCDWebServer requires ARC
#endif
#import <TargetConditionals.h>
#import <netdb.h>
#ifdef __WGCDWEBSERVER_ENABLE_TESTING__
#import <libkern/OSAtomic.h>
#endif
#import "WGCDWebServerPrivate.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 __WGCDWEBSERVER_ENABLE_TESTING__
static int32_t _connectionCounter = 0;
#endif
@interface WGCDWebServerConnection () {
@private
WGCDWebServer* _server;
NSData* _localAddress;
NSData* _remoteAddress;
CFSocketNativeHandle _socket;
NSUInteger _bytesRead;
NSUInteger _bytesWritten;
BOOL _virtualHEAD;
CFHTTPMessageRef _requestMessage;
WGCDWebServerRequest* _request;
WGCDWebServerHandler* _handler;
CFHTTPMessageRef _responseMessage;
WGCDWebServerResponse* _response;
NSInteger _statusCode;
BOOL _opened;
#ifdef __WGCDWEBSERVER_ENABLE_TESTING__
NSUInteger _connectionIndex;
NSString* _requestPath;
int _requestFD;
NSString* _responsePath;
int _responseFD;
#endif
}
@end
@implementation WGCDWebServerConnection (Read)
- (void)_readData:(NSMutableData*)data withLength:(NSUInteger)length completionBlock:(ReadDataCompletionBlock)block {
dispatch_read(_socket, length, dispatch_get_global_queue(_server.dispatchQueuePriority, 0), ^(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 WGCDWebServerConnection (Write)
- (void)_writeData:(NSData*)data withCompletionBlock:(WriteDataCompletionBlock)block {
dispatch_data_t buffer = dispatch_data_create(data.bytes, data.length, dispatch_get_global_queue(_server.dispatchQueuePriority, 0), ^{
[data self]; // Keeps ARC from releasing data too early
});
dispatch_write(_socket, buffer, dispatch_get_global_queue(_server.dispatchQueuePriority, 0), ^(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 WGCDWebServerConnection
@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 = WGCDWebServerComputeMD5Digest(@"%@", 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)WGCDWebServerFormatRFC822([NSDate date]));
}
- (void)_startProcessingRequest {
GWS_DCHECK(_responseMessage == NULL);
WGCDWebServerResponse* preflightResponse = [self preflightRequest:_request];
if (preflightResponse) {
[self _finishProcessingRequest:preflightResponse];
} else {
[self processRequest:_request completion:^(WGCDWebServerResponse* processResponse) {
[self _finishProcessingRequest:processResponse];
}];
}
}
// http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html
- (void)_finishProcessingRequest:(WGCDWebServerResponse*)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)WGCDWebServerFormatRFC822(_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)WGCDWebServerNormalizeHeaderValue(_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:kWGCDWebServerHTTPStatusCode_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:kWGCDWebServerHTTPStatusCode_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:kWGCDWebServerHTTPStatusCode_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:kWGCDWebServerHTTPStatusCode_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:kWGCDWebServerHTTPStatusCode_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:kWGCDWebServerHTTPStatusCode_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:kWGCDWebServerHTTPStatusCode_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 ? WGCDWebServerUnescapeURLString(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 ? WGCDWebServerParseURLEncodedForm(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) {
_request.localAddressData = self.localAddressData;
_request.remoteAddressData = self.remoteAddressData;
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:kWGCDWebServerHTTPStatusCode_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:kWGCDWebServerHTTPStatusCode_BadRequest];
}
} else {
[self _startProcessingRequest];
}
} else {
_request = [[WGCDWebServerRequest alloc] initWithMethod:requestMethod url:requestURL headers:requestHeaders path:requestPath query:requestQuery];
GWS_DCHECK(_request);
[self abortRequest:_request withStatusCode:kWGCDWebServerHTTPStatusCode_MethodNotAllowed];
}
} else {
[self abortRequest:nil withStatusCode:kWGCDWebServerHTTPStatusCode_InternalServerError];
GWS_DNOT_REACHED();
}
} else {
[self abortRequest:nil withStatusCode:kWGCDWebServerHTTPStatusCode_InternalServerError];
}
}];
}
- (id)initWithServer:(WGCDWebServer*)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 WGCDWebServerStringFromSockAddr(_localAddress.bytes, YES);
}
- (NSString*)remoteAddressString {
return WGCDWebServerStringFromSockAddr(_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 WGCDWebServerConnection (Subclassing)
- (BOOL)open {
#ifdef __WGCDWEBSERVER_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 __WGCDWEBSERVER_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 __WGCDWEBSERVER_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
- (WGCDWebServerResponse*)preflightRequest:(WGCDWebServerRequest*)request {
GWS_LOG_DEBUG(@"Connection on socket %i preflighting request \"%@ %@\" with %lu bytes body", _socket, _virtualHEAD ? @"HEAD" : _request.method, _request.path, (unsigned long)_bytesRead);
WGCDWebServerResponse* 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 = [WGCDWebServerResponse responseWithStatusCode:kWGCDWebServerHTTPStatusCode_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 = WGCDWebServerExtractHeaderValueParameter(authorizationHeader, @"realm");
if ([realm isEqualToString:_server.authenticationRealm]) {
NSString* nonce = WGCDWebServerExtractHeaderValueParameter(authorizationHeader, @"nonce");
if ([nonce isEqualToString:_digestAuthenticationNonce]) {
NSString* username = WGCDWebServerExtractHeaderValueParameter(authorizationHeader, @"username");
NSString* uri = WGCDWebServerExtractHeaderValueParameter(authorizationHeader, @"uri");
NSString* actualResponse = WGCDWebServerExtractHeaderValueParameter(authorizationHeader, @"response");
NSString* ha1 = [_server.authenticationDigestAccounts objectForKey:username];
NSString* ha2 = WGCDWebServerComputeMD5Digest(@"%@:%@", request.method, uri); // We cannot use "request.path" as the query string is required
NSString* expectedResponse = WGCDWebServerComputeMD5Digest(@"%@:%@:%@", ha1, _digestAuthenticationNonce, ha2);
if ([actualResponse isEqualToString:expectedResponse]) {
authenticated = YES;
}
} else if (nonce.length) {
isStaled = YES;
}
}
}
if (!authenticated) {
response = [WGCDWebServerResponse responseWithStatusCode:kWGCDWebServerHTTPStatusCode_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:(WGCDWebServerRequest*)request completion:(WGCDWebServerCompletionBlock)completion {
GWS_LOG_DEBUG(@"Connection on socket %i processing request \"%@ %@\" with %lu bytes body", _socket, _virtualHEAD ? @"HEAD" : _request.method, _request.path, (unsigned long)_bytesRead);
_handler.asyncProcessBlock(request, [completion copy]);
}
// http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.25
// http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.26
static inline BOOL _CompareResources(NSString* responseETag, NSString* requestETag, NSDate* responseLastModified, NSDate* requestLastModified) {
if (requestLastModified && responseLastModified) {
if ([responseLastModified compare:requestLastModified] != NSOrderedDescending) {
return YES;
}
}
if (requestETag && responseETag) { // Per the specs "If-None-Match" must be checked after "If-Modified-Since"
if ([requestETag isEqualToString:@"*"]) {
return YES;
}
if ([responseETag isEqualToString:requestETag]) {
return YES;
}
}
return NO;
}
- (WGCDWebServerResponse*)overrideResponse:(WGCDWebServerResponse*)response forRequest:(WGCDWebServerRequest*)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"] ? kWGCDWebServerHTTPStatusCode_NotModified : kWGCDWebServerHTTPStatusCode_PreconditionFailed;
WGCDWebServerResponse* newResponse = [WGCDWebServerResponse responseWithStatusCode:code];
newResponse.cacheControlMaxAge = response.cacheControlMaxAge;
newResponse.lastModifiedDate = response.lastModifiedDate;
newResponse.eTag = response.eTag;
GWS_DCHECK(newResponse);
return newResponse;
}
return response;
}
- (void)abortRequest:(WGCDWebServerRequest*)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 __WGCDWEBSERVER_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-2015, 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* WGCDWebServerGetMimeTypeForExtension(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* WGCDWebServerEscapeURLString(NSString* string);
/**
* Unescapes a URL percent-encoded string.
*/
NSString* WGCDWebServerUnescapeURLString(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* WGCDWebServerParseURLEncodedForm(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* WGCDWebServerGetPrimaryIPAddress(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* WGCDWebServerFormatRFC822(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* WGCDWebServerParseRFC822(NSString* string);
/**
* Converts a date into a string using IOS 8601 formatting.
* http://tools.ietf.org/html/rfc3339#section-5.6
*/
NSString* WGCDWebServerFormatISO8601(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* WGCDWebServerParseISO8601(NSString* string);
#ifdef __cplusplus
}
#endif

View File

@ -0,0 +1,307 @@
/*
Copyright (c) 2012-2015, 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 WGCDWebServer 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 "WGCDWebServerPrivate.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 WGCDWebServerInitializeFunctions() {
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* WGCDWebServerNormalizeHeaderValue(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* WGCDWebServerTruncateHeaderValue(NSString* value) {
NSRange range = [value rangeOfString:@";"];
return range.location != NSNotFound ? [value substringToIndex:range.location] : value;
}
NSString* WGCDWebServerExtractHeaderValueParameter(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 WGCDWebServerStringEncodingFromCharset(NSString* charset) {
NSStringEncoding encoding = kCFStringEncodingInvalidId;
if (charset) {
encoding = CFStringConvertEncodingToNSStringEncoding(CFStringConvertIANACharSetNameToEncoding((CFStringRef)charset));
}
return (encoding != kCFStringEncodingInvalidId ? encoding : NSUTF8StringEncoding);
}
NSString* WGCDWebServerFormatRFC822(NSDate* date) {
__block NSString* string;
dispatch_sync(_dateFormatterQueue, ^{
string = [_dateFormatterRFC822 stringFromDate:date];
});
return string;
}
NSDate* WGCDWebServerParseRFC822(NSString* string) {
__block NSDate* date;
dispatch_sync(_dateFormatterQueue, ^{
date = [_dateFormatterRFC822 dateFromString:string];
});
return date;
}
NSString* WGCDWebServerFormatISO8601(NSDate* date) {
__block NSString* string;
dispatch_sync(_dateFormatterQueue, ^{
string = [_dateFormatterISO8601 stringFromDate:date];
});
return string;
}
NSDate* WGCDWebServerParseISO8601(NSString* string) {
__block NSDate* date;
dispatch_sync(_dateFormatterQueue, ^{
date = [_dateFormatterISO8601 dateFromString:string];
});
return date;
}
BOOL WGCDWebServerIsTextContentType(NSString* type) {
return ([type hasPrefix:@"text/"] || [type hasPrefix:@"application/json"] || [type hasPrefix:@"application/xml"]);
}
NSString* WGCDWebServerDescribeData(NSData* data, NSString* type) {
if (WGCDWebServerIsTextContentType(type)) {
NSString* charset = WGCDWebServerExtractHeaderValueParameter(type, @"charset");
NSString* string = [[NSString alloc] initWithData:data encoding:WGCDWebServerStringEncodingFromCharset(charset)];
if (string) {
return string;
}
}
return [NSString stringWithFormat:@"<%lu bytes>", (unsigned long)data.length];
}
NSString* WGCDWebServerGetMimeTypeForExtension(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 : kWGCDWebServerDefaultMimeType;
}
NSString* WGCDWebServerEscapeURLString(NSString* string) {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
return CFBridgingRelease(CFURLCreateStringByAddingPercentEscapes(kCFAllocatorDefault, (CFStringRef)string, NULL, CFSTR(":@/?&=+"), kCFStringEncodingUTF8));
#pragma clang diagnostic pop
}
NSString* WGCDWebServerUnescapeURLString(NSString* string) {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
return CFBridgingRelease(CFURLCreateStringByReplacingPercentEscapesUsingEncoding(kCFAllocatorDefault, (CFStringRef)string, CFSTR(""), kCFStringEncodingUTF8));
#pragma clang diagnostic pop
}
NSDictionary* WGCDWebServerParseURLEncodedForm(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 ? WGCDWebServerUnescapeURLString(key) : nil;
value = [value stringByReplacingOccurrencesOfString:@"+" withString:@" "];
NSString* unescapedValue = value ? WGCDWebServerUnescapeURLString(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* WGCDWebServerStringFromSockAddr(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* WGCDWebServerGetPrimaryIPAddress(BOOL useIPv6) {
NSString* address = nil;
#if TARGET_OS_IPHONE
#if !TARGET_IPHONE_SIMULATOR && !TARGET_OS_TV
const char* primaryInterface = "en0"; // WiFi interface on iOS
#endif
#else
const char* primaryInterface = NULL;
SCDynamicStoreRef store = SCDynamicStoreCreate(kCFAllocatorDefault, CFSTR("WGCDWebServer"), 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 || TARGET_OS_TV
// Assume en0 is Ethernet and en1 is WiFi since there is no way to use SystemConfiguration framework in iOS Simulator
// Assumption holds for Apple TV running tvOS
if (strcmp(ifap->ifa_name, "en0") && strcmp(ifap->ifa_name, "en1"))
#else
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 = WGCDWebServerStringFromSockAddr(ifap->ifa_addr, NO);
break;
}
}
freeifaddrs(list);
}
return address;
}
NSString* WGCDWebServerComputeMD5Digest(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-2015, 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, WGCDWebServerInformationalHTTPStatusCode) {
kWGCDWebServerHTTPStatusCode_Continue = 100,
kWGCDWebServerHTTPStatusCode_SwitchingProtocols = 101,
kWGCDWebServerHTTPStatusCode_Processing = 102
};
/**
* Convenience constants for "successful" HTTP status codes.
*/
typedef NS_ENUM(NSInteger, WGCDWebServerSuccessfulHTTPStatusCode) {
kWGCDWebServerHTTPStatusCode_OK = 200,
kWGCDWebServerHTTPStatusCode_Created = 201,
kWGCDWebServerHTTPStatusCode_Accepted = 202,
kWGCDWebServerHTTPStatusCode_NonAuthoritativeInformation = 203,
kWGCDWebServerHTTPStatusCode_NoContent = 204,
kWGCDWebServerHTTPStatusCode_ResetContent = 205,
kWGCDWebServerHTTPStatusCode_PartialContent = 206,
kWGCDWebServerHTTPStatusCode_MultiStatus = 207,
kWGCDWebServerHTTPStatusCode_AlreadyReported = 208
};
/**
* Convenience constants for "redirection" HTTP status codes.
*/
typedef NS_ENUM(NSInteger, WGCDWebServerRedirectionHTTPStatusCode) {
kWGCDWebServerHTTPStatusCode_MultipleChoices = 300,
kWGCDWebServerHTTPStatusCode_MovedPermanently = 301,
kWGCDWebServerHTTPStatusCode_Found = 302,
kWGCDWebServerHTTPStatusCode_SeeOther = 303,
kWGCDWebServerHTTPStatusCode_NotModified = 304,
kWGCDWebServerHTTPStatusCode_UseProxy = 305,
kWGCDWebServerHTTPStatusCode_TemporaryRedirect = 307,
kWGCDWebServerHTTPStatusCode_PermanentRedirect = 308
};
/**
* Convenience constants for "client error" HTTP status codes.
*/
typedef NS_ENUM(NSInteger, WGCDWebServerClientErrorHTTPStatusCode) {
kWGCDWebServerHTTPStatusCode_BadRequest = 400,
kWGCDWebServerHTTPStatusCode_Unauthorized = 401,
kWGCDWebServerHTTPStatusCode_PaymentRequired = 402,
kWGCDWebServerHTTPStatusCode_Forbidden = 403,
kWGCDWebServerHTTPStatusCode_NotFound = 404,
kWGCDWebServerHTTPStatusCode_MethodNotAllowed = 405,
kWGCDWebServerHTTPStatusCode_NotAcceptable = 406,
kWGCDWebServerHTTPStatusCode_ProxyAuthenticationRequired = 407,
kWGCDWebServerHTTPStatusCode_RequestTimeout = 408,
kWGCDWebServerHTTPStatusCode_Conflict = 409,
kWGCDWebServerHTTPStatusCode_Gone = 410,
kWGCDWebServerHTTPStatusCode_LengthRequired = 411,
kWGCDWebServerHTTPStatusCode_PreconditionFailed = 412,
kWGCDWebServerHTTPStatusCode_RequestEntityTooLarge = 413,
kWGCDWebServerHTTPStatusCode_RequestURITooLong = 414,
kWGCDWebServerHTTPStatusCode_UnsupportedMediaType = 415,
kWGCDWebServerHTTPStatusCode_RequestedRangeNotSatisfiable = 416,
kWGCDWebServerHTTPStatusCode_ExpectationFailed = 417,
kWGCDWebServerHTTPStatusCode_UnprocessableEntity = 422,
kWGCDWebServerHTTPStatusCode_Locked = 423,
kWGCDWebServerHTTPStatusCode_FailedDependency = 424,
kWGCDWebServerHTTPStatusCode_UpgradeRequired = 426,
kWGCDWebServerHTTPStatusCode_PreconditionRequired = 428,
kWGCDWebServerHTTPStatusCode_TooManyRequests = 429,
kWGCDWebServerHTTPStatusCode_RequestHeaderFieldsTooLarge = 431
};
/**
* Convenience constants for "server error" HTTP status codes.
*/
typedef NS_ENUM(NSInteger, WGCDWebServerServerErrorHTTPStatusCode) {
kWGCDWebServerHTTPStatusCode_InternalServerError = 500,
kWGCDWebServerHTTPStatusCode_NotImplemented = 501,
kWGCDWebServerHTTPStatusCode_BadGateway = 502,
kWGCDWebServerHTTPStatusCode_ServiceUnavailable = 503,
kWGCDWebServerHTTPStatusCode_GatewayTimeout = 504,
kWGCDWebServerHTTPStatusCode_HTTPVersionNotSupported = 505,
kWGCDWebServerHTTPStatusCode_InsufficientStorage = 507,
kWGCDWebServerHTTPStatusCode_LoopDetected = 508,
kWGCDWebServerHTTPStatusCode_NotExtended = 510,
kWGCDWebServerHTTPStatusCode_NetworkAuthenticationRequired = 511
};

View File

@ -0,0 +1,226 @@
/*
Copyright (c) 2012-2015, 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 WGCDWebServer headers.
*/
#import "WGCDWebServerHTTPStatusCodes.h"
#import "WGCDWebServerFunctions.h"
#import "WGCDWebServer.h"
#import "WGCDWebServerConnection.h"
#import "WGCDWebServerDataRequest.h"
#import "WGCDWebServerFileRequest.h"
#import "WGCDWebServerMultiPartFormRequest.h"
#import "WGCDWebServerURLEncodedFormRequest.h"
#import "WGCDWebServerDataResponse.h"
#import "WGCDWebServerErrorResponse.h"
#import "WGCDWebServerFileResponse.h"
#import "WGCDWebServerStreamedResponse.h"
/**
* Check if a custom logging facility should be used instead.
*/
#if defined(__WGCDWEBSERVER_LOGGING_HEADER__)
#define __WGCDWEBSERVER_LOGGING_FACILITY_CUSTOM__
#import __WGCDWEBSERVER_LOGGING_HEADER__
/**
* Automatically detect if XLFacility is available and if so use it as a
* logging facility.
*/
#elif defined(__has_include) && __has_include("XLFacilityMacros.h")
#define __WGCDWEBSERVER_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_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("CocoaLumberjack/CocoaLumberjack.h")
#import <CocoaLumberjack/CocoaLumberjack.h>
#define __WGCDWEBSERVER_LOGGING_FACILITY_COCOALUMBERJACK__
#undef LOG_LEVEL_DEF
#define LOG_LEVEL_DEF WGCDWebServerLogLevel
extern DDLogLevel WGCDWebServerLogLevel;
#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__)
/**
* If all of the above fail, then use WGCDWebServer built-in
* logging facility.
*/
#else
#define __WGCDWEBSERVER_LOGGING_FACILITY_BUILTIN__
typedef NS_ENUM(int, WGCDWebServerLoggingLevel) {
kWGCDWebServerLoggingLevel_Debug = 0,
kWGCDWebServerLoggingLevel_Verbose,
kWGCDWebServerLoggingLevel_Info,
kWGCDWebServerLoggingLevel_Warning,
kWGCDWebServerLoggingLevel_Error
};
extern WGCDWebServerLoggingLevel WGCDWebServerLogLevel;
extern void WGCDWebServerLogMessage(WGCDWebServerLoggingLevel level, NSString* format, ...) NS_FORMAT_FUNCTION(2, 3);
#if DEBUG
#define GWS_LOG_DEBUG(...) do { if (WGCDWebServerLogLevel <= kWGCDWebServerLoggingLevel_Debug) WGCDWebServerLogMessage(kWGCDWebServerLoggingLevel_Debug, __VA_ARGS__); } while (0)
#else
#define GWS_LOG_DEBUG(...)
#endif
#define GWS_LOG_VERBOSE(...) do { if (WGCDWebServerLogLevel <= kWGCDWebServerLoggingLevel_Verbose) WGCDWebServerLogMessage(kWGCDWebServerLoggingLevel_Verbose, __VA_ARGS__); } while (0)
#define GWS_LOG_INFO(...) do { if (WGCDWebServerLogLevel <= kWGCDWebServerLoggingLevel_Info) WGCDWebServerLogMessage(kWGCDWebServerLoggingLevel_Info, __VA_ARGS__); } while (0)
#define GWS_LOG_WARNING(...) do { if (WGCDWebServerLogLevel <= kWGCDWebServerLoggingLevel_Warning) WGCDWebServerLogMessage(kWGCDWebServerLoggingLevel_Warning, __VA_ARGS__); } while (0)
#define GWS_LOG_ERROR(...) do { if (WGCDWebServerLogLevel <= kWGCDWebServerLoggingLevel_Error) WGCDWebServerLogMessage(kWGCDWebServerLoggingLevel_Error, __VA_ARGS__); } while (0)
#endif
/**
* Consistency check macros used when building Debug only.
*/
#if !defined(GWS_DCHECK) || !defined(GWS_DNOT_REACHED)
#if DEBUG
#define GWS_DCHECK(__CONDITION__) \
do { \
if (!(__CONDITION__)) { \
abort(); \
} \
} while (0)
#define GWS_DNOT_REACHED() abort()
#else
#define GWS_DCHECK(__CONDITION__)
#define GWS_DNOT_REACHED()
#endif
#endif
/**
* WGCDWebServer internal constants and APIs.
*/
#define kWGCDWebServerDefaultMimeType @"application/octet-stream"
#define kWGCDWebServerErrorDomain @"WGCDWebServerErrorDomain"
static inline BOOL WGCDWebServerIsValidByteRange(NSRange range) {
return ((range.location != NSUIntegerMax) || (range.length > 0));
}
static inline NSError* WGCDWebServerMakePosixError(int code) {
return [NSError errorWithDomain:NSPOSIXErrorDomain code:code userInfo:@{NSLocalizedDescriptionKey: [NSString stringWithUTF8String:strerror(code)]}];
}
extern void WGCDWebServerInitializeFunctions();
extern NSString* WGCDWebServerNormalizeHeaderValue(NSString* value);
extern NSString* WGCDWebServerTruncateHeaderValue(NSString* value);
extern NSString* WGCDWebServerExtractHeaderValueParameter(NSString* header, NSString* attribute);
extern NSStringEncoding WGCDWebServerStringEncodingFromCharset(NSString* charset);
extern BOOL WGCDWebServerIsTextContentType(NSString* type);
extern NSString* WGCDWebServerDescribeData(NSData* data, NSString* contentType);
extern NSString* WGCDWebServerComputeMD5Digest(NSString* format, ...) NS_FORMAT_FUNCTION(1,2);
extern NSString* WGCDWebServerStringFromSockAddr(const struct sockaddr* addr, BOOL includeService);
@interface WGCDWebServerConnection ()
- (id)initWithServer:(WGCDWebServer*)server localAddress:(NSData*)localAddress remoteAddress:(NSData*)remoteAddress socket:(CFSocketNativeHandle)socket;
@end
@interface WGCDWebServer ()
@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;
@property(nonatomic, readonly) dispatch_queue_priority_t dispatchQueuePriority;
- (void)willStartConnection:(WGCDWebServerConnection*)connection;
- (void)didEndConnection:(WGCDWebServerConnection*)connection;
@end
@interface WGCDWebServerHandler : NSObject
@property(nonatomic, readonly) WGCDWebServerMatchBlock matchBlock;
@property(nonatomic, readonly) WGCDWebServerAsyncProcessBlock asyncProcessBlock;
@end
@interface WGCDWebServerRequest ()
@property(nonatomic, readonly) BOOL usesChunkedTransferEncoding;
@property(nonatomic, readwrite) NSData* localAddressData;
@property(nonatomic, readwrite) NSData* remoteAddressData;
- (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 WGCDWebServerResponse ()
@property(nonatomic, readonly) NSDictionary* additionalHeaders;
@property(nonatomic, readonly) BOOL usesChunkedTransferEncoding;
- (void)prepareForReading;
- (BOOL)performOpen:(NSError**)error;
- (void)performReadDataWithCompletion:(WGCDWebServerBodyReaderCompletionBlock)block;
- (void)performClose;
@end

View File

@ -0,0 +1,206 @@
/*
Copyright (c) 2012-2015, 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 WGCDWebServerRequest
* 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 WGCDWebServerRequestAttribute_RegexCaptures;
/**
* This protocol is used by the WGCDWebServerConnection to communicate with
* the WGCDWebServerRequest and write the received HTTP body data.
*
* Note that multiple WGCDWebServerBodyWriter objects can be chained together
* internally e.g. to automatically decode gzip encoded content before
* passing it on to the WGCDWebServerRequest.
*
* @warning These methods can be called on any WGCD thread.
*/
@protocol WGCDWebServerBodyWriter <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 WGCDWebServerRequest class is instantiated by the WGCDWebServerConnection
* after the HTTP headers have been received. Each instance wraps a single HTTP
* request. If a body is present, the methods from the WGCDWebServerBodyWriter
* protocol will be called by the WGCDWebServerConnection to receive it.
*
* The default implementation of the WGCDWebServerBodyWriter protocol on the class
* simply ignores the body data.
*
* @warning WGCDWebServerRequest instances can be created and used on any WGCD thread.
*/
@interface WGCDWebServerRequest : NSObject <WGCDWebServerBodyWriter>
/**
* 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;
/**
* Returns the address of the local peer (i.e. server) for the request
* as a raw "struct sockaddr".
*/
@property(nonatomic, readonly) NSData* localAddressData;
/**
* Returns the address of the local peer (i.e. server) for the request
* as a string.
*/
@property(nonatomic, readonly) NSString* localAddressString;
/**
* Returns the address of the remote peer (i.e. client) for the request
* as a raw "struct sockaddr".
*/
@property(nonatomic, readonly) NSData* remoteAddressData;
/**
* Returns the address of the remote peer (i.e. client) for the request
* as a string.
*/
@property(nonatomic, readonly) NSString* remoteAddressString;
/**
* This method is the designated initializer for the class.
*/
- (instancetype)initWithMethod:(NSString*)method url:(NSURL*)url headers:(NSDictionary*)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,334 @@
/*
Copyright (c) 2012-2015, 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 WGCDWebServer requires ARC
#endif
#import <zlib.h>
#import "WGCDWebServerPrivate.h"
NSString* const WGCDWebServerRequestAttribute_RegexCaptures = @"WGCDWebServerRequestAttribute_RegexCaptures";
#define kZlibErrorDomain @"ZlibErrorDomain"
#define kGZipInitialBufferSize (256 * 1024)
@interface WGCDWebServerBodyDecoder : NSObject <WGCDWebServerBodyWriter>
- (id)initWithRequest:(WGCDWebServerRequest*)request writer:(id<WGCDWebServerBodyWriter>)writer;
@end
@interface WGCDWebServerGZipDecoder : WGCDWebServerBodyDecoder
@end
@interface WGCDWebServerBodyDecoder () {
@private
WGCDWebServerRequest* __unsafe_unretained _request;
id<WGCDWebServerBodyWriter> __unsafe_unretained _writer;
}
@end
@implementation WGCDWebServerBodyDecoder
- (id)initWithRequest:(WGCDWebServerRequest*)request writer:(id<WGCDWebServerBodyWriter>)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 WGCDWebServerGZipDecoder () {
@private
z_stream _stream;
BOOL _finished;
}
@end
@implementation WGCDWebServerGZipDecoder
- (BOOL)open:(NSError**)error {
int result = inflateInit2(&_stream, 15 + 16);
if (result != Z_OK) {
if (error) {
*error = [NSError errorWithDomain:kZlibErrorDomain code:result userInfo:nil];
}
return NO;
}
if (![super open:error]) {
inflateEnd(&_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)) {
if (error) {
*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 WGCDWebServerRequest () {
@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;
NSData* _localAddress;
NSData* _remoteAddress;
BOOL _opened;
NSMutableArray* _decoders;
NSMutableDictionary* _attributes;
id<WGCDWebServerBodyWriter> __unsafe_unretained _writer;
}
@end
@implementation WGCDWebServerRequest : 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, localAddressData=_localAddress, remoteAddressData=_remoteAddress;
- (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 = WGCDWebServerNormalizeHeaderValue([_headers objectForKey:@"Content-Type"]);
_chunked = [WGCDWebServerNormalizeHeaderValue([_headers objectForKey:@"Transfer-Encoding"]) isEqualToString:@"chunked"];
NSString* lengthHeader = [_headers objectForKey:@"Content-Length"];
if (lengthHeader) {
NSInteger length = [lengthHeader integerValue];
if (_chunked || (length < 0)) {
GWS_LOG_WARNING(@"Invalid 'Content-Length' header '%@' for '%@' request on \"%@\"", lengthHeader, _method, _url);
GWS_DNOT_REACHED();
return nil;
}
_length = length;
if (_type == nil) {
_type = kWGCDWebServerDefaultMimeType;
}
} else if (_chunked) {
if (_type == nil) {
_type = kWGCDWebServerDefaultMimeType;
}
_length = NSUIntegerMax;
} else {
if (_type) {
GWS_LOG_WARNING(@"Ignoring 'Content-Type' header for '%@' request on \"%@\"", _method, _url);
_type = nil; // Content-Type without Content-Length or chunked-encoding doesn't make sense
}
_length = NSUIntegerMax;
}
NSString* modifiedHeader = [_headers objectForKey:@"If-Modified-Since"];
if (modifiedHeader) {
_modifiedSince = [WGCDWebServerParseRFC822(modifiedHeader) copy];
}
_noneMatch = [_headers objectForKey:@"If-None-Match"];
_range = NSMakeRange(NSUIntegerMax, 0);
NSString* rangeHeader = WGCDWebServerNormalizeHeaderValue([_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 WGCDWebServerIsValidByteRange(_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 ([WGCDWebServerNormalizeHeaderValue([self.headers objectForKey:@"Content-Encoding"]) isEqualToString:@"gzip"]) {
WGCDWebServerGZipDecoder* decoder = [[WGCDWebServerGZipDecoder 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*)localAddressString {
return WGCDWebServerStringFromSockAddr(_localAddress.bytes, YES);
}
- (NSString*)remoteAddressString {
return WGCDWebServerStringFromSockAddr(_remoteAddress.bytes, YES);
}
- (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-2015, 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 WGCDWebServerBodyReaderCompletionBlock is passed by WGCDWebServer to the
* WGCDWebServerBodyReader object when reading data from it asynchronously.
*/
typedef void (^WGCDWebServerBodyReaderCompletionBlock)(NSData* data, NSError* error);
/**
* This protocol is used by the WGCDWebServerConnection to communicate with
* the WGCDWebServerResponse and read the HTTP body data to send.
*
* Note that multiple WGCDWebServerBodyReader objects can be chained together
* internally e.g. to automatically apply gzip encoding to the content before
* passing it on to the WGCDWebServerResponse.
*
* @warning These methods can be called on any WGCD thread.
*/
@protocol WGCDWebServerBodyReader <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:(WGCDWebServerBodyReaderCompletionBlock)block;
@end
/**
* The WGCDWebServerResponse class is used to wrap a single HTTP response.
* It is instantiated by the handler of the WGCDWebServer that handled the request.
* If a body is present, the methods from the WGCDWebServerBodyReader protocol
* will be called by the WGCDWebServerConnection to send it.
*
* The default implementation of the WGCDWebServerBodyReader protocol
* on the class simply returns an empty body.
*
* @warning WGCDWebServerResponse instances can be created and used on any WGCD thread.
*/
@interface WGCDWebServerResponse : NSObject <WGCDWebServerBodyReader>
/**
* 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 WGCDWebServerConnection 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 WGCDWebServerResponse 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 WGCDWebServerResponse (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,310 @@
/*
Copyright (c) 2012-2015, 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 WGCDWebServer requires ARC
#endif
#import <zlib.h>
#import "WGCDWebServerPrivate.h"
#define kZlibErrorDomain @"ZlibErrorDomain"
#define kGZipInitialBufferSize (256 * 1024)
@interface WGCDWebServerBodyEncoder : NSObject <WGCDWebServerBodyReader>
- (id)initWithResponse:(WGCDWebServerResponse*)response reader:(id<WGCDWebServerBodyReader>)reader;
@end
@interface WGCDWebServerGZipEncoder : WGCDWebServerBodyEncoder
@end
@interface WGCDWebServerBodyEncoder () {
@private
WGCDWebServerResponse* __unsafe_unretained _response;
id<WGCDWebServerBodyReader> __unsafe_unretained _reader;
}
@end
@implementation WGCDWebServerBodyEncoder
- (id)initWithResponse:(WGCDWebServerResponse*)response reader:(id<WGCDWebServerBodyReader>)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 WGCDWebServerGZipEncoder () {
@private
z_stream _stream;
BOOL _finished;
}
@end
@implementation WGCDWebServerGZipEncoder
- (id)initWithResponse:(WGCDWebServerResponse*)response reader:(id<WGCDWebServerBodyReader>)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) {
if (error) {
*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) {
if (error) {
*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 WGCDWebServerResponse () {
@private
NSString* _type;
NSUInteger _length;
NSInteger _status;
NSUInteger _maxAge;
NSDate* _lastModified;
NSString* _eTag;
NSMutableDictionary* _headers;
BOOL _chunked;
BOOL _gzipped;
BOOL _opened;
NSMutableArray* _encoders;
id<WGCDWebServerBodyReader> __unsafe_unretained _reader;
}
@end
@implementation WGCDWebServerResponse
@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 = kWGCDWebServerHTTPStatusCode_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) {
WGCDWebServerGZipEncoder* encoder = [[WGCDWebServerGZipEncoder 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:(WGCDWebServerBodyReaderCompletionBlock)block {
GWS_DCHECK(_opened);
if ([_reader respondsToSelector:@selector(asyncReadDataWithCompletion:)]) {
[_reader asyncReadDataWithCompletion:[block copy]];
} 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 WGCDWebServerResponse (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 ? kWGCDWebServerHTTPStatusCode_MovedPermanently : kWGCDWebServerHTTPStatusCode_TemporaryRedirect;
[self setValue:[location absoluteString] forAdditionalHeader:@"Location"];
}
return self;
}
@end

View File

@ -0,0 +1,60 @@
/*
Copyright (c) 2012-2015, 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 "WGCDWebServerRequest.h"
/**
* The WGCDWebServerDataRequest subclass of WGCDWebServerRequest stores the body
* of the HTTP request in memory.
*/
@interface WGCDWebServerDataRequest : WGCDWebServerRequest
/**
* Returns the data for the request body.
*/
@property(nonatomic, readonly) NSData* data;
@end
@interface WGCDWebServerDataRequest (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,108 @@
/*
Copyright (c) 2012-2015, 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 WGCDWebServer requires ARC
#endif
#import "WGCDWebServerPrivate.h"
@interface WGCDWebServerDataRequest () {
@private
NSMutableData* _data;
NSString* _text;
id _jsonObject;
}
@end
@implementation WGCDWebServerDataRequest
@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) {
if (error) {
*error = [NSError errorWithDomain:kWGCDWebServerErrorDomain 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:WGCDWebServerDescribeData(_data, self.contentType)];
}
return description;
}
@end
@implementation WGCDWebServerDataRequest (Extensions)
- (NSString*)text {
if (_text == nil) {
if ([self.contentType hasPrefix:@"text/"]) {
NSString* charset = WGCDWebServerExtractHeaderValueParameter(self.contentType, @"charset");
_text = [[NSString alloc] initWithData:self.data encoding:WGCDWebServerStringEncodingFromCharset(charset)];
} else {
GWS_DNOT_REACHED();
}
}
return _text;
}
- (id)jsonObject {
if (_jsonObject == nil) {
NSString* mimeType = WGCDWebServerTruncateHeaderValue(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-2015, 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 "WGCDWebServerRequest.h"
/**
* The WGCDWebServerFileRequest subclass of WGCDWebServerRequest stores the body
* of the HTTP request to a file on disk.
*/
@interface WGCDWebServerFileRequest : WGCDWebServerRequest
/**
* Returns the path to the temporary file containing the request body.
*
* @warning This temporary file will be automatically deleted when the
* WGCDWebServerFileRequest 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,109 @@
/*
Copyright (c) 2012-2015, 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 WGCDWebServer requires ARC
#endif
#import "WGCDWebServerPrivate.h"
@interface WGCDWebServerFileRequest () {
@private
NSString* _temporaryPath;
int _file;
}
@end
@implementation WGCDWebServerFileRequest
@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) {
if (error) {
*error = WGCDWebServerMakePosixError(errno);
}
return NO;
}
return YES;
}
- (BOOL)writeData:(NSData*)data error:(NSError**)error {
if (write(_file, data.bytes, data.length) != (ssize_t)data.length) {
if (error) {
*error = WGCDWebServerMakePosixError(errno);
}
return NO;
}
return YES;
}
- (BOOL)close:(NSError**)error {
if (close(_file) < 0) {
if (error) {
*error = WGCDWebServerMakePosixError(errno);
}
return NO;
}
#ifdef __WGCDWEBSERVER_ENABLE_TESTING__
NSString* creationDateHeader = [self.headers objectForKey:@"X-WGCDWebServer-CreationDate"];
if (creationDateHeader) {
NSDate* date = WGCDWebServerParseISO8601(creationDateHeader);
if (!date || ![[NSFileManager defaultManager] setAttributes:@{NSFileCreationDate: date} ofItemAtPath:_temporaryPath error:error]) {
return NO;
}
}
NSString* modifiedDateHeader = [self.headers objectForKey:@"X-WGCDWebServer-ModifiedDate"];
if (modifiedDateHeader) {
NSDate* date = WGCDWebServerParseRFC822(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-2015, 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 "WGCDWebServerRequest.h"
/**
* The WGCDWebServerMultiPart class is an abstract class that wraps the content
* of a part.
*/
@interface WGCDWebServerMultiPart : 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 WGCDWebServerMultiPartArgument subclass of WGCDWebServerMultiPart wraps
* the content of a part as data in memory.
*/
@interface WGCDWebServerMultiPartArgument : WGCDWebServerMultiPart
/**
* 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 WGCDWebServerMultiPartFile subclass of WGCDWebServerMultiPart wraps
* the content of a part as a file on disk.
*/
@interface WGCDWebServerMultiPartFile : WGCDWebServerMultiPart
/**
* 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
* WGCDWebServerMultiPartFile 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 WGCDWebServerMultiPartFormRequest subclass of WGCDWebServerRequest
* parses the body of the HTTP request as a multipart encoded form.
*/
@interface WGCDWebServerMultiPartFormRequest : WGCDWebServerRequest
/**
* Returns the argument parts from the multipart encoded form as
* name / WGCDWebServerMultiPartArgument pairs.
*/
@property(nonatomic, readonly) NSArray* arguments;
/**
* Returns the files parts from the multipart encoded form as
* name / WGCDWebServerMultiPartFile 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.
*/
- (WGCDWebServerMultiPartArgument*)firstArgumentForControlName:(NSString*)name;
/**
* Returns the first file for a given control name or nil if not found.
*/
- (WGCDWebServerMultiPartFile*)firstFileForControlName:(NSString*)name;
@end

View File

@ -0,0 +1,445 @@
/*
Copyright (c) 2012-2015, 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 WGCDWebServer requires ARC
#endif
#import "WGCDWebServerPrivate.h"
#define kMultiPartBufferSize (256 * 1024)
typedef enum {
kParserState_Undefined = 0,
kParserState_Start,
kParserState_Headers,
kParserState_Content,
kParserState_End
} ParserState;
@interface WGCDWebServerMIMEStreamParser : 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 WGCDWebServerMultiPart () {
@private
NSString* _controlName;
NSString* _contentType;
NSString* _mimeType;
}
@end
@implementation WGCDWebServerMultiPart
@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 = WGCDWebServerTruncateHeaderValue(_contentType);
}
return self;
}
@end
@interface WGCDWebServerMultiPartArgument () {
@private
NSData* _data;
NSString* _string;
}
@end
@implementation WGCDWebServerMultiPartArgument
@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 = WGCDWebServerExtractHeaderValueParameter(self.contentType, @"charset");
_string = [[NSString alloc] initWithData:_data encoding:WGCDWebServerStringEncodingFromCharset(charset)];
}
}
return self;
}
- (NSString*)description {
return [NSString stringWithFormat:@"<%@ | '%@' | %lu bytes>", [self class], self.mimeType, (unsigned long)_data.length];
}
@end
@interface WGCDWebServerMultiPartFile () {
@private
NSString* _fileName;
NSString* _temporaryPath;
}
@end
@implementation WGCDWebServerMultiPartFile
@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 WGCDWebServerMIMEStreamParser () {
@private
NSData* _boundary;
NSString* _defaultcontrolName;
ParserState _state;
NSMutableData* _data;
NSMutableArray* _arguments;
NSMutableArray* _files;
NSString* _controlName;
NSString* _fileName;
NSString* _contentType;
NSString* _tmpPath;
int _tmpFile;
WGCDWebServerMIMEStreamParser* _subParser;
}
@end
@implementation WGCDWebServerMIMEStreamParser
+ (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 = WGCDWebServerNormalizeHeaderValue(value);
} else if ([name caseInsensitiveCompare:@"Content-Disposition"] == NSOrderedSame) {
NSString* contentDisposition = WGCDWebServerNormalizeHeaderValue(value);
if ([WGCDWebServerTruncateHeaderValue(contentDisposition) isEqualToString:@"form-data"]) {
_controlName = WGCDWebServerExtractHeaderValueParameter(contentDisposition, @"name");
_fileName = WGCDWebServerExtractHeaderValueParameter(contentDisposition, @"filename");
} else if ([WGCDWebServerTruncateHeaderValue(contentDisposition) isEqualToString:@"file"]) {
_controlName = _defaultcontrolName;
_fileName = WGCDWebServerExtractHeaderValueParameter(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 ([WGCDWebServerTruncateHeaderValue(_contentType) isEqualToString:@"multipart/mixed"]) {
NSString* boundary = WGCDWebServerExtractHeaderValueParameter(_contentType, @"boundary");
_subParser = [[WGCDWebServerMIMEStreamParser 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;
WGCDWebServerMultiPartFile* file = [[WGCDWebServerMultiPartFile 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];
WGCDWebServerMultiPartArgument* argument = [[WGCDWebServerMultiPartArgument 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 WGCDWebServerMultiPartFormRequest () {
@private
WGCDWebServerMIMEStreamParser* _parser;
NSMutableArray* _arguments;
NSMutableArray* _files;
}
@end
@implementation WGCDWebServerMultiPartFormRequest
@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 = WGCDWebServerExtractHeaderValueParameter(self.contentType, @"boundary");
_parser = [[WGCDWebServerMIMEStreamParser alloc] initWithBoundary:boundary defaultControlName:nil arguments:_arguments files:_files];
if (_parser == nil) {
if (error) {
*error = [NSError errorWithDomain:kWGCDWebServerErrorDomain 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]) {
if (error) {
*error = [NSError errorWithDomain:kWGCDWebServerErrorDomain 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) {
if (error) {
*error = [NSError errorWithDomain:kWGCDWebServerErrorDomain code:-1 userInfo:@{NSLocalizedDescriptionKey: @"Failed finishing to parse multipart form data"}];
}
return NO;
}
return YES;
}
- (WGCDWebServerMultiPartArgument*)firstArgumentForControlName:(NSString*)name {
for (WGCDWebServerMultiPartArgument* argument in _arguments) {
if ([argument.controlName isEqualToString:name]) {
return argument;
}
}
return nil;
}
- (WGCDWebServerMultiPartFile*)firstFileForControlName:(NSString*)name {
for (WGCDWebServerMultiPartFile* 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 (WGCDWebServerMultiPartArgument* argument in _arguments) {
[description appendFormat:@"\n%@ (%@)\n", argument.controlName, argument.contentType];
[description appendString:WGCDWebServerDescribeData(argument.data, argument.contentType)];
}
}
if (_files.count) {
[description appendString:@"\n"];
for (WGCDWebServerMultiPartFile* 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-2015, 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 "WGCDWebServerDataRequest.h"
/**
* The WGCDWebServerURLEncodedFormRequest subclass of WGCDWebServerRequest
* parses the body of the HTTP request as a URL encoded form using
* WGCDWebServerParseURLEncodedForm().
*/
@interface WGCDWebServerURLEncodedFormRequest : WGCDWebServerDataRequest
/**
* 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-2015, 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 WGCDWebServer requires ARC
#endif
#import "WGCDWebServerPrivate.h"
@interface WGCDWebServerURLEncodedFormRequest () {
@private
NSDictionary* _arguments;
}
@end
@implementation WGCDWebServerURLEncodedFormRequest
@synthesize arguments=_arguments;
+ (NSString*)mimeType {
return @"application/x-www-form-urlencoded";
}
- (BOOL)close:(NSError**)error {
if (![super close:error]) {
return NO;
}
NSString* charset = WGCDWebServerExtractHeaderValueParameter(self.contentType, @"charset");
NSString* string = [[NSString alloc] initWithData:self.data encoding:WGCDWebServerStringEncodingFromCharset(charset)];
_arguments = WGCDWebServerParseURLEncodedForm(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-2015, 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 "WGCDWebServerResponse.h"
/**
* The WGCDWebServerDataResponse subclass of WGCDWebServerResponse reads the body
* of the HTTP response from memory.
*/
@interface WGCDWebServerDataResponse : WGCDWebServerResponse
/**
* 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 WGCDWebServerDataResponse (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-2015, 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 WGCDWebServer requires ARC
#endif
#import "WGCDWebServerPrivate.h"
@interface WGCDWebServerDataResponse () {
@private
NSData* _data;
BOOL _done;
}
@end
@implementation WGCDWebServerDataResponse
+ (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:WGCDWebServerDescribeData(_data, self.contentType)];
return description;
}
@end
@implementation WGCDWebServerDataResponse (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-2015, 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 "WGCDWebServerDataResponse.h"
#import "WGCDWebServerHTTPStatusCodes.h"
/**
* The WGCDWebServerDataResponse subclass of WGCDWebServerDataResponse generates
* an HTML body from an HTTP status code and an error message.
*/
@interface WGCDWebServerErrorResponse : WGCDWebServerDataResponse
/**
* Creates a client error response with the corresponding HTTP status code.
*/
+ (instancetype)responseWithClientError:(WGCDWebServerClientErrorHTTPStatusCode)errorCode message:(NSString*)format, ... NS_FORMAT_FUNCTION(2,3);
/**
* Creates a server error response with the corresponding HTTP status code.
*/
+ (instancetype)responseWithServerError:(WGCDWebServerServerErrorHTTPStatusCode)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:(WGCDWebServerClientErrorHTTPStatusCode)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:(WGCDWebServerServerErrorHTTPStatusCode)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:(WGCDWebServerClientErrorHTTPStatusCode)errorCode message:(NSString*)format, ... NS_FORMAT_FUNCTION(2,3);
/**
* Initializes a server error response with the corresponding HTTP status code.
*/
- (instancetype)initWithServerError:(WGCDWebServerServerErrorHTTPStatusCode)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:(WGCDWebServerClientErrorHTTPStatusCode)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:(WGCDWebServerServerErrorHTTPStatusCode)errorCode underlyingError:(NSError*)underlyingError message:(NSString*)format, ... NS_FORMAT_FUNCTION(3,4);
@end

View File

@ -0,0 +1,128 @@
/*
Copyright (c) 2012-2015, 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 WGCDWebServer requires ARC
#endif
#import "WGCDWebServerPrivate.h"
@interface WGCDWebServerErrorResponse ()
- (instancetype)initWithStatusCode:(NSInteger)statusCode underlyingError:(NSError*)underlyingError messageFormat:(NSString*)format arguments:(va_list)arguments;
@end
@implementation WGCDWebServerErrorResponse
+ (instancetype)responseWithClientError:(WGCDWebServerClientErrorHTTPStatusCode)errorCode message:(NSString*)format, ... {
GWS_DCHECK(((NSInteger)errorCode >= 400) && ((NSInteger)errorCode < 500));
va_list arguments;
va_start(arguments, format);
WGCDWebServerErrorResponse* response = [[self alloc] initWithStatusCode:errorCode underlyingError:nil messageFormat:format arguments:arguments];
va_end(arguments);
return response;
}
+ (instancetype)responseWithServerError:(WGCDWebServerServerErrorHTTPStatusCode)errorCode message:(NSString*)format, ... {
GWS_DCHECK(((NSInteger)errorCode >= 500) && ((NSInteger)errorCode < 600));
va_list arguments;
va_start(arguments, format);
WGCDWebServerErrorResponse* response = [[self alloc] initWithStatusCode:errorCode underlyingError:nil messageFormat:format arguments:arguments];
va_end(arguments);
return response;
}
+ (instancetype)responseWithClientError:(WGCDWebServerClientErrorHTTPStatusCode)errorCode underlyingError:(NSError*)underlyingError message:(NSString*)format, ... {
GWS_DCHECK(((NSInteger)errorCode >= 400) && ((NSInteger)errorCode < 500));
va_list arguments;
va_start(arguments, format);
WGCDWebServerErrorResponse* response = [[self alloc] initWithStatusCode:errorCode underlyingError:underlyingError messageFormat:format arguments:arguments];
va_end(arguments);
return response;
}
+ (instancetype)responseWithServerError:(WGCDWebServerServerErrorHTTPStatusCode)errorCode underlyingError:(NSError*)underlyingError message:(NSString*)format, ... {
GWS_DCHECK(((NSInteger)errorCode >= 500) && ((NSInteger)errorCode < 600));
va_list arguments;
va_start(arguments, format);
WGCDWebServerErrorResponse* 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:(WGCDWebServerClientErrorHTTPStatusCode)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:(WGCDWebServerServerErrorHTTPStatusCode)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:(WGCDWebServerClientErrorHTTPStatusCode)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:(WGCDWebServerServerErrorHTTPStatusCode)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-2015, 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 "WGCDWebServerResponse.h"
/**
* The WGCDWebServerFileResponse subclass of WGCDWebServerResponse reads the body
* of the HTTP response from a file on disk.
*
* It will automatically set the contentType, lastModifiedDate and eTag
* properties of the WGCDWebServerResponse according to the file extension and
* metadata.
*/
@interface WGCDWebServerFileResponse : WGCDWebServerResponse
/**
* 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 WGCDWebServerRequest.
*/
- (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,187 @@
/*
Copyright (c) 2012-2015, 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 WGCDWebServer requires ARC
#endif
#import <sys/stat.h>
#import "WGCDWebServerPrivate.h"
#define kFileReadBufferSize (32 * 1024)
@interface WGCDWebServerFileResponse () {
@private
NSString* _path;
NSUInteger _offset;
NSUInteger _size;
int _file;
}
@end
@implementation WGCDWebServerFileResponse
+ (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 = WGCDWebServerIsValidByteRange(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:kWGCDWebServerHTTPStatusCode_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, WGCDWebServerEscapeURLString(fileName)];
[self setValue:value forAdditionalHeader:@"Content-Disposition"];
} else {
GWS_DNOT_REACHED();
}
}
self.contentType = WGCDWebServerGetMimeTypeForExtension([_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) {
if (error) {
*error = WGCDWebServerMakePosixError(errno);
}
return NO;
}
if (lseek(_file, _offset, SEEK_SET) != (off_t)_offset) {
if (error) {
*error = WGCDWebServerMakePosixError(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) {
if (error) {
*error = WGCDWebServerMakePosixError(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,75 @@
/*
Copyright (c) 2012-2015, 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 "WGCDWebServerResponse.h"
/**
* The WGCDWebServerStreamBlock is called to stream the data for the HTTP body.
* The block must return either a chunk of data, an empty NSData when done, or
* nil on error and set the "error" argument which is guaranteed to be non-NULL.
*/
typedef NSData* (^WGCDWebServerStreamBlock)(NSError** error);
/**
* The WGCDWebServerAsyncStreamBlock works like the WGCDWebServerStreamBlock
* except the streamed data can be returned at a later time allowing for
* truly asynchronous generation of the data.
*
* The block must call "completionBlock" passing the new chunk of data when ready,
* an empty NSData when done, or nil on error and pass a NSError.
*
* The block cannot call "completionBlock" more than once per invocation.
*/
typedef void (^WGCDWebServerAsyncStreamBlock)(WGCDWebServerBodyReaderCompletionBlock completionBlock);
/**
* The WGCDWebServerStreamedResponse subclass of WGCDWebServerResponse streams
* the body of the HTTP response using a WGCD block.
*/
@interface WGCDWebServerStreamedResponse : WGCDWebServerResponse
/**
* Creates a response with streamed data and a given content type.
*/
+ (instancetype)responseWithContentType:(NSString*)type streamBlock:(WGCDWebServerStreamBlock)block;
/**
* Creates a response with async streamed data and a given content type.
*/
+ (instancetype)responseWithContentType:(NSString*)type asyncStreamBlock:(WGCDWebServerAsyncStreamBlock)block;
/**
* Initializes a response with streamed data and a given content type.
*/
- (instancetype)initWithContentType:(NSString*)type streamBlock:(WGCDWebServerStreamBlock)block;
/**
* This method is the designated initializer for the class.
*/
- (instancetype)initWithContentType:(NSString*)type asyncStreamBlock:(WGCDWebServerAsyncStreamBlock)block;
@end

View File

@ -0,0 +1,79 @@
/*
Copyright (c) 2012-2015, 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 WGCDWebServer requires ARC
#endif
#import "WGCDWebServerPrivate.h"
@interface WGCDWebServerStreamedResponse () {
@private
WGCDWebServerAsyncStreamBlock _block;
}
@end
@implementation WGCDWebServerStreamedResponse
+ (instancetype)responseWithContentType:(NSString*)type streamBlock:(WGCDWebServerStreamBlock)block {
return [[[self class] alloc] initWithContentType:type streamBlock:block];
}
+ (instancetype)responseWithContentType:(NSString*)type asyncStreamBlock:(WGCDWebServerAsyncStreamBlock)block {
return [[[self class] alloc] initWithContentType:type asyncStreamBlock:block];
}
- (instancetype)initWithContentType:(NSString*)type streamBlock:(WGCDWebServerStreamBlock)block {
return [self initWithContentType:type asyncStreamBlock:^(WGCDWebServerBodyReaderCompletionBlock completionBlock) {
NSError* error = nil;
NSData* data = block(&error);
completionBlock(data, error);
}];
}
- (instancetype)initWithContentType:(NSString*)type asyncStreamBlock:(WGCDWebServerAsyncStreamBlock)block {
if ((self = [super init])) {
_block = [block copy];
self.contentType = type;
}
return self;
}
- (void)asyncReadDataWithCompletion:(WGCDWebServerBodyReaderCompletionBlock)block {
_block(block);
}
- (NSString*)description {
NSMutableString* description = [NSMutableString stringWithString:[super description]];
[description appendString:@"\n\n<STREAM>"];
return description;
}
@end