1 /**
2  * Authors: Szabo Bogdan <szabobogdan@yahoo.com>
3  * Date: 3 21, 2015
4  * License: Subject to the terms of the MIT license, as written in the included LICENSE.txt file.
5  * Copyright: Public Domain
6  */
7 module vibedav.http;
8 
9 import vibedav.base;
10 
11 import std.datetime;
12 import std..string;
13 import std.conv;
14 
15 import vibe.http.server;
16 import vibe.inet.message;
17 import vibe.core.file;
18 import vibe.stream.operations;
19 import vibe.utils.dictionarylist;
20 
21 debug {
22 	import std.stdio;
23 }
24 
25 alias HeaderList = DictionaryList!(string, false, 12L, false);
26 
27 string getHeaderValue(HeaderList headers, string name, string defaultValue = "") {
28 
29 	string value = defaultValue;
30 
31 	if(name !in headers && defaultValue == "")
32 		throw new DavException(HTTPStatus.internalServerError, "Can't find '"~name~"' in headers.");
33 	else
34 		value = headers.get(name, defaultValue);
35 
36 	return value;
37 }
38 
39 @("basic check for getHeaderValue")
40 unittest {
41 	HeaderList list;
42 	list["key"] = "value";
43 	auto val = getHeaderValue(list, "key");
44 	assert(val == "value");
45 }
46 
47 @("getHeaderValue with default value")
48 unittest {
49 	HeaderList list;
50 	auto val = getHeaderValue(list, "key", "default");
51 	assert(val == "default");
52 }
53 
54 @("check if getHeaderValue fails")
55 unittest {
56 	bool raised = false;
57 
58 	try {
59 		HeaderList list;
60 		list["key"] = "value";
61 		getHeaderValue(list, "key1");
62 	} catch(DavException e) {
63 		raised = true;
64 	}
65 
66 	assert(raised);
67 }
68 
69 void enforce(string[] valid)(string value) {
70 	bool isValid;
71 
72 	foreach(validValue; valid)
73 		if(value == validValue)
74 			isValid = true;
75 
76 	if(!isValid)
77 		throw new DavException(HTTPStatus.internalServerError, "Invalid value.");
78 }
79 
80 string getHeader(string name)(HeaderList headers) {
81 	string value;
82 
83 	static if(name == "Depth") {
84 		value = getHeaderValue(headers, name, "infinity");
85 		value.enforce!(["0", "1", "infinity"]);
86 	} else static if(name == "Overwrite") {
87 		value = getHeaderValue(headers, name, "F");
88 		value.enforce!(["T", "F"]);
89 	} else static if(name == "If") {
90 		value = getHeaderValue(headers, name, "()");
91 	} else static if(name == "Content-Length") {
92 		value = getHeaderValue(headers, name, "0");
93 	} else {
94 		value = getHeaderValue(headers, name);
95 	}
96 
97 	return value;
98 }
99 
100 /// The HTTP response wrapper
101 struct DavResponse {
102 	private {
103 		HTTPServerResponse response;
104 		string _content;
105 	}
106 
107 	HTTPStatus statusCode = HTTPStatus.ok;
108 
109 	@property {
110 		void content(string value) {
111 			_content = value;
112 		}
113 
114 		void content(DavProp value) {
115 			content(value.toString);
116 		}
117 
118 		void mimeType(string value) {
119 			response.headers["Content-Type"] = value;
120 		}
121 	}
122 
123 	this(HTTPServerResponse res) {
124 		this.response = res;
125 		this.response.headers["Content-Type"] = "text/plain";
126 	}
127 
128 	void opIndexAssign(T)(string value, T key) {
129 		static if(is( T == string )) {
130 			response.headers[key] = value;
131 		} else {
132 			response.headers[key] = value.to!string;
133 		}
134 	}
135 
136 	string opIndex(string key) {
137 		return response.headers[key];
138 	}
139 
140 	version(unittest) {
141 		static DavResponse Create() {
142 			import vibe.stream.stdio;
143 			import vibe.utils.memory;
144 
145 			StdFileStream conn = new StdFileStream(false, true);
146 			ConnectionStream raw_connection = new StdFileStream(false, true);
147 			HTTPServerSettings settings = new HTTPServerSettings;
148 			Allocator req_alloc = defaultAllocator();
149 
150 			HTTPServerResponse response = new HTTPServerResponse(conn, raw_connection, settings, req_alloc);
151 			return DavResponse(response);
152 		}
153 	}
154 
155 	@("Test opIndex")
156 	unittest {
157 		DavResponse davResponse = DavResponse.Create;
158 		davResponse["test"] = "value";
159 		assert(davResponse["test"] == "value");
160 	}
161 
162 	void flush() {
163 		response.statusCode = statusCode;
164 		debug writeln("\n", _content);
165 		response.writeBody(_content, response.headers["Content-Type"]);
166 	}
167 
168 	void flush(DavResource resource) {
169 		response.statusCode = statusCode;
170 		response.writeRawBody(resource.stream, resource.contentLength);
171 	}
172 
173 	void setPropContent (DavResource[] list, bool[string] props, HTTPStatus[string] responseCodes = null) {
174 		statusCode = HTTPStatus.multiStatus;
175 		mimeType = "application/xml";
176 
177 		string str = `<?xml version="1.0" encoding="UTF-8"?>`;
178 		auto multistatus = new DavProp;
179 		multistatus.addNamespace("d", "DAV:");
180 		multistatus.name = "d:multistatus";
181 
182 		foreach(item; list)
183 			item.filterProps(multistatus, props);
184 
185 		if(responseCodes !is null) {
186 			foreach(string path, HTTPStatus status; responseCodes) {
187 				DavProp element = new DavProp;
188 				multistatus.addChild(element);
189 
190 				element.name = "response";
191 				element.namespaceAttr = "DAV:";
192 				element[`d:href`] = path;
193 				element[`d:status`] = "HTTP/1.1 " ~ status.to!int.to!string ~ " " ~ status.to!string;
194 			}
195 		}
196 
197 		_content =  str ~ multistatus.toString;
198 	}
199 }
200 
201 /// The HTTP request wrapper
202 struct DavRequest {
203 	private {
204 		HTTPServerRequest request;
205 		DavProp document;
206 	}
207 
208 	this(HTTPServerRequest req) {
209 		request = req;
210 	}
211 
212 	@property {
213 		string path() {
214 			return request.path;
215 		}
216 
217 		string lockToken() {
218 			return getHeader!"Lock-Token"(request.headers)[1..$-1];
219 		}
220 
221 		DavDepth depth() {
222 			if("depth" in request.headers) {
223 				string strDepth = getHeader!"Depth"(request.headers);
224 
225 				if(strDepth == "0") return DavDepth.zero;
226 				if(strDepth == "1") return DavDepth.one;
227 			}
228 
229 			return DavDepth.infinity;
230 		}
231 
232 		ulong contentLength() {
233 
234 			string value = "0";
235 
236 			if("Content-Length" in request.headers)
237 				value = getHeader!"Content-Length"(request.headers);
238 			else if("Transfer-Encoding" in request.headers && "X-Expected-Entity-Length" in request.headers) {
239 				enforceBadRequest(request.headers["Transfer-Encoding"] == "chunked" ||
240 								  request.headers["Transfer-Encoding"] == "Chunked");
241 				value = getHeader!"X-Expected-Entity-Length"(request.headers);
242 			}
243 
244 			return value.to!ulong;
245 		}
246 
247 		DavProp content() {
248 
249 			if(document !is null)
250 				return document;
251 
252 			if(request.bodyReader is null)
253 				return document;
254 
255 			string requestXml = cast(string)request.bodyReader.readAllUTF8;
256 
257 			debug {
258 				writeln("\n", requestXml, "\n");
259 			}
260 
261 			if(requestXml.length > 0) {
262 				try document = requestXml.parseXMLProp;
263 				catch (DavPropException e)
264 					throw new DavException(HTTPStatus.badRequest, "Invalid xml body.");
265 			}
266 
267 			return document;
268 		}
269 
270 		ubyte[] rawContent() {
271 			return request.bodyReader.readAll;
272 		}
273 
274 		InputStream stream() {
275 			return request.bodyReader;
276 		}
277 
278 		URL url() {
279 			return request.fullURL;
280 		}
281 
282 		string requestUrl() {
283 			return request.requestURL;
284 		}
285 
286 		Duration timeout() {
287 			Duration t;
288 
289 			string strTimeout = getHeader!"Timeout"(request.headers);
290 			auto secIndex = strTimeout.indexOf("Second-");
291 
292 			if(strTimeout.indexOf("Infinite") != -1) {
293 				t = dur!"hours"(24);
294 			} else if(secIndex != -1) {
295 				auto val = strTimeout[secIndex+7..$].to!int;
296 				t = dur!"seconds"(val);
297 			} else {
298 				throw new DavException(HTTPStatus.internalServerError, "Invalid timeout value");
299 			}
300 
301 			return t;
302 		}
303 
304 		IfHeader ifCondition() {
305 			return IfHeader.parse(getHeader!"If"(request.headers));
306 		}
307 
308 		URL destination() {
309 			return URL(getHeader!"Destination"(request.headers));
310 		}
311 
312 		bool overwrite() {
313 			return getHeader!"Overwrite"(request.headers) == "T";
314 		}
315 
316 		static DavRequest Create(string requestUrl="") {
317 			string[string] headers;
318 			return DavRequest.Create(requestUrl, headers);
319 		}
320 
321 		static DavRequest Create(string requestUrl, string[string] headers) {
322 			InetHeaderMap inetHeaders;
323 
324 			//set the headers
325 			foreach(string name, string val; headers)
326 				inetHeaders[name] = val;
327 
328 			HTTPServerRequest req = createTestHTTPServerRequest(URL(requestUrl), HTTPMethod.GET, inetHeaders);
329 
330 			return DavRequest(req);
331 		}
332 
333 		string username() {
334 			return request.username;
335 		}
336 	}
337 
338 	bool ifModifiedSince(DavResource resource) {
339 		if( auto pv = "If-Modified-Since" in request.headers )
340 			if( *pv == toRFC822DateTimeString(resource.lastModified) )
341 				return false;
342 
343 		return true;
344 	}
345 
346 	bool ifNoneMatch(DavResource resource) {
347 		if( auto pv = "If-None-Match" in request.headers )
348 			if ( *pv == resource.eTag )
349 				return false;
350 
351 		return true;
352 	}
353 }