1 /**
2 	A HTTP 1.1/1.0 server implementation.
3 
4 	Copyright: © 2012-2013 Sönke Ludwig
5 	License: Subject to the terms of the MIT license, as written in the included LICENSE.txt file.
6 	Authors: Sönke Ludwig, Jan Krüger
7 */
8 module vibe.http.log;
9 
10 import vibe.core.file;
11 import vibe.core.log;
12 import vibe.core.sync : InterruptibleTaskMutex, performLocked;
13 import vibe.http.server;
14 import vibe.utils.array : FixedAppender;
15 
16 import std.array;
17 import std.conv;
18 import std.exception;
19 import std.string;
20 
21 
22 class HTTPLogger {
23 	@safe:
24 
25 	private {
26 		string m_format;
27 		const(HTTPServerSettings) m_settings;
28 		InterruptibleTaskMutex m_mutex;
29 		Appender!(char[]) m_lineAppender;
30 	}
31 
32 	this(const HTTPServerSettings settings, string format)
33 	{
34 		m_format = format;
35 		m_settings = settings;
36 		m_mutex = new InterruptibleTaskMutex;
37 		m_lineAppender.reserve(2048);
38 	}
39 
40 	void close() {}
41 
42 	final void log(scope HTTPServerRequest req, scope HTTPServerResponse res)
43 	{
44 		m_mutex.performLocked!(() @safe {
45 			m_lineAppender.clear();
46 			formatApacheLog(m_lineAppender, m_format, req, res, m_settings);
47 			writeLine(m_lineAppender.data);
48 		});
49 	}
50 
51 	protected abstract void writeLine(const(char)[] ln);
52 }
53 
54 
55 final class HTTPConsoleLogger : HTTPLogger {
56 @safe:
57 
58 	this(HTTPServerSettings settings, string format)
59 	{
60 		super(settings, format);
61 	}
62 
63 	protected override void writeLine(const(char)[] ln)
64 	{
65 		logInfo("%s", ln);
66 	}
67 }
68 
69 
70 final class HTTPFileLogger : HTTPLogger {
71 @safe:
72 
73 	private {
74 		FileStream m_stream;
75 	}
76 
77 	this(HTTPServerSettings settings, string format, string filename)
78 	{
79 		m_stream = openFile(filename, FileMode.append);
80 		super(settings, format);
81 	}
82 
83 	override void close()
84 	{
85 		m_stream.close();
86 		m_stream = FileStream.init;
87 	}
88 
89 	protected override void writeLine(const(char)[] ln)
90 	{
91 		assert(!!m_stream);
92 		m_stream.write(ln);
93 		m_stream.write("\n");
94 		m_stream.flush();
95 	}
96 }
97 
98 void formatApacheLog(R)(ref R ln, string format, scope HTTPServerRequest req, scope HTTPServerResponse res, in HTTPServerSettings settings)
99 @safe {
100 	import std.format : formattedWrite;
101 	enum State {Init, Directive, Status, Key, Command}
102 
103 	State state = State.Init;
104 	bool conditional = false;
105 	bool negate = false;
106 	bool match = false;
107 	string statusStr;
108 	string key = "";
109 	while( format.length > 0 ) {
110 		final switch(state) {
111 			case State.Init:
112 				auto idx = format.indexOf('%');
113 				if( idx < 0 ) {
114 					ln.put( format );
115 					format = "";
116 				} else {
117 					ln.put( format[0 .. idx] );
118 					format = format[idx+1 .. $];
119 
120 					state = State.Directive;
121 				}
122 				break;
123 			case State.Directive:
124 				if( format[0] == '!' ) {
125 					conditional = true;
126 					negate = true;
127 					format = format[1 .. $];
128 					state = State.Status;
129 				} else if( format[0] == '%' ) {
130 					ln.put("%");
131 					format = format[1 .. $];
132 					state = State.Init;
133 				} else if( format[0] == '{' ) {
134 					format = format[1 .. $];
135 					state = State.Key;
136 				} else if( format[0] >= '0' && format[0] <= '9' ) {
137 					conditional = true;
138 					state = State.Status;
139 				} else {
140 					state = State.Command;
141 				}
142 				break;
143 			case State.Status:
144 				if( format[0] >= '0' && format[0] <= '9' ) {
145 					statusStr ~= format[0];
146 					format = format[1 .. $];
147 				} else if( format[0] == ',' ) {
148 					statusStr = "";
149 					format = format[1 .. $];
150 				} else if( format[0] == '{' ) {
151 					format = format[1 .. $];
152 					state = State.Key;
153 				} else {
154 					state = State.Command;
155 				}
156 				if (statusStr.length == 3 && !match) {
157 					auto status = parse!int(statusStr);
158 					match = status == res.statusCode;
159 				}
160 				break;
161 			case State.Key:
162 				auto idx = format.indexOf('}');
163 				enforce(idx > -1, "Missing '}'");
164 				key = format[0 .. idx];
165 				format = format[idx+1 .. $];
166 				state = State.Command;
167 				break;
168 			case State.Command:
169 				if( conditional && negate == match ) {
170 					ln.put('-');
171 					format = format[1 .. $];
172 					state = State.Init;
173 					break;
174 				}
175 				switch(format[0]) {
176 					case 'a': //Remote IP-address
177 						ln.put(req.peer);
178 						break;
179 					//TODO case 'A': //Local IP-address
180 					//case 'B': //Size of Response in bytes, excluding headers
181 					case 'b': //same as 'B' but a '-' is written if no bytes where sent
182 						if (!res.bytesWritten) ln.put('-');
183 						else formattedWrite(() @trusted { return &ln; } (), "%s", res.bytesWritten);
184 						break;
185 					case 'C': //Cookie content {cookie}
186 						import std.algorithm : joiner;
187 						enforce(key != "", "cookie name missing");
188 						auto values = req.cookies.getAll(key);
189 						if (values.length) ln.formattedWrite("%s", values.joiner(";"));
190 						else ln.put("-");
191 						break;
192 					case 'D': //The time taken to serve the request
193 						auto d = res.timeFinalized - req.timeCreated;
194 						formattedWrite(() @trusted { return &ln; } (), "%s", d.total!"msecs"());
195 						break;
196 					//case 'e': //Environment variable {variable}
197 					//case 'f': //Filename
198 					case 'h': //Remote host
199 						ln.put(req.peer);
200 						break;
201 					case 'H': //The request protocol
202 						ln.put("HTTP");
203 						break;
204 					case 'i': //Request header {header}
205 						enforce(key != "", "header name missing");
206 						if (auto pv = key in req.headers) ln.put(*pv);
207 						else ln.put("-");
208 						break;
209 					case 'm': //Request method
210 						ln.put(httpMethodString(req.method));
211 						break;
212 					case 'o': //Response header {header}
213 						enforce(key != "", "header name missing");
214 						if( auto pv = key in res.headers ) ln.put(*pv);
215 						else ln.put("-");
216 						break;
217 					case 'p': //port
218 						formattedWrite(() @trusted { return &ln; } (), "%s", settings.port);
219 						break;
220 					//case 'P': //Process ID
221 					case 'q': //query string (with prepending '?')
222 						ln.put("?");
223 						ln.put(req.queryString);
224 						break;
225 					case 'r': //First line of Request
226 						ln.put(httpMethodString(req.method));
227 						ln.put(' ');
228 						ln.put(req.requestURL);
229 						ln.put(' ');
230 						ln.put(getHTTPVersionString(req.httpVersion));
231 						break;
232 					case 's': //Status
233 						formattedWrite(() @trusted { return &ln; } (), "%s", res.statusCode);
234 						break;
235 					case 't': //Time the request was received {format}
236 						ln.put(req.timeCreated.toSimpleString());
237 						break;
238 					case 'T': //Time taken to server the request in seconds
239 						auto d = res.timeFinalized - req.timeCreated;
240 						formattedWrite(() @trusted { return &ln; } (), "%s", d.total!"seconds");
241 						break;
242 					case 'u': //Remote user
243 						ln.put(req.username.length ? req.username : "-");
244 						break;
245 					case 'U': //The URL path without query string
246 						ln.put(req.path);
247 						break;
248 					case 'v': //Server name
249 						ln.put(req.host.length ? req.host : "-");
250 						break;
251 					default:
252 						throw new Exception("Unknown directive '" ~ format[0] ~ "' in log format string");
253 				}
254 				state = State.Init;
255 				format = format[1 .. $];
256 				break;
257 		}
258 	}
259 }