2 * Copyright 2018 Rubén Beltrán del Río
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
7 * http://www.apache.org/licenses/LICENSE-2.0
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.
14 #include <ngx_config.h>
24 const ngx_uint_t WEEK_LENGTH
= 7;
25 const char * CLOSED_TOKEN
= "closed";
26 const ngx_str_t TIME_REGEX
= ngx_string("([0-9]{1,2})(?:\\:([0-9]{2}))?\\-([0-9]{1,2})(?:\\:([0-9]{2}))?");
27 const ngx_str_t HEAD_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><ul>");
28 const ngx_str_t OPEN_SERVER_TIME_HTML
= ngx_string("</ul><p><em>Current Server Time is: ");
29 const ngx_str_t CLOSE_SERVER_TIME_HTML
= ngx_string("</em></p>");
30 const ngx_str_t FOOT_HTML
= ngx_string("</body></html>");
31 const char * DAY_NAMES
[7] = {
41 /* Main Configuration Structure */
44 ngx_array_t
*office_hours
;
45 ngx_str_t additional_information
;
46 } ngx_http_office_hours_conf_t
;
48 /* Lifecycle Functions For Module Context */
50 static void *ngx_http_office_hours_create_conf(ngx_conf_t
* cf
);
51 static char *ngx_http_office_hours_merge_conf(ngx_conf_t
* cf
,
52 void *parent
, void *child
);
53 static ngx_int_t
ngx_http_office_hours_init(ngx_conf_t
* cf
);
55 /* Configuration Handler */
57 static char *ngx_http_office_hours(ngx_conf_t
* cf
, ngx_command_t
* cmd
,
62 static ngx_http_output_header_filter_pt ngx_http_next_header_filter
;
63 static ngx_http_output_body_filter_pt ngx_http_next_body_filter
;
65 /* Utility Functions */
66 static ngx_uint_t
** parse_office_hours(ngx_array_t
* office_hours
);
67 static ngx_uint_t
* parse_office_hours_string(ngx_str_t office_hours
);
68 static ngx_flag_t
within_office_hours(ngx_uint_t
** office_hours
);
69 static ngx_uint_t
get_day_of_week(time_t time
);
70 static ngx_uint_t
get_seconds_of_day(time_t time
);
71 static ngx_uint_t
parse_number(ngx_str_t string
, ngx_uint_t start
, ngx_uint_t end
);
72 static ngx_str_t
create_output_html(ngx_http_office_hours_conf_t
* conf
);
73 static char * format_hours(ngx_uint_t
* seconds
, char * day
);
74 static char * format_seconds(ngx_uint_t seconds
);
75 static char * left_pad(unsigned int number
);
78 ngx_regex_compile_t rc
;
79 ngx_str_t OUTPUT_HTML
;
85 /* Module Directives */
87 static ngx_command_t ngx_http_office_hours_commands
[] = {
89 ngx_string("office_hours"),
90 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
,
91 ngx_http_office_hours
,
92 NGX_HTTP_LOC_CONF_OFFSET
,
93 offsetof(ngx_http_office_hours_conf_t
, office_hours
),
97 ngx_string("office_hours_additional_information"),
98 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
,
99 ngx_conf_set_str_slot
,
100 NGX_HTTP_LOC_CONF_OFFSET
,
101 offsetof(ngx_http_office_hours_conf_t
, additional_information
),
110 static ngx_http_module_t ngx_http_office_hours_filter_module_ctx
= {
111 NULL
, /* Preconfiguration */
112 ngx_http_office_hours_init
, /* Postconfiguration */
114 NULL
, /* Create main configuration */
115 NULL
, /* Initialize main configuration */
117 NULL
, /* Create server configuration */
118 NULL
, /* Merge server configuration */
120 ngx_http_office_hours_create_conf
, /* Create location configuration */
121 ngx_http_office_hours_merge_conf
/* Merge location configuration */
124 /* Module Definition */
126 ngx_module_t ngx_http_office_hours_filter_module
= {
127 NGX_MODULE_V1
, //Module Version
128 &ngx_http_office_hours_filter_module_ctx
, //Module context
129 ngx_http_office_hours_commands
, //Module commands
130 NGX_HTTP_MODULE
, //Module Type
131 NULL
, //Initialize Master
132 NULL
, //Initialize Module
133 NULL
, //Initialize Process
134 NULL
, //Initialize Thread
138 NGX_MODULE_V1_PADDING
143 * If the current time is within office hours, it goes to the next
144 * handler. Otherwise it sets the headers to 403
148 ngx_http_office_hours_header_filter(ngx_http_request_t
* r
)
151 ngx_uint_t
** parsed_office_hours
;
152 ngx_http_office_hours_conf_t
*conf
;
155 ngx_http_get_module_loc_conf(r
,
156 ngx_http_office_hours_filter_module
);
158 if (conf
->office_hours
== NULL
) {
159 ngx_log_error(NGX_LOG_DEBUG
, r
->connection
->log
, 0,
160 "Office hours disabled");
161 return ngx_http_next_header_filter(r
);
164 parsed_office_hours
= parse_office_hours(conf
->office_hours
);
166 if (within_office_hours(parsed_office_hours
)) {
167 ngx_log_error(NGX_LOG_DEBUG
, r
->connection
->log
, 0,
168 "Within office hours");
169 return ngx_http_next_header_filter(r
);
172 ngx_log_error(NGX_LOG_DEBUG
, r
->connection
->log
, 0,
173 "Outside office hours");
175 if (OUTPUT_HTML
.data
== NULL
) {
176 OUTPUT_HTML
= create_output_html(conf
);
179 r
->headers_out
.status
= NGX_HTTP_FORBIDDEN
;
180 r
->headers_out
.content_length_n
= OUTPUT_HTML
.len
;
182 return ngx_http_next_header_filter(r
);
187 * If the current time is within office hours, it goes to the next
188 * handler. Otherwise it replaces the body with the office hours.
192 ngx_http_office_hours_body_filter(ngx_http_request_t
* r
, ngx_chain_t
*in
)
196 ngx_uint_t
** parsed_office_hours
;
198 ngx_http_office_hours_conf_t
*conf
;
201 ngx_http_get_module_loc_conf(r
,
202 ngx_http_office_hours_filter_module
);
204 if (conf
->office_hours
== NULL
) {
205 ngx_log_error(NGX_LOG_DEBUG
, r
->connection
->log
, 0,
206 "Office hours disabled");
207 return ngx_http_next_body_filter(r
, in
);
210 parsed_office_hours
= parse_office_hours(conf
->office_hours
);
212 if (within_office_hours(parsed_office_hours
)) {
213 ngx_log_error(NGX_LOG_DEBUG
, r
->connection
->log
, 0,
214 "Within office hours");
215 return ngx_http_next_body_filter(r
, in
);
218 ngx_log_error(NGX_LOG_DEBUG
, r
->connection
->log
, 0,
219 "Outside office hours");
221 if (OUTPUT_HTML
.data
== NULL
) {
222 OUTPUT_HTML
= create_output_html(conf
);
225 b
= ngx_pcalloc(r
->pool
, sizeof(ngx_buf_t
));
227 ngx_log_error(NGX_LOG_ERR
, r
->connection
->log
, 0, "Failed to allocate response buffer.");
228 return NGX_HTTP_INTERNAL_SERVER_ERROR
;
234 b
->start
= OUTPUT_HTML
.data
;
236 b
->end
= OUTPUT_HTML
.data
+ OUTPUT_HTML
.len
;
242 return ngx_http_next_body_filter(r
, &out
);
246 * Callback for `office_hours ` directive
247 * Reads the configuration loaded from the config file(cf)
248 * And writes it to the right place in the module configuration(conf)
251 static char *ngx_http_office_hours(ngx_conf_t
* cf
, ngx_command_t
* cmd
,
255 char *conf_structure
= conf
;
257 ngx_str_t
*hours
, *value
;
258 ngx_array_t
**office_hours
;
261 /* Gets the array from the config structure using the defined
262 * offset, and if the pointer is unset it creates a new one.
263 * (The first element is the directive itself, so we should be
266 office_hours
= (ngx_array_t
**) (conf_structure
+ cmd
->offset
);
268 if (*office_hours
== NGX_CONF_UNSET_PTR
) {
269 *office_hours
= ngx_array_create(cf
->pool
, cf
->args
->nelts
- 1,
272 if (*office_hours
== NULL
) {
273 return NGX_CONF_ERROR
;
276 value
= cf
->args
->elts
;
278 for (i
= 1; i
< cf
->args
->nelts
; ++i
) {
279 hours
= ngx_array_push(*office_hours
);
281 return NGX_CONF_ERROR
;
291 * Initializes the configuration structure
294 static void *ngx_http_office_hours_create_conf(ngx_conf_t
* cf
)
297 ngx_http_office_hours_conf_t
*conf
;
299 conf
= ngx_pcalloc(cf
->pool
, sizeof(ngx_http_office_hours_conf_t
));
304 conf
->office_hours
= NGX_CONF_UNSET_PTR
;
310 * Merge Config Values
311 * Sets the defaults for the configuration and merges
312 * with other configurations
315 static char *ngx_http_office_hours_merge_conf(ngx_conf_t
* cf
,
316 void *parent
, void *child
)
319 ngx_http_office_hours_conf_t
*prev
= parent
;
320 ngx_http_office_hours_conf_t
*conf
= child
;
322 ngx_conf_merge_ptr_value(conf
->office_hours
, prev
->office_hours
, NULL
);
328 * Parse the office hour strings in the configuration file
329 * to fill out the hours array (in seconds)
332 static ngx_uint_t
** parse_office_hours(ngx_array_t
* office_hours
)
336 ngx_uint_t
** parsed_office_hours
;
339 parsed_office_hours
= malloc(7 * sizeof(ngx_uint_t
*));
341 hours
= office_hours
->elts
;
344 * On the configuration file, the leftmost element
345 * always applies to all remaining days, all others
346 * are read from right to left. So first we will apply
347 * the initial override, and then iterate based on the
348 * number of overrides
351 for (i
= 0; i
< WEEK_LENGTH
+ 1 - office_hours
->nelts
; ++i
) {
352 parsed_office_hours
[i
] = parse_office_hours_string(hours
[0]);
355 for (i
= 1; i
< office_hours
->nelts
; ++i
) {
356 j
= WEEK_LENGTH
- office_hours
->nelts
+ i
;
357 parsed_office_hours
[j
] = parse_office_hours_string(hours
[i
]);
360 return parsed_office_hours
;
364 * Given a time string or the closed token, return a tuple
365 * of numbers representing opening and closing hours
368 static ngx_uint_t
* parse_office_hours_string(ngx_str_t office_hours
)
371 int captures
[(1 + 4) * 3];
373 ngx_uint_t
* parsed_hours
;
375 parsed_hours
= malloc(2 * sizeof(ngx_uint_t
));
377 if(ngx_strcmp(office_hours
.data
, CLOSED_TOKEN
) == 0) {
383 n
= ngx_regex_exec(rc
.regex
, &office_hours
, captures
, (1 + 4) * 3);
388 parsed_hours
[0] = 60 * 60 * parse_number(office_hours
, captures
[2], captures
[3]);
389 parsed_hours
[0] = parsed_hours
[0] + 60 * parse_number(office_hours
, captures
[4], captures
[5]);
391 parsed_hours
[1] = 60 * 60 * parse_number(office_hours
, captures
[6], captures
[7]);
392 parsed_hours
[1] = parsed_hours
[1] + 60 * parse_number(office_hours
, captures
[8], captures
[9]);
397 /* Non-matching strings count as open */
400 parsed_hours
[1] = 86400;
405 * Given an office hours array, it returns whether or not
406 * it is currently within office hours.
409 static ngx_flag_t
within_office_hours(ngx_uint_t
** office_hours
)
413 ngx_uint_t day_of_week
, seconds_of_day
;
414 ngx_uint_t
* current_hours
;
418 day_of_week
= get_day_of_week(now
);
419 seconds_of_day
= get_seconds_of_day(now
);
420 current_hours
= office_hours
[day_of_week
];
422 return seconds_of_day
>= current_hours
[0] && seconds_of_day
<= current_hours
[1];
426 * Calculate the day of the week given a timestamp
429 static ngx_uint_t
get_day_of_week(time_t time
)
432 /* Epoch was thursday, so add 3 so we start on monday */
433 return (time
/ 86400 + 3) % 7;
437 * Calculate the number of seconds elapsed today
440 static ngx_uint_t
get_seconds_of_day(time_t time
)
443 return time
- (time
/ 86400) * 86400;
447 * Parses a string, returns 0 if match was not found
450 static ngx_uint_t
parse_number(ngx_str_t string
, ngx_uint_t start
, ngx_uint_t end
)
453 if (end
- start
== 0) {
457 return ngx_atoi(&string
.data
[start
], end
- start
);
461 * Given the current office hours configuration it creates the custom
465 static ngx_str_t
create_output_html(ngx_http_office_hours_conf_t
* conf
)
468 char * output_buffer
;
470 ngx_str_t output_html
;
471 ngx_uint_t i
, seconds_of_day
;
472 ngx_uint_t
** parsed_office_hours
;
474 size_t base_size
= 1024;
475 size_t additional_info_size
= conf
->additional_information
.len
;
476 size_t buffer_size
= base_size
+ additional_info_size
+ 1;
477 output_buffer
= malloc(buffer_size
* sizeof(char));
479 /* UH-OH we couldn't allocate the bufer */
480 if (!output_buffer
) {
481 output_html
.data
= NULL
;
486 parsed_office_hours
= parse_office_hours(conf
->office_hours
);
488 seconds_of_day
= get_seconds_of_day(now
);
492 written
+= snprintf(output_buffer
+ written
, buffer_size
- written
, "%s", (char *) HEAD_HTML
.data
);
494 for (i
= 0; i
< 7; ++i
) {
495 written
+= snprintf(output_buffer
+ written
, buffer_size
- written
, "%s", format_hours(parsed_office_hours
[i
], (char *) DAY_NAMES
[i
]));
498 written
+= snprintf(output_buffer
+ written
, buffer_size
- written
, "%s", (char *) OPEN_SERVER_TIME_HTML
.data
);
499 written
+= snprintf(output_buffer
+ written
, buffer_size
- written
, "%s", format_seconds(seconds_of_day
));
500 written
+= snprintf(output_buffer
+ written
, buffer_size
- written
, "%s", (char *) CLOSE_SERVER_TIME_HTML
.data
);
502 if (conf
->additional_information
.len
> 0) {
503 char additional_info
[conf
->additional_information
.len
+ 1];
504 ngx_memcpy(additional_info
, conf
->additional_information
.data
, conf
->additional_information
.len
);
505 additional_info
[conf
->additional_information
.len
] = '\0';
506 written
+= snprintf(output_buffer
+ written
, buffer_size
- written
, "%s", additional_info
);
509 snprintf(output_buffer
+ written
, buffer_size
- written
, "%s", (char *) FOOT_HTML
.data
);
511 output_html
.data
= (unsigned char *) output_buffer
;
512 output_html
.len
= strlen(output_buffer
);
518 * Given a tuple of seconds and a day name, outputs an HTML
519 * string containing the formatted data as a list item
522 static char * format_hours(ngx_uint_t
* hours
, char * day
)
527 output_html
= malloc(64 * sizeof(char));
528 if (hours
[0] == hours
[1]) {
529 sprintf(output_html
, "<li><strong>%s</strong>: CLOSED</li>",
535 sprintf(output_html
, "<li><strong>%s</strong>: %s - %s</li>",
537 (char *) format_seconds(hours
[0]),
538 (char *) format_seconds(hours
[1])
545 * Given seconds of the day, it returns a string showing
546 * HH:MM in 24 hour format
549 static char * format_seconds(ngx_uint_t seconds
)
553 unsigned int hours
, minutes
;
555 output_html
= malloc(6 * sizeof(char));
557 hours
= seconds
/ 60 / 60;
558 minutes
= (seconds
/ 60) % 60;
560 sprintf(output_html
, "%s:%s",
569 * Returns a number as a string adding 0 if < 10
572 static char * left_pad(unsigned int number
)
575 char * output_string
;
579 output_string
= malloc(4 * sizeof(char));
585 snprintf(output_string
, 4, "%s%u", padding
, number
);
587 return output_string
;
591 * Postconfig Initialization Handler
592 * Sets the request filter at the top of the chain
595 static ngx_int_t
ngx_http_office_hours_init(ngx_conf_t
* cf
)
598 ngx_http_next_header_filter
= ngx_http_top_header_filter
;
599 ngx_http_top_header_filter
= ngx_http_office_hours_header_filter
;
601 ngx_http_next_body_filter
= ngx_http_top_body_filter
;
602 ngx_http_top_body_filter
= ngx_http_office_hours_body_filter
;
604 rc
.pattern
= TIME_REGEX
;
606 if (ngx_regex_compile(&rc
) != NGX_OK
) {