]> git.r.bdr.sh - rbdr/ngx_http_office_hours_filter_module/blob - ngx_http_office_hours_filter_module.c
b9df683205ead8b2e83e5dd925705b62688edaff
[rbdr/ngx_http_office_hours_filter_module] / ngx_http_office_hours_filter_module.c
1 /*
2 * Copyright 2018 Rubén Beltrán del Río
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the
5 * License. You may obtain a copy of the License at
6 *
7 * http://www.apache.org/licenses/LICENSE-2.0
8 *
9 * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an
10 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
11 * specific language governing permissions and limitations under the License.
12 */
13
14
15 #include <ngx_config.h>
16 #include <ngx_core.h>
17 #include <ngx_http.h>
18
19 /*
20 * Declarations
21 */
22
23 /* Constants */
24
25 const ngx_uint_t WEEK_LENGTH = 7;
26 const char * CLOSED_TOKEN = "closed";
27 const ngx_str_t TIME_REGEX = ngx_string("([0-9]{1,2})(?:\\:([0-9]{2}))?\\-([0-9]{1,2})(?:\\:([0-9]{2}))?");
28 const ngx_str_t OUTPUT_HTML = ngx_string("<!doctype html><html><head><title>This website is currently closed!</title></head><body><h1>This website is currently closed!</h1><p>This website has closed for the day, please check our office hours below</p></body></html>");
29
30 /* Main Configuration Structure */
31
32 typedef struct {
33 ngx_array_t *office_hours;
34 } ngx_http_office_hours_conf_t;
35
36 /* Lifecycle Functions For Module Context */
37
38 static void *ngx_http_office_hours_create_conf(ngx_conf_t * cf);
39 static char *ngx_http_office_hours_merge_conf(ngx_conf_t * cf,
40 void *parent, void *child);
41 static ngx_int_t ngx_http_office_hours_init(ngx_conf_t * cf);
42
43 /* Configuration Handler */
44
45 static char *ngx_http_office_hours(ngx_conf_t * cf, ngx_command_t * cmd,
46 void *conf);
47
48 /* Filter Storage */
49
50 static ngx_http_output_header_filter_pt ngx_http_next_header_filter;
51 static ngx_http_output_body_filter_pt ngx_http_next_body_filter;
52
53 /* Utility Functions */
54 static ngx_uint_t ** parse_office_hours(ngx_array_t * office_hours);
55 static ngx_uint_t * parse_office_hours_string(ngx_str_t office_hours);
56 static ngx_flag_t within_office_hours(ngx_uint_t ** office_hours);
57 static ngx_uint_t get_day_of_week(time_t time);
58 static ngx_uint_t get_seconds_of_day(time_t time);
59 static ngx_uint_t parse_number(ngx_str_t string, ngx_uint_t start, ngx_uint_t end);
60
61 /* Compiled Regex */
62 ngx_regex_compile_t rc;
63
64 /*
65 * Module Definitions
66 */
67
68 /* Module Directives */
69
70 static ngx_command_t ngx_http_office_hours_commands[] = {
71 {
72 ngx_string("office_hours"),
73 NGX_HTTP_MAIN_CONF | NGX_HTTP_SRV_CONF | NGX_HTTP_LOC_CONF | NGX_CONF_TAKE1 | NGX_CONF_TAKE2 | NGX_CONF_TAKE3 | NGX_CONF_TAKE4 | NGX_CONF_TAKE5 | NGX_CONF_TAKE6 | NGX_CONF_TAKE7,
74 ngx_http_office_hours,
75 NGX_HTTP_LOC_CONF_OFFSET,
76 offsetof(ngx_http_office_hours_conf_t, office_hours),
77 NULL
78 },
79
80 ngx_null_command
81 };
82
83
84 /* Module Context */
85
86 static ngx_http_module_t ngx_http_office_hours_filter_module_ctx = {
87 NULL, /* Preconfiguration */
88 ngx_http_office_hours_init, /* Postconfiguration */
89
90 NULL, /* Create main configuration */
91 NULL, /* Initialize main configuration */
92
93 NULL, /* Create server configuration */
94 NULL, /* Merge server configuration */
95
96 ngx_http_office_hours_create_conf, /* Create location configuration */
97 ngx_http_office_hours_merge_conf /* Merge location configuration */
98 };
99
100
101 /* Module Definition */
102
103 ngx_module_t ngx_http_office_hours_filter_module = {
104 NGX_MODULE_V1, //Module Version
105 &ngx_http_office_hours_filter_module_ctx, //Module context
106 ngx_http_office_hours_commands, //Module commands
107 NGX_HTTP_MODULE, //Module Type
108 NULL, //Initialize Master
109 NULL, //Initialize Module
110 NULL, //Initialize Process
111 NULL, //Initialize Thread
112 NULL, //Exit Thread
113 NULL, //Exit Process
114 NULL, //Exit Master
115 NGX_MODULE_V1_PADDING
116 };
117
118
119 /*
120 * Main Header Filter
121 * If the current time is within office hours, it goes to the next
122 * handler. Otherwise it sets the headers to 403
123 */
124
125 static ngx_int_t
126 ngx_http_office_hours_header_filter(ngx_http_request_t * r)
127 {
128
129 ngx_uint_t ** parsed_office_hours;
130 ngx_http_office_hours_conf_t *conf;
131
132 conf =
133 ngx_http_get_module_loc_conf(r,
134 ngx_http_office_hours_filter_module);
135
136
137 if (conf->office_hours == NULL) {
138 ngx_log_error(NGX_LOG_DEBUG, r->connection->log, 0,
139 "Office hours disabled");
140 return ngx_http_next_header_filter(r);
141 }
142
143 parsed_office_hours = parse_office_hours(conf->office_hours);
144
145 if (within_office_hours(parsed_office_hours)) {
146 ngx_log_error(NGX_LOG_DEBUG, r->connection->log, 0,
147 "Within office hours");
148 return ngx_http_next_header_filter(r);
149 }
150
151 ngx_log_error(NGX_LOG_DEBUG, r->connection->log, 0,
152 "Outside office hours");
153
154 r->headers_out.status = NGX_HTTP_FORBIDDEN;
155 r->headers_out.content_length_n = OUTPUT_HTML.len;
156
157 return ngx_http_next_header_filter(r);
158 }
159
160 /*
161 * Main Body Filter
162 * If the current time is within office hours, it goes to the next
163 * handler. Otherwise it replaces the body with the office hours.
164 */
165
166 static ngx_int_t
167 ngx_http_office_hours_body_filter(ngx_http_request_t * r, ngx_chain_t *in)
168 {
169
170 ngx_buf_t *b;
171 ngx_uint_t ** parsed_office_hours;
172 ngx_chain_t out;
173 ngx_http_office_hours_conf_t *conf;
174
175 conf =
176 ngx_http_get_module_loc_conf(r,
177 ngx_http_office_hours_filter_module);
178
179
180 if (conf->office_hours == NULL) {
181 ngx_log_error(NGX_LOG_DEBUG, r->connection->log, 0,
182 "Office hours disabled");
183 return ngx_http_next_body_filter(r, in);
184 }
185
186 parsed_office_hours = parse_office_hours(conf->office_hours);
187
188 if (within_office_hours(parsed_office_hours)) {
189 ngx_log_error(NGX_LOG_DEBUG, r->connection->log, 0,
190 "Within office hours");
191 return ngx_http_next_body_filter(r, in);
192 }
193
194 ngx_log_error(NGX_LOG_DEBUG, r->connection->log, 0,
195 "Outside office hours");
196
197 b = ngx_pcalloc(r->pool, sizeof(ngx_buf_t));
198 if (b == NULL) {
199 ngx_log_error(NGX_LOG_ERR, r->connection->log, 0, "Failed to allocate response buffer.");
200 return NGX_HTTP_INTERNAL_SERVER_ERROR;
201 }
202
203 out.buf = b;
204 out.next = NULL;
205
206 b->start = OUTPUT_HTML.data;
207 b->pos = b->start;
208 b->end = OUTPUT_HTML.data + OUTPUT_HTML.len;
209 b->last = b->end;
210
211 b->memory = 1;
212 b->last_buf = 1;
213
214 return ngx_http_next_body_filter(r, &out);
215 }
216
217 /*
218 * Callback for `office_hours ` directive
219 * Reads the configuration loaded from the config file(cf)
220 * And writes it to the right place in the module configuration(conf)
221 */
222
223 static char *ngx_http_office_hours(ngx_conf_t * cf, ngx_command_t * cmd,
224 void *conf)
225 {
226
227 char *conf_structure = conf;
228
229 ngx_str_t *hours, *value;
230 ngx_array_t **office_hours;
231 ngx_uint_t i;
232
233 /* Gets the array from the config structure using the defined
234 * offset, and if the pointer is unset it creates a new one.
235 * (The first element is the directive itself, so we should be
236 * offset by 1)
237 */
238 office_hours = (ngx_array_t **) (conf_structure + cmd->offset);
239
240 if (*office_hours == NGX_CONF_UNSET_PTR) {
241 *office_hours = ngx_array_create(cf->pool, cf->args->nelts - 1,
242 sizeof(ngx_str_t));
243
244 if (*office_hours == NULL) {
245 return NGX_CONF_ERROR;
246 }
247 }
248 value = cf->args->elts;
249
250 for (i = 1; i < cf->args->nelts; ++i) {
251 hours = ngx_array_push(*office_hours);
252 if (hours == NULL) {
253 return NGX_CONF_ERROR;
254 }
255 *hours = value[i];
256 }
257
258 return NGX_CONF_OK;
259 }
260
261
262 /*
263 * Config Creator
264 * Initializes the configuration structure
265 */
266
267 static void *ngx_http_office_hours_create_conf(ngx_conf_t * cf)
268 {
269
270 ngx_http_office_hours_conf_t *conf;
271
272 conf = ngx_pcalloc(cf->pool, sizeof(ngx_http_office_hours_conf_t));
273
274 if (conf == NULL) {
275 return NULL;
276 }
277 conf->office_hours = NGX_CONF_UNSET_PTR;
278
279 return conf;
280 }
281
282 /*
283 * Merge Config Values
284 * Sets the defaults for the configuration and merges
285 * with other configurations
286 */
287
288 static char *ngx_http_office_hours_merge_conf(ngx_conf_t * cf,
289 void *parent, void *child)
290 {
291
292 ngx_http_office_hours_conf_t *prev = parent;
293 ngx_http_office_hours_conf_t *conf = child;
294
295 ngx_conf_merge_ptr_value(conf->office_hours, prev->office_hours, NULL);
296
297 return NGX_CONF_OK;
298 }
299
300 /*
301 * Parse the office hour strings in the configuration file
302 * to fill out the hours array (in seconds)
303 */
304
305 static ngx_uint_t ** parse_office_hours(ngx_array_t * office_hours)
306 {
307
308 ngx_str_t *hours;
309 ngx_uint_t ** parsed_office_hours;
310 ngx_uint_t i, j;
311
312 parsed_office_hours = malloc(7 * sizeof(ngx_uint_t *));
313
314 hours = office_hours->elts;
315
316 /*
317 * On the configuration file, the leftmost element
318 * always applies to all remaining days, all others
319 * are read from right to left. So first we will apply
320 * the initial override, and then iterate based on the
321 * number of overrides
322 */
323
324 for (i = 0; i < WEEK_LENGTH + 1 - office_hours->nelts; ++i) {
325 parsed_office_hours[i] = parse_office_hours_string(hours[0]);
326 }
327
328 for (i = 1; i < office_hours->nelts; ++i) {
329 j = WEEK_LENGTH - office_hours->nelts + i;
330 parsed_office_hours[j] = parse_office_hours_string(hours[i]);
331 }
332
333 return parsed_office_hours;
334 }
335
336 /*
337 * Given a time string or the closed token, return a tuple
338 * of numbers representing opening and closing hours
339 */
340
341 static ngx_uint_t * parse_office_hours_string(ngx_str_t office_hours)
342 {
343
344 int captures[(1 + 4) * 3];
345 ngx_int_t n;
346 ngx_uint_t * parsed_hours;
347
348 parsed_hours = malloc(2 * sizeof(ngx_uint_t));
349
350 if(ngx_strcmp(office_hours.data, CLOSED_TOKEN) == 0) {
351 parsed_hours[0] = 0;
352 parsed_hours[1] = 0;
353 return parsed_hours;
354 }
355
356 n = ngx_regex_exec(rc.regex, &office_hours, captures, (1 + 4) * 3);
357
358 if (n >= 0) {
359 /* Opening Hours */
360
361 parsed_hours[0] = 60 * 60 * parse_number(office_hours, captures[2], captures[3]);
362 parsed_hours[0] = parsed_hours[0] + 60 * parse_number(office_hours, captures[4], captures[5]);
363
364 parsed_hours[1] = 60 * 60 * parse_number(office_hours, captures[6], captures[7]);
365 parsed_hours[1] = parsed_hours[1] + 60 * parse_number(office_hours, captures[8], captures[9]);
366
367 return parsed_hours;
368 }
369
370 /* Non-matching strings count as open */
371
372 parsed_hours[0] = 0;
373 parsed_hours[1] = 86400;
374 return parsed_hours;
375 }
376
377 /*
378 * Given an office hours array, it returns whether or not
379 * it is currently within office hours.
380 */
381
382 static ngx_flag_t within_office_hours(ngx_uint_t ** office_hours)
383 {
384
385 time_t now;
386 ngx_uint_t day_of_week, seconds_of_day;
387 ngx_uint_t * current_hours;
388
389 ngx_time_update();
390 now = ngx_time();
391 day_of_week = get_day_of_week(now);
392 seconds_of_day = get_seconds_of_day(now);
393 current_hours = office_hours[day_of_week];
394
395 return seconds_of_day >= current_hours[0] && seconds_of_day <= current_hours[1];
396 }
397
398 /*
399 * Calculate the day of the week given a timestamp
400 */
401 static ngx_uint_t get_day_of_week(time_t time)
402 {
403
404 /* Epoch was thursday, so add 3 so we start on monday */
405 return (time / 86400 + 3) % 7;
406 }
407
408 /*
409 * Calculate the number of seconds elapsed today
410 */
411 static ngx_uint_t get_seconds_of_day(time_t time)
412 {
413
414 return time - (time / 86400) * 86400;
415 }
416
417 /*
418 * Parses a string, returns 0 if match was not found
419 */
420 static ngx_uint_t parse_number(ngx_str_t string, ngx_uint_t start, ngx_uint_t end)
421 {
422
423 if (end - start == 0) {
424 return 0;
425 }
426
427 return ngx_atoi(&string.data[start], end - start);
428 }
429
430 /*
431 * Postconfig Initialization Handler
432 * Sets the request filter at the top of the chain
433 */
434
435 static ngx_int_t ngx_http_office_hours_init(ngx_conf_t * cf)
436 {
437
438 ngx_http_next_header_filter = ngx_http_top_header_filter;
439 ngx_http_top_header_filter = ngx_http_office_hours_header_filter;
440
441 ngx_http_next_body_filter = ngx_http_top_body_filter;
442 ngx_http_top_body_filter = ngx_http_office_hours_body_filter;
443
444 rc.pattern = TIME_REGEX;
445 rc.pool = cf->pool;
446 if (ngx_regex_compile(&rc) != NGX_OK) {
447 return NGX_ERROR;
448 }
449
450 return NGX_OK;
451 }