Skip to content

Commit d20b712

Browse files
Francisco Miguel Bietedenji
Francisco Miguel Biete
authored andcommitted
feat(partial keys): Support partial keys to purge multiple keys.
Put an '*' at the end of your purge cache URL. e.g: proxy_cache_key $scheme$host$uri$is_args$args$cookie_JSESSIONID; curl -X PURGE https://example.com/pass* This will remove every cached page whose key cache starting with: httpsexample.com/pass* Be careful not passing any value for the values after the $uri, or put it at the end of your cache key. Signed-off-by: Francisco Miguel Biete <[email protected]> Signed-off-by: Francisco Miguel Biete <[email protected]> Resolved FRiCKLE#33 Resolved FRiCKLE#35
1 parent b6dace3 commit d20b712

File tree

3 files changed

+318
-7
lines changed

3 files changed

+318
-7
lines changed

README.md

+12
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,18 @@ uwsgi_cache_purge
9191
Sets area and key used for purging selected pages from `uWSGI`'s cache.
9292

9393

94+
95+
Partial Keys
96+
============
97+
Sometimes it's not possible to pass the exact key cache to purge a page. For example; when the content of a cookie or the params are part of the key.
98+
You can specify a partial key adding an asterisk at the end of the URL.
99+
100+
curl -X PURGE /page*
101+
102+
The asterisk must be the last character of the key, so you **must** put the $uri variable at the end.
103+
104+
105+
94106
Sample configuration (same location syntax)
95107
===========================================
96108
http {

ngx_cache_purge_module.c

+138-7
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,8 @@ ngx_int_t ngx_http_file_cache_purge(ngx_http_request_t *r);
113113

114114

115115
void ngx_http_cache_purge_all(ngx_http_request_t *r, ngx_http_file_cache_t *cache);
116+
void ngx_http_cache_purge_partial(ngx_http_request_t *r, ngx_http_file_cache_t *cache);
117+
ngx_int_t ngx_http_cache_purge_is_partial(ngx_http_request_t *r);
116118

117119
char *ngx_http_cache_purge_conf(ngx_conf_t *cf,
118120
ngx_http_cache_purge_conf_t *cpcf);
@@ -429,6 +431,14 @@ ngx_http_fastcgi_cache_purge_handler(ngx_http_request_t *r)
429431
if (cplcf->conf->purge_all) {
430432
ngx_http_cache_purge_all(r, cache);
431433
}
434+
else {
435+
if (ngx_http_cache_purge_is_partial(r)) {
436+
ngx_log_debug(NGX_LOG_DEBUG_HTTP, r->connection->log, 0,
437+
"http file cache purge with partial enabled");
438+
439+
ngx_http_cache_purge_partial(r, cache);
440+
}
441+
}
432442

433443
# if (nginx_version >= 8011)
434444
r->main->count++;
@@ -707,6 +717,14 @@ ngx_http_proxy_cache_purge_handler(ngx_http_request_t *r)
707717
if (cplcf->conf->purge_all) {
708718
ngx_http_cache_purge_all(r, cache);
709719
}
720+
else {
721+
if (ngx_http_cache_purge_is_partial(r)) {
722+
ngx_log_debug(NGX_LOG_DEBUG_HTTP, r->connection->log, 0,
723+
"http file cache purge with partial enabled");
724+
725+
ngx_http_cache_purge_partial(r, cache);
726+
}
727+
}
710728

711729
# if (nginx_version >= 8011)
712730
r->main->count++;
@@ -927,6 +945,14 @@ ngx_http_scgi_cache_purge_handler(ngx_http_request_t *r)
927945
if (cplcf->conf->purge_all) {
928946
ngx_http_cache_purge_all(r, cache);
929947
}
948+
else {
949+
if (ngx_http_cache_purge_is_partial(r)) {
950+
ngx_log_debug(NGX_LOG_DEBUG_HTTP, r->connection->log, 0,
951+
"http file cache purge with partial enabled");
952+
953+
ngx_http_cache_purge_partial(r, cache);
954+
}
955+
}
930956

931957
# if (nginx_version >= 8011)
932958
r->main->count++;
@@ -1170,6 +1196,14 @@ ngx_http_uwsgi_cache_purge_handler(ngx_http_request_t *r)
11701196
if (cplcf->conf->purge_all) {
11711197
ngx_http_cache_purge_all(r, cache);
11721198
}
1199+
else {
1200+
if (ngx_http_cache_purge_is_partial(r)) {
1201+
ngx_log_debug(NGX_LOG_DEBUG_HTTP, r->connection->log, 0,
1202+
"http file cache purge with partial enabled");
1203+
1204+
ngx_http_cache_purge_partial(r, cache);
1205+
}
1206+
}
11731207

11741208
# if (nginx_version >= 8011)
11751209
r->main->count++;
@@ -1202,6 +1236,59 @@ ngx_http_purge_file_cache_delete_file(ngx_tree_ctx_t *ctx, ngx_str_t *path)
12021236
return NGX_OK;
12031237
}
12041238

