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 }