1 /* 2 * Archttp - A highly performant web framework written in D. 3 * 4 * Copyright (C) 2021-2022 Kerisy.com 5 * 6 * Website: https://www.kerisy.com 7 * 8 * Licensed under the Apache-2.0 License. 9 * 10 */ 11 12 module archttp.HttpRequestParser; 13 14 import archttp.HttpRequest; 15 import archttp.HttpMessageParser; 16 import archttp.MultiPart; 17 import archttp.HttpRequestParserHandler; 18 19 import std.conv : to; 20 import std.array : split; 21 import std.string : indexOf, stripRight; 22 import std.algorithm : startsWith; 23 import std.regex : regex, matchAll; 24 import std.file : write, isDir, isFile, mkdir, mkdirRecurse, FileException; 25 26 import std.stdio : writeln; 27 28 enum ParserStatus : ushort { 29 READY = 1, 30 PARTIAL, 31 COMPLETED, 32 FAILED 33 } 34 35 class HttpRequestParser 36 { 37 private 38 { 39 string _data; 40 41 long _parsedLength = 0; 42 ParserStatus _parserStatus; 43 44 bool _headerParsed = false; 45 46 HttpRequestParserHandler _headerHandler; 47 HttpMessageParser _headerParser; 48 49 HttpRequest _request; 50 51 string _contentType; 52 long _contentLength = 0; 53 54 string _fileUploadTempPath = "./tmp"; 55 } 56 57 this() 58 { 59 _headerHandler = new HttpRequestParserHandler; 60 _headerParser = new HttpMessageParser(_headerHandler); 61 62 _parserStatus = ParserStatus.READY; 63 } 64 65 ParserStatus parserStatus() 66 { 67 return _parserStatus; 68 } 69 70 ulong parse(string data) 71 { 72 _data = data; 73 74 if (_headerParsed == false && !parseHeader()) 75 { 76 return 0; 77 } 78 79 // var for paring content 80 _contentType = _request.header("Content-Type"); 81 82 string contentLengthString = _request.header("Content-Length"); 83 if (contentLengthString.length > 0) 84 _contentLength = contentLengthString.to!long; 85 86 if (_contentLength > 0) 87 { 88 if (!parseBody()) 89 { 90 return 0; 91 } 92 } 93 94 _parserStatus = ParserStatus.COMPLETED; 95 96 return _parsedLength; 97 } 98 99 private bool parseHeader() 100 { 101 auto result = _headerParser.parseRequest(_data); 102 if (result < 0) 103 { 104 if (result == -1) 105 { 106 _parserStatus = ParserStatus.PARTIAL; 107 return false; 108 } 109 110 _parserStatus = ParserStatus.FAILED; 111 return false; 112 } 113 114 _request = _headerHandler.request(); 115 _headerHandler.reset(); 116 _parsedLength = result; 117 _headerParsed = true; 118 119 return true; 120 } 121 122 private bool parseBody() 123 { 124 if (_data.length < _parsedLength + _contentLength) 125 { 126 writeln(_data.length, " - ", _parsedLength, " - ", _contentLength); 127 return false; 128 } 129 130 if (_contentType.startsWith("application/json") || _contentType.startsWith("text/")) 131 { 132 _request.body(_data[_parsedLength.._parsedLength + _contentLength]); 133 _parsedLength += _contentLength; 134 return true; 135 } 136 137 if (_contentType.startsWith("application/x-www-form-urlencoded")) 138 { 139 if (!parseFormFields()) 140 return false; 141 142 return true; 143 } 144 145 if (_contentType.startsWith("multipart/form-data")) 146 { 147 if (!parseMultipart()) 148 return false; 149 } 150 151 return true; 152 } 153 154 private bool parseFormFields() 155 { 156 foreach (fieldStr; _data[_parsedLength.._parsedLength + _contentLength].split("&")) 157 { 158 auto s = fieldStr.indexOf("="); 159 if (s > 0) 160 _request.fields[fieldStr[0..s]] = fieldStr[s+1..fieldStr.length]; 161 } 162 163 _parsedLength += _contentLength; 164 165 return true; 166 } 167 168 private bool parseMultipart() 169 { 170 string boundary = "--" ~ getBoundary(); 171 172 while (true) 173 { 174 bool isFile = false; 175 176 long boundaryIndex = _data[_parsedLength .. $].indexOf(boundary); 177 if (boundaryIndex == -1) 178 break; 179 180 boundaryIndex += _parsedLength; 181 182 long boundaryEndIndex = boundaryIndex + boundary.length + 2; // boundary length + "--" length + "\r\n" length 183 if (boundaryEndIndex + 2 == _data.length && _data[boundaryIndex .. boundaryEndIndex] == boundary ~ "--") 184 { 185 writeln("parse done"); 186 _parserStatus = ParserStatus.COMPLETED; 187 _parsedLength = boundaryIndex + boundary.length + 2 + 2; 188 break; 189 } 190 191 long ignoreBoundaryIndex = boundaryIndex + boundary.length + 2; 192 193 long nextBoundaryIndex = _data[ignoreBoundaryIndex .. $].indexOf(boundary); 194 195 if (nextBoundaryIndex == -1) 196 { 197 // not last boundary? parse error? 198 writeln("not last boundary? parse error?"); 199 break; 200 } 201 202 nextBoundaryIndex += ignoreBoundaryIndex; 203 204 long contentIndex = _data[ignoreBoundaryIndex .. nextBoundaryIndex].indexOf("\r\n\r\n"); 205 if (contentIndex == -1) 206 { 207 break; 208 } 209 contentIndex += ignoreBoundaryIndex + 4; 210 211 string headerData = _data[ignoreBoundaryIndex .. contentIndex-4]; 212 MultiPart part; 213 214 foreach (headerContent ; headerData.split("\r\n")) 215 { 216 long i = headerContent.indexOf(":"); 217 string headerKey = headerContent[0..i]; 218 string headerValue = headerContent[i..headerContent.length]; 219 if (headerKey != "Content-Disposition") 220 { 221 part.headers[headerKey] = headerValue; 222 223 continue; 224 } 225 226 // for part.name 227 string nameValuePrefix = "name=\""; 228 long nameIndex = headerValue.indexOf(nameValuePrefix); 229 if (nameIndex == -1) 230 continue; 231 232 long nameValueIndex = nameIndex + nameValuePrefix.length; 233 long nameEndIndex = nameValueIndex + headerValue[nameValueIndex..$].indexOf("\""); 234 part.name = headerValue[nameValueIndex..nameEndIndex]; 235 236 // for part.filename 237 string filenameValuePrefix = "filename=\""; 238 long filenameIndex = headerValue.indexOf(filenameValuePrefix); 239 if (filenameIndex >= 0) 240 { 241 isFile = true; 242 243 long filenameValueIndex = filenameIndex + filenameValuePrefix.length; 244 long filenameEndIndex = filenameValueIndex + headerValue[filenameValueIndex..$].indexOf("\""); 245 part.filename = headerValue[filenameValueIndex..filenameEndIndex]; 246 } 247 } 248 249 long contentSize = nextBoundaryIndex-2-contentIndex; 250 if (isFile) 251 { 252 if (!part.filename.length == 0) 253 { 254 part.filesize = contentSize; 255 // TODO: FreeBSD / macOS / Linux or Windows? 256 string dirSeparator = "/"; 257 version (Windows) 258 { 259 dirSeparator = "\\"; 260 } 261 262 if (!isDir(_fileUploadTempPath)) 263 { 264 try 265 { 266 mkdir(_fileUploadTempPath); 267 } 268 catch (FileException e) 269 { 270 writeln("mkdir error: ", e); 271 // throw e; 272 } 273 } 274 275 string filepath = stripRight(_fileUploadTempPath, dirSeparator) ~ dirSeparator ~ part.filename; 276 part.filepath = filepath; 277 try 278 { 279 write(filepath, _data[contentIndex..nextBoundaryIndex-2]); 280 } 281 catch (FileException e) 282 { 283 writeln("file write error: ", e); 284 } 285 286 _request.files ~= part; 287 } 288 } 289 else 290 { 291 _request.fields[part.name] = _data[contentIndex..nextBoundaryIndex-2]; 292 } 293 294 _parsedLength = nextBoundaryIndex; 295 } 296 297 return true; 298 } 299 300 private string getBoundary() 301 { 302 string searchString = "boundary="; 303 long index = _contentType.indexOf(searchString); 304 if (index == -1) 305 return ""; 306 307 return _contentType[index+searchString.length.._contentType.length]; 308 } 309 310 private void extractCookies() 311 { 312 // QByteArrayList temp(headerField.values(HTTP::COOKIE)); 313 // int size = temp.size(); 314 // for(int i = 0; i < size; ++i) 315 // { 316 // const QByteArray &txt = temp[i].replace(";", ";\n");; 317 // QList<QNetworkCookie> cookiesList = QNetworkCookie::parseCookies(txt); 318 // for(QNetworkCookie &cookie : cookiesList) 319 // { 320 // if(cookie.name() == HTTP::SESSION_ID) 321 // sessionId = cookie.value(); 322 // cookies.push_back(std::move(cookie)); 323 // } 324 // } 325 } 326 327 HttpRequest request() 328 { 329 return _request; 330 } 331 332 void reset() 333 { 334 _contentLength = 0; 335 _request = null; 336 _headerParsed = false; 337 _parserStatus = ParserStatus.READY; 338 _data = ""; 339 _contentType = ""; 340 } 341 }