1239+
1240+
static ngx_int_t
1241+
ngx_http_purge_file_cache_delete_partial_file(ngx_tree_ctx_t *ctx, ngx_str_t *path)
1242+
{
1243+
u_char *key_partial;
1244+
u_char *key_in_file;
1245+
ngx_uint_t len;
1246+
ngx_flag_t remove_file = 0;
1247+
1248+
key_partial = ctx->data;
1249+
len = ngx_strlen(key_partial);
1250+
1251+
// if key_partial is empty always match, because is a *
1252+
if (len == 0) {
1253+
ngx_log_debug(NGX_LOG_DEBUG_HTTP, ctx->log, 0,
1254+
"empty key_partial, forcing deletion");
1255+
remove_file = 1;
1256+
}
1257+
else {
1258+
ngx_file_t file;
1259+
1260+
file.offset = file.sys_offset = 0;
1261+
file.fd = ngx_open_file(path->data, NGX_FILE_RDONLY, NGX_FILE_OPEN,
1262+
NGX_FILE_DEFAULT_ACCESS);
1263+
1264+
// I don't know if it's a good idea to use the ngx_cycle pool for this, but the request is not available here
1265+
key_in_file = ngx_pcalloc(ngx_cycle->pool, sizeof(u_char) * (len + 1));
1266+
1267+
// KEY: /proxy/passwd
1268+
// since we don't need the "KEY: " ignore 5 + 1 extra u_char from last intro
1269+
// Optimization: we don't need to read the full key only the n chars included in key_partial
1270+
ngx_read_file(&file, key_in_file, sizeof(u_char) * len,
1271+
sizeof(ngx_http_file_cache_header_t) + sizeof(u_char) * 6);
1272+
ngx_close_file(file.fd);
1273+
1274+
ngx_log_debug2(NGX_LOG_DEBUG_HTTP, ctx->log, 0,
1275+
"http cache file \"%s\" key read: \"%s\"", path->data, key_in_file);
1276+
1277+
if (ngx_strncasecmp(key_in_file, key_partial, len) == 0) {
1278+
ngx_log_debug(NGX_LOG_DEBUG_HTTP, ctx->log, 0,
1279+
"match found, deleting file \"%s\"", path->data);
1280+
remove_file = 1;
1281+
}
1282+
}
1283+
1284+
if (remove_file && ngx_delete_file(path->data) == NGX_FILE_ERROR) {
1285+
ngx_log_error(NGX_LOG_CRIT, ctx->log, ngx_errno,
1286+
ngx_delete_file_n " \"%s\" failed", path->data);
1287+
}
1288+
1289+
return NGX_OK;
1290+
}
1291+
12051292
ngx_int_t
12061293
ngx_http_cache_purge_access_handler(ngx_http_request_t *r)
12071294
{
@@ -1469,15 +1556,13 @@ ngx_http_cache_purge_handler(ngx_http_request_t *r)
14691556
# endif
14701557

14711558
cplcf = ngx_http_get_module_loc_conf(r, ngx_http_cache_purge_module);
1472-
if (cplcf->conf->purge_all) {
1473-
rc = NGX_OK;
1474-
}
1475-
else {
1559+
rc = NGX_OK;
1560+
if (!cplcf->conf->purge_all && !ngx_http_cache_purge_is_partial(r)) {
14761561
rc = ngx_http_file_cache_purge(r);
14771562

14781563
ngx_log_debug2(NGX_LOG_DEBUG_HTTP, r->connection->log, 0,
1479-
"http file cache purge: %i, \"%s\"",
1480-
rc, r->cache->file.name.data);
1564+
"http file cache purge: %i, \"%s\"",
1565+
rc, r->cache->file.name.data);
14811566
}
14821567

14831568
switch (rc) {
@@ -1578,13 +1663,59 @@ ngx_http_cache_purge_all(ngx_http_request_t *r, ngx_http_file_cache_t *cache) {
15781663
tree.pre_tree_handler = ngx_http_purge_file_cache_noop;
15791664
tree.post_tree_handler = ngx_http_purge_file_cache_noop;
15801665
tree.spec_handler = ngx_http_purge_file_cache_noop;
1581-
tree.data = cache;
1666+
tree.data = NULL;
15821667
tree.alloc = 0;
15831668
tree.log = ngx_cycle->log;
15841669

15851670
ngx_walk_tree(&tree, &cache->path->name);
15861671
}
15871672

1673+
void
1674+
ngx_http_cache_purge_partial(ngx_http_request_t *r, ngx_http_file_cache_t *cache) {
1675+
ngx_log_debug(NGX_LOG_DEBUG_HTTP, r->connection->log, 0,
1676+
"purge_partial http in %s",
1677+
cache->path->name.data);
1678+
1679+
u_char *key_partial;
1680+
ngx_str_t *key;
1681+
ngx_http_cache_t *c;
1682+
ngx_uint_t len;
1683+
1684+
c = r->cache;
1685+
key = c->keys.elts;
1686+
len = key[0].len;
1687+
1688+
// Only check the first key
1689+
key_partial = ngx_pcalloc(r->pool, sizeof(u_char) * len);
1690+
ngx_memcpy(key_partial, key[0].data, sizeof(u_char) * (len - 1));
1691+
1692+
// Walk the tree and remove all the files matching key_partial
1693+
ngx_tree_ctx_t tree;
1694+
tree.init_handler = NULL;
1695+
tree.file_handler = ngx_http_purge_file_cache_delete_partial_file;
1696+
tree.pre_tree_handler = ngx_http_purge_file_cache_noop;
1697+
tree.post_tree_handler = ngx_http_purge_file_cache_noop;
1698+
tree.spec_handler = ngx_http_purge_file_cache_noop;
1699+
tree.data = key_partial;
1700+
tree.alloc = 0;
1701+
tree.log = ngx_cycle->log;
1702+
1703+
ngx_walk_tree(&tree, &cache->path->name);
1704+
}
1705+
1706+
ngx_int_t
1707+
ngx_http_cache_purge_is_partial(ngx_http_request_t *r)
1708+
{
1709+
ngx_str_t *key;
1710+
ngx_http_cache_t *c;
1711+
1712+
c = r->cache;
1713+
key = c->keys.elts;
1714+
1715+
// Only check the first key
1716+
return key[0].data[key[0].len - 1] == '*';
1717+
}
1718+
15881719
char *
15891720
ngx_http_cache_purge_conf(ngx_conf_t *cf, ngx_http_cache_purge_conf_t *cpcf)
15901721
{

t/proxy4.t

+168
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
1+
# vi:filetype=perl
2+
3+
use lib 'lib';
4+
use Test::Nginx::Socket;
5+
6+
repeat_each(1);
7+
8+
plan tests => 41;
9+
10+
our $http_config = <<'_EOC_';
11+
proxy_cache_path /tmp/ngx_cache_purge_cache keys_zone=test_cache:10m;
12+
proxy_temp_path /tmp/ngx_cache_purge_temp 1 2;
13+
_EOC_
14+
15+
our $config = <<'_EOC_';
16+
location /proxy {
17+
proxy_pass $scheme://127.0.0.1:$server_port/etc/passwd;
18+
proxy_cache test_cache;
19+
proxy_cache_key $uri$is_args$args;
20+
proxy_cache_valid 3m;
21+
add_header X-Cache-Status $upstream_cache_status;
22+
23+
proxy_cache_purge PURGE from 1.0.0.0/8 127.0.0.0/8 3.0.0.0/8;
24+
}
25+
26+
27+
location = /etc/passwd {
28+
root /var/www/html;
29+
}
30+
_EOC_
31+
32+
worker_connections(128);
33+
no_shuffle();
34+
run_tests();
35+
36+
no_diff();
37+
38+
__DATA__
39+
40+
=== TEST 1: prepare passwd
41+
--- http_config eval: $::http_config
42+
--- config eval: $::config
43+
--- request
44+
GET /proxy/passwd
45+
--- error_code: 200
46+
--- response_headers
47+
Content-Type: text/plain
48+
--- response_body_like: root
49+
--- timeout: 10
50+
--- no_error_log eval
51+
qr/\[(warn|error|crit|alert|emerg)\]/
52+
53+
54+
=== TEST 2: prepare passwd2
55+
--- http_config eval: $::http_config
56+
--- config eval: $::config
57+
--- request
58+
GET /proxy/passwd2
59+
--- error_code: 200
60+
--- response_headers
61+
Content-Type: text/plain
62+
--- response_body_like: root
63+
--- timeout: 10
64+
--- no_error_log eval
65+
qr/\[(warn|error|crit|alert|emerg)\]/
66+
67+
68+
=== TEST 3: prepare shadow
69+
--- http_config eval: $::http_config
70+
--- config eval: $::config
71+
--- request
72+
GET /proxy/shadow
73+
--- error_code: 200
74+
--- response_headers
75+
Content-Type: text/plain
76+
--- response_body_like: root
77+
--- timeout: 10
78+
--- no_error_log eval
79+
qr/\[(warn|error|crit|alert|emerg)\]/
80+
81+
82+
=== TEST 4: get from cache passwd
83+
--- http_config eval: $::http_config
84+
--- config eval: $::config
85+
--- request
86+
GET /proxy/passwd
87+
--- error_code: 200
88+
--- response_headers
89+
Content-Type: text/plain
90+
X-Cache-Status: HIT
91+
--- response_body_like: root
92+
--- timeout: 10
93+
--- no_error_log eval
94+
qr/\[(warn|error|crit|alert|emerg)\]/
95+
96+
97+
=== TEST 5: get from cache passwd2
98+
--- http_config eval: $::http_config
99+
--- config eval: $::config
100+
--- request
101+
GET /proxy/passwd2
102+
--- error_code: 200
103+
--- response_headers
104+
Content-Type: text/plain
105+
X-Cache-Status: HIT
106+
--- response_body_like: root
107+
--- timeout: 10
108+
--- no_error_log eval
109+
qr/\[(warn|error|crit|alert|emerg)\]/
110+
111+
112+
=== TEST 6: purge from cache
113+
--- http_config eval: $::http_config
114+
--- config eval: $::config
115+
--- request
116+
PURGE /proxy/pass*
117+
--- error_code: 200
118+
--- response_headers
119+
Content-Type: text/html
120+
--- response_body_like: Successful purge
121+
--- timeout: 10
122+
--- no_error_log eval
123+
qr/\[(warn|error|crit|alert|emerg)\]/
124+
125+
126+
=== TEST 7: get from empty cache passwd
127+
--- http_config eval: $::http_config
128+
--- config eval: $::config
129+
--- request
130+
GET /proxy/passwd
131+
--- error_code: 200
132+
--- response_headers
133+
Content-Type: text/plain
134+
X-Cache-Status: MISS
135+
--- response_body_like: root
136+
--- timeout: 10
137+
--- no_error_log eval
138+
qr/\[(warn|error|crit|alert|emerg)\]/
139+
140+
141+
=== TEST 8: get from empty cache passwd2
142+
--- http_config eval: $::http_config
143+
--- config eval: $::config
144+
--- request
145+
GET /proxy/passwd2
146+
--- error_code: 200
147+
--- response_headers
148+
Content-Type: text/plain
149+
X-Cache-Status: MISS
150+
--- response_body_like: root
151+
--- timeout: 10
152+
--- no_error_log eval
153+
qr/\[(warn|error|crit|alert|emerg)\]/
154+
155+
156+
=== TEST 9: get from cache shadow
157+
--- http_config eval: $::http_config
158+
--- config eval: $::config
159+
--- request
160+
GET /proxy/shadow
161+
--- error_code: 200
162+
--- response_headers
163+
Content-Type: text/plain
164+
X-Cache-Status: HIT
165+
--- response_body_like: root
166+
--- timeout: 10
167+
--- no_error_log eval
168+
qr/\[(warn|error|crit|alert|emerg)\]/

0 commit comments

Comments
 (0)