├── README.markdown ├── config └── ngx_http_static_etags_module.c /README.markdown: -------------------------------------------------------------------------------- 1 | Nginx Static Etags 2 | ------------------ 3 | 4 | Nginx doesn't generate etags for static content. I think it should. If I can remember enough C from college to make it work as a module, I will. 5 | 6 | ### Installation 7 | 8 | Download the module however you like. I'd recommend pulling it down with Git by simply cloning this repository: 9 | 10 | mkdir ~/src 11 | cd ~/src 12 | git clone https://github.com/kkung/nginx-static-etags.git ./nginx-static-stags 13 | 14 | To use the module, you'll have to compile it into Nginx. So, download the Nginx source, configure it with the module path, and compile: 15 | 16 | mkdir ~/src 17 | cd ~/src 18 | curl -O http://sysoev.ru/nginx/nginx-0.6.32.tar.gz 19 | tar -zxvf ./nginx-0.6.32.tar.gz 20 | cd ./nginx-0.6.32 21 | ./configure --add-module=/~src/nginx-static-etags 22 | make 23 | sudo make install 24 | 25 | And you're done! 26 | 27 | ### Configuration 28 | 29 | Add `etags` to the relevant `location` blocks in your `nginx.conf` file: 30 | 31 | location / { 32 | ... 33 | etags on; 34 | etag_hash on|off; 35 | etag_hash_method md5|sha1; 36 | ... 37 | } 38 | 39 | It's currently an on/off toggle. The plan is to bring it to feature parity with [the Apache configuration option][apache]. It's really not there yet. 40 | 41 | [apache]: http://httpd.apache.org/docs/1.3/mod/core.html#fileetag 42 | -------------------------------------------------------------------------------- /config: -------------------------------------------------------------------------------- 1 | USE_MD5=YES 2 | USE_SHA1=YES 3 | ngx_addon_name=ngx_http_static_etags_module 4 | #HTTP_AUX_FILTER_MODULES="$HTTP_AUX_FILTER_MODULES ngx_http_static_etags_module" 5 | HTTP_NOT_MODIFIED_FILTER_MODULE="$HTTP_NOT_MODIFIED_FILTER_MODULE ngx_http_static_etags_module" 6 | NGX_ADDON_SRCS="$NGX_ADDON_SRCS $ngx_addon_dir/ngx_http_static_etags_module.c" 7 | -------------------------------------------------------------------------------- /ngx_http_static_etags_module.c: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2009 Adrian Jung ( http://me2day.net/kkung, kkungkkung@gmail.com ). 3 | * All rights reserved. 4 | * All original code was written by Mike West ( http://mikewest.org/ ) 5 | * 6 | * Copyright 2008 Mike West ( http://mikewest.org/ ) 7 | * 8 | * The following is released under the Creative Commons BSD license, 9 | * available for your perusal at `http://creativecommons.org/licenses/BSD/` 10 | */ 11 | #include 12 | #include 13 | #include 14 | #include 15 | 16 | #if (NGX_HAVE_OPENSSL_MD5_H) 17 | #include 18 | #else 19 | #include 20 | #endif 21 | 22 | #if (NGX_OPENSSL_MD5) 23 | #define MD5Init MD5_Init 24 | #define MD5Update MD5_Update 25 | #define MD5Final MD5_Final 26 | #endif 27 | 28 | #if (NGX_HAVE_OPENSSL_SHA1_H) 29 | #include 30 | #else 31 | #include 32 | #endif 33 | 34 | #define MD5_HEX_DIGEST_LENGTH MD5_DIGEST_LENGTH*2 35 | #define SHA_HEX_DIGEST_LENGTH SHA_DIGEST_LENGTH*2 36 | /* 37 | * Two configuration elements: `enable_etags` and `etag_format`, specified in 38 | * the `Location` block. 39 | */ 40 | typedef struct { 41 | ngx_flag_t enable; 42 | ngx_flag_t enable_hash; 43 | ngx_str_t hash_method; 44 | 45 | } ngx_http_static_etags_loc_conf_t; 46 | 47 | static ngx_http_output_header_filter_pt ngx_http_next_header_filter; 48 | /*static ngx_http_output_body_filter_pt ngx_http_next_body_filter;*/ 49 | 50 | static void * ngx_http_static_etags_create_loc_conf(ngx_conf_t *cf); 51 | static char * ngx_http_static_etags_merge_loc_conf(ngx_conf_t *cf, void *parent, void *child); 52 | static ngx_int_t ngx_http_static_etags_init(ngx_conf_t *cf); 53 | static ngx_int_t ngx_http_static_etags_header_filter(ngx_http_request_t *r); 54 | static char *ngx_http_static_etags_hash_method(ngx_conf_t *cf, ngx_command_t *cmd, void *conf); 55 | 56 | static ngx_command_t ngx_http_static_etags_commands[] = { 57 | { ngx_string( "etags" ), 58 | NGX_HTTP_MAIN_CONF|NGX_HTTP_SRV_CONF|NGX_HTTP_LOC_CONF|NGX_CONF_FLAG, 59 | ngx_conf_set_flag_slot, 60 | NGX_HTTP_LOC_CONF_OFFSET, 61 | offsetof( ngx_http_static_etags_loc_conf_t, enable ), 62 | NULL }, 63 | { ngx_string( "etag_hash"), 64 | NGX_HTTP_MAIN_CONF|NGX_HTTP_SRV_CONF|NGX_HTTP_LOC_CONF|NGX_CONF_FLAG, 65 | ngx_conf_set_flag_slot, 66 | NGX_HTTP_LOC_CONF_OFFSET, 67 | offsetof ( ngx_http_static_etags_loc_conf_t, enable_hash), 68 | NULL }, 69 | { ngx_string( "etag_hash_method"), 70 | NGX_HTTP_MAIN_CONF|NGX_HTTP_SRV_CONF|NGX_HTTP_LOC_CONF|NGX_CONF_FLAG, 71 | ngx_http_static_etags_hash_method, 72 | NGX_HTTP_LOC_CONF_OFFSET, 73 | 0, 74 | NULL }, 75 | ngx_null_command 76 | }; 77 | 78 | static ngx_http_module_t ngx_http_static_etags_module_ctx = { 79 | NULL, /* preconfiguration */ 80 | ngx_http_static_etags_init, /* postconfiguration */ 81 | 82 | NULL, /* create main configuration */ 83 | NULL, /* init main configuration */ 84 | 85 | NULL, /* create server configuration */ 86 | NULL, /* merge server configuration */ 87 | 88 | ngx_http_static_etags_create_loc_conf, /* create location configuration */ 89 | ngx_http_static_etags_merge_loc_conf, /* merge location configuration */ 90 | }; 91 | 92 | ngx_module_t ngx_http_static_etags_module = { 93 | NGX_MODULE_V1, 94 | &ngx_http_static_etags_module_ctx, /* module context */ 95 | ngx_http_static_etags_commands, /* module directives */ 96 | NGX_HTTP_MODULE, /* module type */ 97 | NULL, /* init master */ 98 | NULL, /* init module */ 99 | NULL, /* init process */ 100 | NULL, /* init thread */ 101 | NULL, /* exit thread */ 102 | NULL, /* exit process */ 103 | NULL, /* exit master */ 104 | NGX_MODULE_V1_PADDING 105 | }; 106 | 107 | 108 | static char *ngx_http_static_etags_hash_method(ngx_conf_t *cf, ngx_command_t *cmd, void *conf) { 109 | ngx_http_static_etags_loc_conf_t *l_conf = conf; 110 | ngx_str_t *value; 111 | 112 | if (l_conf->hash_method.len) { 113 | return "is duplicate"; 114 | } 115 | 116 | value = cf->args->elts; 117 | 118 | if ( ngx_strcmp(value[1].data,"md5") == 0 ) { 119 | l_conf->hash_method = value[1]; 120 | } else if ( ngx_strcmp(value[1].data,"sha1") == 0 ) { 121 | l_conf->hash_method = value[1]; 122 | } else { 123 | return "invalid value. md5 or sha1 allowed"; 124 | } 125 | 126 | return NGX_CONF_OK; 127 | 128 | } 129 | static void * ngx_http_static_etags_create_loc_conf(ngx_conf_t *cf) { 130 | ngx_http_static_etags_loc_conf_t *conf; 131 | 132 | conf = ngx_pcalloc( cf->pool, sizeof( ngx_http_static_etags_loc_conf_t ) ); 133 | if ( NULL == conf ) { 134 | return NGX_CONF_ERROR; 135 | } 136 | conf->enable = NGX_CONF_UNSET_UINT; 137 | conf->enable_hash = NGX_CONF_UNSET_UINT; 138 | return conf; 139 | } 140 | 141 | static char * ngx_http_static_etags_merge_loc_conf(ngx_conf_t *cf, void *parent, void *child) { 142 | ngx_http_static_etags_loc_conf_t *prev = parent; 143 | ngx_http_static_etags_loc_conf_t *conf = child; 144 | 145 | ngx_conf_merge_value( conf->enable, prev->enable, 0 ); 146 | ngx_conf_merge_value( conf->enable_hash, prev->enable_hash, 1); 147 | ngx_conf_merge_str_value( conf->hash_method, prev->hash_method, "md5"); 148 | 149 | return NGX_CONF_OK; 150 | } 151 | 152 | static ngx_int_t ngx_http_static_etags_init(ngx_conf_t *cf) { 153 | ngx_http_next_header_filter = ngx_http_top_header_filter; 154 | ngx_http_top_header_filter = ngx_http_static_etags_header_filter; 155 | 156 | return NGX_OK; 157 | } 158 | 159 | static ngx_int_t ngx_http_static_etags_header_filter(ngx_http_request_t *r) { 160 | // int status; 161 | ngx_log_t *log; 162 | u_char *p; 163 | size_t root; 164 | ngx_str_t path; 165 | ngx_http_static_etags_loc_conf_t *loc_conf; 166 | // struct stat stat_result; 167 | ngx_str_t str_buffer; 168 | ngx_str_t etag; 169 | 170 | uint i; 171 | static u_char hex[] = "0123456789abcdef"; 172 | 173 | u_char *hashed_etag; 174 | u_char *hash = NULL; 175 | 176 | // for nginx_open_file_cache 177 | ngx_open_file_info_t of; 178 | ngx_http_core_loc_conf_t *clcf; 179 | 180 | log = r->connection->log; 181 | 182 | loc_conf = ngx_http_get_module_loc_conf( r, ngx_http_static_etags_module ); 183 | 184 | ngx_log_debug1(NGX_LOG_DEBUG_HTTP, log,0 ,"[etag] working? \"%d\"",loc_conf->enable); 185 | 186 | // Is the module active? 187 | if ( loc_conf->enable ) { 188 | 189 | p = ngx_http_map_uri_to_path( r, &path, &root, 0 ); 190 | if ( NULL == p ) { 191 | return NGX_HTTP_INTERNAL_SERVER_ERROR; 192 | } 193 | 194 | 195 | ngx_log_debug1(NGX_LOG_DEBUG_HTTP, log, 0, 196 | "[etag] http filename: \"%s\"", path.data); 197 | 198 | 199 | clcf = ngx_http_get_module_loc_conf(r, ngx_http_core_module); 200 | of.test_dir = 0; 201 | of.test_only = 1; 202 | of.valid = clcf->open_file_cache_valid; 203 | of.min_uses = clcf->open_file_cache_min_uses; 204 | of.errors = clcf->open_file_cache_errors; 205 | of.events = clcf->open_file_cache_events; 206 | 207 | 208 | // status = stat( (char *) path.data, &stat_result ); 209 | // ngx_log_debug1(NGX_LOG_DEBUG_HTTP, log, 0, 210 | // "[etag] stat returned: \"%d\"", status); 211 | // 212 | // Did the `stat` succeed? 213 | // if ( 0 == status) { 214 | if (ngx_open_cached_file(clcf->open_file_cache, &path, &of, r->pool) == NGX_OK ) { 215 | 216 | str_buffer.data = ngx_palloc(r->pool, 217 | 3 + r->uri.len + sizeof(of.size) + sizeof(of.mtime) 218 | ); 219 | if ( str_buffer.data == NULL ) { 220 | ngx_log_debug(NGX_LOG_DEBUG_HTTP, log, 0, 221 | "[etag] failed memory allocation"); 222 | return NGX_ERROR; 223 | } 224 | 225 | str_buffer.len = ngx_sprintf(str_buffer.data, "%V_%T_%z",&r->uri,of.mtime,of.size) - str_buffer.data; 226 | 227 | 228 | ngx_log_debug1(NGX_LOG_DEBUG_HTTP, log, 0, 229 | "[etag] st_size: '%d'", of.size); 230 | ngx_log_debug1(NGX_LOG_DEBUG_HTTP, log, 0, 231 | "[etag] st_mtime: '%d'", of.mtime); 232 | ngx_log_debug1(NGX_LOG_DEBUG_HTTP, log, 0, 233 | "[etag] Concatted: '%V'", &str_buffer ); 234 | 235 | r->headers_out.etag = ngx_list_push(&r->headers_out.headers); 236 | if (r->headers_out.etag == NULL) { 237 | return NGX_ERROR; 238 | } 239 | r->headers_out.etag->hash = 1; 240 | r->headers_out.etag->key.len = sizeof("Etag") - 1; 241 | r->headers_out.etag->key.data = (u_char *) "Etag"; 242 | 243 | if ( loc_conf->enable_hash) { 244 | 245 | uint digest_length = 0; 246 | uint hex_digest_length = 0; 247 | 248 | if ( ngx_strcmp(loc_conf->hash_method.data,"md5") == 0) { 249 | digest_length = MD5_DIGEST_LENGTH; 250 | hex_digest_length = MD5_HEX_DIGEST_LENGTH; 251 | 252 | hash = ngx_palloc(r->pool, digest_length); 253 | if ( hash == NULL ) { 254 | ngx_log_debug(NGX_LOG_DEBUG_HTTP, log, 0, 255 | "[etag] failed memory allocation"); 256 | return NGX_ERROR; 257 | } 258 | 259 | MD5_CTX md5_ctx; 260 | MD5Init(&md5_ctx); 261 | MD5Update(&md5_ctx,str_buffer.data,str_buffer.len); 262 | MD5Final(hash,&md5_ctx); 263 | 264 | } else if ( ngx_strcmp(loc_conf->hash_method.data,"sha1") == 0 ) { 265 | digest_length = SHA_DIGEST_LENGTH; 266 | hex_digest_length = SHA_HEX_DIGEST_LENGTH; 267 | 268 | hash = ngx_palloc(r->pool, digest_length); 269 | if ( hash == NULL ) { 270 | ngx_log_debug(NGX_LOG_DEBUG_HTTP, log, 0, 271 | "[etag] failed memory allocation"); 272 | return NGX_ERROR; 273 | } 274 | 275 | SHA_CTX sha1_ctx; 276 | SHA1_Init(&sha1_ctx); 277 | SHA1_Update(&sha1_ctx,str_buffer.data,str_buffer.len); 278 | SHA1_Final(hash,&sha1_ctx); 279 | } else { 280 | // ?? 281 | } 282 | 283 | 284 | 285 | etag.data = ngx_palloc(r->pool, hex_digest_length); 286 | if ( etag.data == NULL ) { 287 | ngx_log_debug(NGX_LOG_DEBUG_HTTP, log, 0, 288 | "[etag] failed memory allocation(hash)"); 289 | return NGX_ERROR; 290 | } 291 | 292 | etag.len = hex_digest_length; 293 | hashed_etag = etag.data; 294 | for ( i = 0 ; i < hex_digest_length; i++ ) { 295 | *hashed_etag++ = hex[hash[i] >> 4]; 296 | *hashed_etag++ = hex[hash[i] & 0xf]; 297 | } 298 | 299 | *hashed_etag = '\0'; 300 | 301 | ngx_log_debug1(NGX_LOG_DEBUG_HTTP, log, 0, "[etag] hash result \"%V\"",&etag); 302 | r->headers_out.etag->value.len = etag.len; 303 | r->headers_out.etag->value.data = etag.data; 304 | } else { 305 | r->headers_out.etag->value.len = str_buffer.len; 306 | r->headers_out.etag->value.data = str_buffer.data; 307 | } 308 | 309 | ngx_uint_t found=0; 310 | ngx_list_part_t *part; 311 | ngx_table_elt_t *header; 312 | ngx_table_elt_t if_none_match; 313 | 314 | part = &r->headers_in.headers.part; 315 | header = part->elts; 316 | 317 | for ( i = 0 ; ; i++ ) { 318 | if ( i >= part->nelts) { 319 | if ( part->next == NULL ) { 320 | break; 321 | } 322 | 323 | part = part->next; 324 | header = part->elts; 325 | i = 0; 326 | } 327 | 328 | ngx_log_debug2(NGX_LOG_DEBUG_HTTP, r->connection->log, 0 , "[Etag] Header %V: %V", &header[i].key, &header[i].value ); 329 | 330 | if ( ngx_strcmp(header[i].key.data, "If-None-Match") == 0 ) { 331 | if_none_match = header[i]; 332 | found = 1; 333 | break; 334 | } 335 | } 336 | 337 | if ( found ) { 338 | ngx_log_debug2(NGX_LOG_DEBUG_HTTP, r->connection->log, 0, 339 | "[Etag] If-None-Match: \"%V\" // Hash: \"%V\"", &if_none_match.value, &r->headers_out.etag->value ); 340 | 341 | if ( ngx_strncmp(r->headers_out.etag->value.data, if_none_match.value.data, r->headers_out.etag->value.len) == 0 ) { 342 | r->headers_out.status = NGX_HTTP_NOT_MODIFIED; 343 | r->headers_out.content_type.len = 0; 344 | 345 | ngx_http_clear_content_length(r); 346 | ngx_http_clear_accept_ranges(r); 347 | } 348 | } 349 | 350 | } 351 | } 352 | 353 | return ngx_http_next_header_filter(r); 354 | } 355 | --------------------------------------------------------------------------------