iOS module, fixes
This commit is contained in:
parent
5161252b4f
commit
9bbbd0fe6a
21
LICENSE
21
LICENSE
|
@ -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
130
README.md
|
@ -1,15 +1,13 @@
|
||||||
# react-native-http-server
|
# react-native-http-bridge
|
||||||
|
|
||||||
HTTP Server for [React Native](https://github.com/facebook/react-native)
|
HTTP Server for [React Native](https://github.com/facebook/react-native)
|
||||||
|
|
||||||
Android only for now.
|
Supports only POST-requests and one-way communication. Created for [Status.im](https://github.com/status-im)
|
||||||
|
|
||||||
Built on top of the NanoHttpd library: https://github.com/NanoHttpd/nanohttpd
|
|
||||||
|
|
||||||
## Install
|
## Install
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
npm install --save react-native-http-server
|
npm install --save react-native-http-bridge
|
||||||
```
|
```
|
||||||
|
|
||||||
## Automatically link
|
## Automatically link
|
||||||
|
@ -17,137 +15,31 @@ npm install --save react-native-http-server
|
||||||
#### With React Native 0.27+
|
#### With React Native 0.27+
|
||||||
|
|
||||||
```shell
|
```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
|
## Example
|
||||||
|
|
||||||
First import/require react-native-http-server:
|
First import/require react-native-http-server:
|
||||||
|
|
||||||
```js
|
```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
|
```js
|
||||||
|
|
||||||
componentWillMount(){
|
componentWillMount(){
|
||||||
|
|
||||||
var options = {
|
// initalize the server (now accessible via localhost:1234)
|
||||||
port: 1234, // note that 80 is reserved on Android - an exception will be thrown
|
httpBridge.start(5561, function(request) {
|
||||||
};
|
|
||||||
|
|
||||||
// initalise the server (now accessible via localhost:1234)
|
// request.url
|
||||||
httpServer.create(options, function(request, send){
|
// request.postData
|
||||||
|
|
||||||
// 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);
|
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -160,7 +52,7 @@ Finally, ensure that you disable the server when your component is being unmount
|
||||||
```js
|
```js
|
||||||
|
|
||||||
componentWillUnmount() {
|
componentWillUnmount() {
|
||||||
httpServer.stop();
|
httpBridge.stop();
|
||||||
}
|
}
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
|
@ -4,30 +4,18 @@ import com.facebook.react.bridge.ReactApplicationContext;
|
||||||
import com.facebook.react.bridge.ReactContextBaseJavaModule;
|
import com.facebook.react.bridge.ReactContextBaseJavaModule;
|
||||||
import com.facebook.react.bridge.LifecycleEventListener;
|
import com.facebook.react.bridge.LifecycleEventListener;
|
||||||
import com.facebook.react.bridge.ReactMethod;
|
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.io.IOException;
|
||||||
import java.util.HashMap;
|
|
||||||
|
|
||||||
import android.support.annotation.Nullable;
|
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
|
|
||||||
public class HttpServerModule extends ReactContextBaseJavaModule implements LifecycleEventListener {
|
public class HttpServerModule extends ReactContextBaseJavaModule implements LifecycleEventListener {
|
||||||
ReactApplicationContext reactContext;
|
ReactApplicationContext reactContext;
|
||||||
|
|
||||||
private static final String TAG = "HttpServer";
|
private static final String MODULE_NAME = "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 int DEFAULT_PORT = 5561;
|
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 port;
|
||||||
private int timeout;
|
|
||||||
private Server server = null;
|
private Server server = null;
|
||||||
|
|
||||||
public HttpServerModule(ReactApplicationContext reactContext) {
|
public HttpServerModule(ReactApplicationContext reactContext) {
|
||||||
|
@ -35,88 +23,46 @@ public class HttpServerModule extends ReactContextBaseJavaModule implements Life
|
||||||
this.reactContext = reactContext;
|
this.reactContext = reactContext;
|
||||||
|
|
||||||
port = DEFAULT_PORT;
|
port = DEFAULT_PORT;
|
||||||
timeout = DEFAULT_TIMEOUT;
|
|
||||||
|
|
||||||
reactContext.addLifecycleEventListener(this);
|
reactContext.addLifecycleEventListener(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String getName() {
|
public String getName() {
|
||||||
return "HttpServer";
|
return MODULE_NAME;
|
||||||
}
|
|
||||||
|
|
||||||
@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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@ReactMethod
|
@ReactMethod
|
||||||
public void init(ReadableMap options, Callback success, @Nullable Callback failure) {
|
public void start(int port) {
|
||||||
Log.d(TAG, "Initializing server...");
|
Log.d(MODULE_NAME, "Initializing server...");
|
||||||
|
this.port = port;
|
||||||
|
|
||||||
if (options.hasKey("port")) {
|
start();
|
||||||
port = options.getInt("port");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (options.hasKey("timeout")) {
|
|
||||||
timeout = options.getInt("timeout");
|
|
||||||
}
|
|
||||||
|
|
||||||
start(success, failure);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@ReactMethod
|
private void start() {
|
||||||
public void start(@Nullable Callback success, @Nullable Callback failure) {
|
if (port == 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
server = new Server(reactContext, port);
|
||||||
try {
|
try {
|
||||||
server = new Server(reactContext, port, timeout);
|
|
||||||
server.start();
|
server.start();
|
||||||
|
} catch (IOException e) {
|
||||||
if (success != null) {
|
Log.e(MODULE_NAME, e.getMessage());
|
||||||
success.invoke();
|
|
||||||
}
|
|
||||||
} catch (Exception e) {
|
|
||||||
Log.e(TAG, e.getMessage());
|
|
||||||
|
|
||||||
if (failure != null) {
|
|
||||||
failure.invoke(e.getMessage());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@ReactMethod
|
@ReactMethod
|
||||||
public void stop() {
|
public void stop() {
|
||||||
Log.d(TAG, "Server Stopped.");
|
Log.d(MODULE_NAME, "Server stopped");
|
||||||
server.stop();
|
server.stop();
|
||||||
server = null;
|
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
|
@Override
|
||||||
public void onHostResume() {
|
public void onHostResume() {
|
||||||
//we can restart the server here as the success callback is not needed since an event is registered
|
start();
|
||||||
start(null, null);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
|
@ -22,21 +22,14 @@ public class Server extends NanoHTTPD {
|
||||||
private Map<String, ReadableMap> response;
|
private Map<String, ReadableMap> response;
|
||||||
private ReactContext reactContext;
|
private ReactContext reactContext;
|
||||||
|
|
||||||
private int timeout;
|
public Server(ReactContext context, int port) {
|
||||||
|
|
||||||
public Server(ReactContext context, int port, int timeout) {
|
|
||||||
super(port);
|
super(port);
|
||||||
reactContext = context;
|
reactContext = context;
|
||||||
this.timeout = timeout;
|
|
||||||
response = new HashMap<>();
|
response = new HashMap<>();
|
||||||
|
|
||||||
Log.d(TAG, "Server started");
|
Log.d(TAG, "Server started");
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setResponse(String uri, ReadableMap response) {
|
|
||||||
this.response.put(uri, response);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Response serve(IHTTPSession session) {
|
public Response serve(IHTTPSession session) {
|
||||||
Log.d(TAG, "Request received!");
|
Log.d(TAG, "Request received!");
|
||||||
|
@ -54,15 +47,13 @@ public class Server extends NanoHTTPD {
|
||||||
}
|
}
|
||||||
|
|
||||||
this.sendEvent(reactContext, SERVER_EVENT_ID, request);
|
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 {
|
private WritableMap fillRequestMap(IHTTPSession session) throws IOException, ResponseException {
|
||||||
Method method = session.getMethod();
|
Method method = session.getMethod();
|
||||||
WritableMap request = Arguments.createMap();
|
WritableMap request = Arguments.createMap();
|
||||||
request.putString("url", session.getUri());
|
request.putString("url", session.getUri());
|
||||||
request.putString("method", method.toString());
|
|
||||||
request.putMap("headers", this.convertToWritableMap(session.getHeaders()));
|
|
||||||
|
|
||||||
Map<String, String> files = new HashMap<>();
|
Map<String, String> files = new HashMap<>();
|
||||||
if (Method.POST.equals(method)) {
|
if (Method.POST.equals(method)) {
|
||||||
|
@ -74,84 +65,7 @@ public class Server extends NanoHTTPD {
|
||||||
return request;
|
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) {
|
private void sendEvent(ReactContext reactContext, String eventName, @Nullable WritableMap params) {
|
||||||
reactContext.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class).emit(eventName, 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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,71 +7,15 @@ import {DeviceEventEmitter} from 'react-native';
|
||||||
import {NativeModules} from 'react-native';
|
import {NativeModules} from 'react-native';
|
||||||
var Server = NativeModules.HttpServer;
|
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 = {
|
module.exports = {
|
||||||
|
start: function (port, callback) {
|
||||||
create: function (options, callback) {
|
if (port == 80) {
|
||||||
if (options.port == 80) {
|
|
||||||
throw "Invalid server port specified. Port 80 is reserved.";
|
throw "Invalid server port specified. Port 80 is reserved.";
|
||||||
}
|
}
|
||||||
|
|
||||||
Server.init(options, function () {
|
Server.start(port, function () {
|
||||||
DeviceEventEmitter.addListener('httpServerResponseReceived', function (request) {
|
DeviceEventEmitter.addListener('httpServerResponseReceived', callback);
|
||||||
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;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
},
|
|
||||||
|
|
||||||
// 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
|
// effectively pause the instance of the server
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
#import "RCTBridgeModule.h"
|
|
@ -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
|
|
@ -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 */;
|
||||||
|
}
|
|
@ -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>
|
Binary file not shown.
|
@ -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>
|
|
@ -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>
|
|
@ -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
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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:¶meter];
|
||||||
|
} else {
|
||||||
|
[scanner scanUpToCharactersFromSet:[NSCharacterSet whitespaceCharacterSet] intoString:¶meter];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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];
|
||||||
|
}
|
|
@ -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
|
||||||
|
};
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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:@"""];
|
||||||
|
}
|
||||||
|
|
||||||
|
- (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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
Loading…
Reference in New Issue