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.HttpResponse;
13 
14 import archttp.HttpStatusCode;
15 import archttp.HttpContext;
16 import archttp.Cookie;
17 import archttp.HttpHeader;
18 
19 import nbuff;
20 
21 import geario.util.DateTime;
22 import geario.logging;
23 
24 import std.format;
25 import std.array;
26 import std.conv : to;
27 import std.json;
28 
29 
30 class HttpResponse
31 {
32     private
33     {
34         ushort         _statusCode = HttpStatusCode.OK;
35         string[string] _headers;
36         string         _body;
37         ubyte[]        _buffer;
38         HttpContext    _httpContext;
39         Cookie[string] _cookies;
40 
41         // for ..
42         bool           _headersSent = false;
43     }
44 
45 public:
46     /*
47      * Construct an empty response.
48      */
49     this(HttpContext ctx)
50     {
51         _httpContext = ctx;
52     }
53 
54     bool headerSent()
55     {
56         return _headersSent;
57     }
58 
59     HttpResponse header(string header, string value)
60     {
61         _headers[header] = value;
62         
63         return this;
64     }
65 
66     HttpResponse code(HttpStatusCode statusCode)
67     {
68         _statusCode = statusCode;
69 
70         return this;
71     }
72 
73     ushort code()
74     {
75         return _statusCode;
76     }
77 
78     HttpResponse cookie(string name, string value, string path = "/", string domain = "", string expires = "", long maxAge = 604800, bool secure = false, bool httpOnly = false)
79     {
80         _cookies[name] = new Cookie(name, value, path, domain, expires, maxAge, secure, httpOnly);
81         return this;
82     }
83 
84     HttpResponse cookie(Cookie cookie)
85     {
86         _cookies[cookie.name()] = cookie;
87         return this;
88     }
89 
90     Cookie cookie(string name)
91     {
92         return _cookies.get(name, null);
93     }
94 
95     void send(string body)
96     {
97         _body = body;
98         
99         header("Content-Type", "text/plain");
100         send();
101     }
102 
103     void send(JSONValue json)
104     {
105         _body = json.toString();
106         
107         header("Content-Type", "application/json");
108         send();
109     }
110 
111     void send()
112     {
113         if (_headersSent)
114         {
115             log.error("Can't set headers after they are sent");
116             return;
117         }
118 
119         if (sendHeader())
120             sendBody();
121     }
122 
123     HttpResponse json(JSONValue json)
124     {
125         _body = json.toString();
126 
127         header("Content-Type", "application/json");
128 
129         return this;
130     }
131 
132     HttpResponse location(HttpStatusCode statusCode, string path)
133     {
134         code(statusCode);
135         location(path);
136 
137         return this;
138     }
139 
140     HttpResponse location(string path)
141     {
142         redirect(HttpStatusCode.SEE_OTHER, path);
143         header("Location", path);
144         return this;
145     }
146 
147     HttpResponse redirect(HttpStatusCode statusCode, string path)
148     {
149         location(statusCode, path);
150         return this;
151     }
152 
153     HttpResponse redirect(string path)
154     {
155         redirect(HttpStatusCode.FOUND, path);
156         return this;
157     }
158 
159     HttpResponse sendFile(string path, string filename = "")
160     {
161         import std.stdio : File;
162         
163         auto file = File(path, "r");
164         auto fileSize = file.size();
165 
166         if (filename.length == 0)
167         {
168             import std.array : split;
169             import std.string : replace;
170 
171             auto parts = path.replace("\\", "/").split("/");
172             if (parts.length == 1)
173             {
174                 filename = path;
175             }
176             else
177             {
178                 filename = parts[parts.length - 1];
179             }
180         }
181 
182         header(HttpHeader.CONTENT_DISPOSITION, "attachment; filename=" ~ filename ~ "; size=" ~ fileSize.to!string);
183         header(HttpHeader.CONTENT_LENGTH, fileSize.to!string);
184 
185         _httpContext.Write(headerToString());
186         _headersSent = true;
187         
188         auto buf = Nbuff.get(fileSize);
189         file.rawRead(buf.data());
190 
191         _httpContext.Write(NbuffChunk(buf, fileSize));
192         reset();
193 
194         return this;
195     }
196 
197     void end()
198     {
199         reset();
200         _httpContext.End();
201     }
202 
203     private bool sendHeader()
204     {
205         if (_headersSent)
206         {
207             log.error("Can't set headers after they are sent");
208             return false;
209         }
210         
211         // if (_httpContext.keepAlive() && this.header(HttpHeader.CONTENT_LENGTH).length == 0)
212         header(HttpHeader.CONTENT_LENGTH, _body.length.to!string);
213 
214         _httpContext.Write(headerToString());
215         _headersSent = true;
216 
217         return true;
218     }
219 
220     private void sendBody()
221     {
222         _httpContext.Write(_body);
223         reset();
224     }
225 
226     void reset()
227     {
228         // clear request object
229         _httpContext.request().reset();
230 
231         _statusCode = HttpStatusCode.OK;
232         _headers = null;
233         _body = null;
234         _buffer = null;
235         _cookies = null;
236         _headersSent = false;
237     }
238 
239     string headerToString()
240     {
241         header(HttpHeader.SERVER, "Archttp");
242         header(HttpHeader.DATE, DateTime.GetTimeAsGMT());
243 
244         auto text = appender!string;
245         text ~= format!"HTTP/1.1 %d %s\r\n"(_statusCode, getHttpStatusMessage(_statusCode));
246         foreach (name, value; _headers) {
247             text ~= format!"%s: %s\r\n"(name, value);
248         }
249 
250         if (_cookies.length)
251         {
252             foreach (cookie ; _cookies)
253             {
254                 text ~= format!"Set-Cookie: %s\r\n"(cookie.toString());
255             }
256         }
257 
258         text ~= "\r\n";
259         
260         return text[];
261     }
262 }