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         send();
99     }
100 
101     void send(JSONValue json)
102     {
103         _body = json.toString();
104         
105         header("Content-Type", "application/json");
106         send();
107     }
108 
109     void send()
110     {
111         if (_headersSent)
112         {
113             LogErrorf("Can't set headers after they are sent");
114             return;
115         }
116 
117         if (sendHeader())
118             sendBody();
119     }
120 
121     HttpResponse json(JSONValue json)
122     {
123         _body = json.toString();
124 
125         header("Content-Type", "application/json");
126 
127         return this;
128     }
129 
130     HttpResponse location(HttpStatusCode statusCode, string path)
131     {
132         code(statusCode);
133         location(path);
134 
135         return this;
136     }
137 
138     HttpResponse location(string path)
139     {
140         redirect(HttpStatusCode.SEE_OTHER, path);
141         header("Location", path);
142         return this;
143     }
144 
145     HttpResponse redirect(HttpStatusCode statusCode, string path)
146     {
147         location(statusCode, path);
148         return this;
149     }
150 
151     HttpResponse redirect(string path)
152     {
153         redirect(HttpStatusCode.FOUND, path);
154         return this;
155     }
156 
157     HttpResponse sendFile(string path, string filename = "")
158     {
159         import std.stdio : File;
160         
161         auto file = File(path, "r");
162         auto fileSize = file.size();
163 
164         if (filename.length == 0)
165         {
166             import std.array : split;
167             import std.string : replace;
168 
169             auto parts = path.replace("\\", "/").split("/");
170             if (parts.length == 1)
171             {
172                 filename = path;
173             }
174             else
175             {
176                 filename = parts[parts.length - 1];
177             }
178         }
179 
180         header(HttpHeader.CONTENT_DISPOSITION, "attachment; filename=" ~ filename ~ "; size=" ~ fileSize.to!string);
181         header(HttpHeader.CONTENT_LENGTH, fileSize.to!string);
182 
183         _httpContext.Write(headerToString());
184         _headersSent = true;
185         
186         auto buf = Nbuff.get(fileSize);
187         file.rawRead(buf.data());
188 
189         _httpContext.Write(NbuffChunk(buf, fileSize));
190         reset();
191 
192         return this;
193     }
194 
195     void end()
196     {
197         reset();
198         _httpContext.End();
199     }
200 
201     private bool sendHeader()
202     {
203         if (_headersSent)
204         {
205             LogErrorf("Can't set headers after they are sent");
206             return false;
207         }
208         
209         // if (_httpContext.keepAlive() && this.header(HttpHeader.CONTENT_LENGTH).length == 0)
210         header(HttpHeader.CONTENT_LENGTH, _body.length.to!string);
211 
212         _httpContext.Write(headerToString());
213         _headersSent = true;
214 
215         return true;
216     }
217 
218     private void sendBody()
219     {
220         _httpContext.Write(_body);
221         reset();
222     }
223 
224     void reset()
225     {
226         // clear request object
227         _httpContext.request().reset();
228 
229         _statusCode = HttpStatusCode.OK;
230         _headers = null;
231         _body = null;
232         _buffer = null;
233         _cookies = null;
234         _headersSent = false;
235     }
236 
237     string headerToString()
238     {
239         header(HttpHeader.SERVER, "Archttp");
240         header(HttpHeader.DATE, DateTime.GetTimeAsGMT());
241 
242         auto text = appender!string;
243         text ~= format!"HTTP/1.1 %d %s\r\n"(_statusCode, getHttpStatusMessage(_statusCode));
244         foreach (name, value; _headers) {
245             text ~= format!"%s: %s\r\n"(name, value);
246         }
247 
248         if (_cookies.length)
249         {
250             foreach (cookie ; _cookies)
251             {
252                 text ~= format!"Set-Cookie: %s\r\n"(cookie.toString());
253             }
254         }
255 
256         text ~= "\r\n";
257         
258         return text[];
259     }
260 }