0 #include <ngx_config.h>
1 #include <ngx_core.h>
2 #include <ngx_http.h>
3 #include <ngx_crypt.h>
4 #ifdef __linux__
5 #include <sys/random.h>
6 #endif
7 #include "base64.c"
8 #include "sha256.c"
9
10 #define VERIFY_IP /* only allow one ip per challenge */
11 #define WORK 0x00005FFF
12 #define WORK_STR "0x00005FFF"
13
14 #if nginx_version < 1023000
15 #define LEGACY
16 #endif
17
18 #define shield_challenge_str "pow-shield-challenge"
19 ngx_str_t shield_challenge = {
20 sizeof(shield_challenge_str) - 1,
21 (u_char*)shield_challenge_str
22 };
23
24 #define shield_answer_str "pow-shield-answer"
25 ngx_str_t shield_answer = {
26 sizeof(shield_answer_str) - 1,
27 (u_char*)shield_answer_str
28 };
29
30 #define shield_id_str "pow-shield-id"
31 ngx_str_t shield_id = {
32 sizeof(shield_id_str) - 1,
33 (u_char*)shield_id_str
34 };
35
36 const char html_page[] =
37 "<!DOCTYPE html><html><head>"
38 "<meta charset=\"utf-8\"><title>PoW Shield</title>"
39 "</head><body>"
40 "<h1>PoW Shield</h1>"
41 "<noscript><p>Javascript required</p></noscript>"
42 "<p id=\"hash-rate\"></p>"
43 "<script>"
44 "function setCookie(cname, cvalue) {"
45 "document.cookie = cname + \"=\" + cvalue + \";"
46 "path=/;SameSite=Strict\";"
47 "}"
48 "function getCookie(cname) {"
49 "let name = cname + \"=\";"
50 "let ca = document.cookie.split(';');"
51 "for(let i = 0; i < ca.length; i++) {"
52 "let c = ca[i];"
53 "while (c.charAt(0) == ' ') {"
54 "c = c.substring(1);"
55 "}"
56 "if (c.indexOf(name) == 0) {"
57 "return c.substring(name.length, c.length);"
58 "}"
59 "}"
60 "return \"\";"
61 "}"
62 "var challenge = Uint8Array.from(atob(getCookie(\"pow-shield-challenge\")) + "
63 "\"\\0\\0\\0\\0\", c => c.charCodeAt(0));"
64 "var hash = 0;"
65 "var startTime = new Date();"
66 "function updateHashRate() {"
67 "let timeDiff = (new Date() - startTime)/1000;"
68 "document.getElementById(\"hash-rate\").innerHTML =\"Hash-rate : \" +"
69 "String(Math.round(hash/timeDiff)) +\" h/s\";"
70 "}"
71 "async function pow(data, i) {"
72 "let bufView = new Uint32Array(data.buffer);"
73 "bufView[8] = i;"
74 "while (bufView[8] + 1 > -1) {"
75 "let h = new Uint8Array(await "
76 "crypto.subtle.digest(\"SHA-256\", data));"
77 "let view32 = new Uint32Array(h.buffer);"
78 "if (view32[0] <= "WORK_STR") {"
79 "break;"
80 "}"
81 "hash++;"
82 "bufView[8]++;"
83 "}"
84 "setCookie(\"pow-shield-answer\", bufView[8]);"
85 "document.cookie = \"pow-shield-challenge=;"
86 "expires=Thu, 01 Jan 1970 00:00:00 UTC;"
87 "path=/;SameSite=Strict\";"
88 "window.location.reload();"
89 "}"
90 "setInterval(updateHashRate, 1000);"
91 "pow(challenge.slice(0), 0x80000000);"
92 "pow(challenge, 0);"
93 "setTimeout(updateHashRate, 100);"
94 "</script>"
95 "</body></html>"
96 ;
97
98 typedef struct {
99 ngx_http_complex_value_t *realm;
100 } ngx_http_powshield_loc_conf_t;
101
102
103 static ngx_int_t ngx_http_powshield_handler(ngx_http_request_t *r);
104 static void *ngx_http_powshield_create_loc_conf(ngx_conf_t *cf);
105 static char *ngx_http_powshield_merge_loc_conf(ngx_conf_t *cf,
106 void *parent, void *child);
107 static ngx_int_t ngx_http_powshield_init(ngx_conf_t *cf);
108
109 static ngx_command_t ngx_http_powshield_commands[] = {
110
111 { ngx_string("powshield"),
112 NGX_HTTP_MAIN_CONF|NGX_HTTP_SRV_CONF|NGX_HTTP_LOC_CONF
113 |NGX_HTTP_LMT_CONF|NGX_CONF_TAKE1,
114 ngx_http_set_complex_value_slot,
115 NGX_HTTP_LOC_CONF_OFFSET,
116 offsetof(ngx_http_powshield_loc_conf_t, realm),
117 NULL },
118
119 ngx_null_command
120 };
121
122 static ngx_http_module_t ngx_http_powshield_module_ctx = {
123 NULL, /* preconfiguration */
124 ngx_http_powshield_init, /* postconfiguration */
125 NULL, /* create main configuration */
126 NULL, /* init main configuration */
127 NULL, /* create server configuration */
128 NULL, /* merge server configuration */
129 ngx_http_powshield_create_loc_conf, /* create location configuration */
130 ngx_http_powshield_merge_loc_conf /* merge location configuration */
131 };
132
133
134 ngx_module_t ngx_http_powshield_module = {
135 NGX_MODULE_V1,
136 &ngx_http_powshield_module_ctx, /* module context */
137 ngx_http_powshield_commands, /* module directives */
138 NGX_HTTP_MODULE, /* module type */
139 NULL, /* init master */
140 NULL, /* init module */
141 NULL, /* init process */
142 NULL, /* init thread */
143 NULL, /* exit thread */
144 NULL, /* exit process */
145 NULL, /* exit master */
146 NGX_MODULE_V1_PADDING
147 };
148
149 #define CHALLENGE_DATA_LENGTH 32
150 #define TABLE_SIZE 131072 /* needs to be a power of 2 */
151 #define TABLE_BITS (TABLE_SIZE - 1)
152 #define EXPIRATION 50 /* seconds before expiration */
153 #define EXPIRATION_COMPLETED 600 /* seconds before expiration when completed */
154 #define MAX_USAGE 200 /* usage count before invalidity */
155 struct pow_challenge {
156 ngx_rbtree_node_t rbnode;
157 unsigned char data[CHALLENGE_DATA_LENGTH + sizeof(uint32_t)];
158 uint32_t id;
159 unsigned int used;
160 time_t created;
161 unsigned completed:1;
162 #ifdef VERIFY_IP
163 uint32_t ip;
164 #endif
165 };
166
167 struct pow_tree {
168 ngx_rbtree_t rbtree;
169 ngx_rbtree_node_t sentinel;
170 };
171
172 struct pow_tree* challenges = NULL;
173 ngx_pool_t *cf_pool = NULL;
174
175 uint32_t
176 fnv(void *buf, size_t len)
177 {
178 uint32_t hval = 0;
179 unsigned char *bp = (unsigned char *)buf;
180 unsigned char *be = bp + len;
181 while (bp < be) {
182 hval *= 0x01000193;
183 hval ^= (uint32_t)*bp++;
184 }
185 return hval;
186 }
187 #define FNV(X) fnv(X, sizeof(X))
188
189 uint32_t
190 powshield_get_ip(ngx_http_request_t *r)
191 {
192 struct sockaddr_in *sin;
193 #if (NGX_HAVE_INET6)
194 struct sockaddr_in6 *sin6;
195 #endif
196 switch (r->connection->sockaddr->sa_family) {
197 case AF_INET:
198 sin = (struct sockaddr_in *) r->connection->sockaddr;
199 return sin->sin_addr.s_addr;
200 #if (NGX_HAVE_INET6)
201 case AF_INET6:
202 sin6 = (struct sockaddr_in6 *) r->connection->sockaddr;
203 return FNV(sin6->sin6_addr.s6_addr);
204 #endif
205 }
206 return 0;
207 }
208
209 struct pow_challenge
210 powshield_new_challenge()
211 {
212 struct pow_challenge challenge = {0};
213 challenge.created = time(NULL);
214 #ifdef __linux__
215 if (getrandom(&challenge.id, sizeof(challenge.id), GRND_RANDOM) !=
216 sizeof(challenge.id))
217 challenge.id = rand();
218 if (getrandom(challenge.data, sizeof(challenge.data), GRND_RANDOM) !=
219 sizeof(challenge.data)) {
220 for (size_t i = 0; i < sizeof(challenge.data); i++) {
221 challenge.data[i] = rand();
222 }
223 }
224 #else
225 arc4random_buf(&challenge.id, sizeof(challenge.id));
226 arc4random_buf(challenge.data, sizeof(challenge.data));
227 #endif
228 return challenge;
229 }
230
231 static int
232 powshield_is_expired(struct pow_challenge *challenge, time_t now)
233 {
234 return -(now - challenge->created > (challenge->completed ?
235 EXPIRATION_COMPLETED : EXPIRATION));
236 }
237
238 static int
239 powshield_insert_challenge(struct pow_challenge challenge)
240 {
241 struct pow_challenge *cnode;
242 struct pow_tree *root = &challenges[challenge.id & TABLE_BITS];
243 ngx_rbtree_node_t *node;
244
245 cnode = ngx_palloc(cf_pool, sizeof(challenge));
246 *cnode = challenge;
247
248 node = &cnode->rbnode;
249 node->key = cnode->id;
250
251 ngx_rbtree_insert(&root->rbtree, node);
252 return 0;
253 }
254
255 static struct pow_challenge *
256 powshield_get_challenge(uint32_t id)
257 {
258 struct pow_challenge *n;
259 ngx_rbtree_t *rbtree;
260 ngx_rbtree_node_t *node, *sentinel;
261
262 rbtree = &challenges[id & TABLE_BITS].rbtree;
263 node = rbtree->root;
264 sentinel = rbtree->sentinel;
265
266 while (node != sentinel) {
267
268 n = (struct pow_challenge *) node;
269
270 if (id != (uint32_t)node->key) {
271 node = (id < node->key) ? node->left : node->right;
272 continue;
273 }
274
275 if (powshield_is_expired(n, time(NULL)))
276 return NULL;
277
278 return n;
279 }
280
281 return NULL;
282 }
283
284 static int
285 powshield_use_challenge(struct pow_challenge *challenge)
286 {
287 challenge->used++;
288 if (challenge->used >= MAX_USAGE || time(NULL) - challenge->created >
289 EXPIRATION_COMPLETED) {
290 memset(challenge, 0, sizeof(*challenge));
291 return -1;
292 }
293 return 0;
294 }
295
296 static int
297 powshield_verify_challenge(struct pow_challenge challenge, uint32_t answer)
298 {
299 unsigned char hash[SHA256_BLOCK_SIZE];
300 uint32_t *hash_u32 = (void*)hash;
301 memcpy(&challenge.data[CHALLENGE_DATA_LENGTH],
302 &answer, sizeof(answer));
303 sha256(challenge.data, sizeof(challenge.data), hash);
304 return -(*hash_u32 > WORK);
305 }
306
307 static int
308 powshield_setcookie(ngx_http_request_t *r, const char* name, const char *data,
309 char *buf, size_t buflen)
310 {
311 ngx_table_elt_t *v;
312 const char cookie[] = "%s=%s;path=/;SameSite=Strict";
313 size_t len;
314
315 v = ngx_list_push(&r->headers_out.headers);
316 if (v == NULL) {
317 return -1;
318 }
319 v->hash = rand();
320 v->key.len = sizeof("Set-Cookie") - 1;
321 v->key.data = (u_char *)"Set-Cookie";
322 len = snprintf(buf, buflen, cookie, name, data);
323 v->value.len = len;
324 v->value.data = (u_char *)buf;
325 return 0;
326 }
327
328 static u_char *
329 powshield_getcookie(ngx_http_request_t *r, ngx_str_t *name)
330 {
331 #ifdef LEGACY
332 ngx_str_t value;
333 ngx_int_t n;
334
335 n = ngx_http_parse_multi_header_lines(&r->headers_in.cookies, name,
336 &value);
337 if (n != NGX_OK) {
338 return NULL;
339 }
340 return value.data;
341 #else
342 ngx_str_t value;
343 ngx_table_elt_t *elt;
344 elt = ngx_http_parse_multi_header_lines(r, r->headers_in.cookie, name,
345 &value);
346 if (!elt) return NULL;
347 return value.data;
348 #endif
349 }
350
351 static ngx_int_t
352 ngx_http_powshield_handler(ngx_http_request_t *r)
353 {
354 ngx_http_powshield_loc_conf_t *alcf;
355 ngx_str_t realm;
356 ngx_buf_t *b;
357 ngx_chain_t out;
358 char buf[1024], base64[CHALLENGE_DATA_LENGTH * 3 + 64], idbuf[128];
359 const u_char *answer = NULL, *id;
360 uint32_t cid = 0;
361 int len, canswer = 0;
362
363 alcf = ngx_http_get_module_loc_conf(r, ngx_http_powshield_module);
364 if (alcf->realm == NULL) {
365 return NGX_DECLINED;
366 }
367
368 id = powshield_getcookie(r, &shield_id);
369 if (id) {
370 cid = atol((const char*)id);//, NULL, 10);
371 if (!cid) id = NULL;
372 }
373 if (id)
374 answer = powshield_getcookie(r, &shield_answer);
375 if (answer)
376 canswer = atoi((const char *)answer);
377 while (answer) {
378 struct pow_challenge *challenge = powshield_get_challenge(cid);
379 if (!challenge) break;
380 if (challenge->completed) {
381 #ifdef VERIFY_IP
382 if (challenge->ip != powshield_get_ip(r))
383 break;
384 #endif
385 if (powshield_use_challenge(challenge))
386 break;
387 return NGX_DECLINED;
388 }
389 if (powshield_verify_challenge(*challenge, canswer)) break;
390 challenge->completed = 1;
391 return NGX_DECLINED;
392 }
393 if (answer)
394 id = NULL;
395
396 if (ngx_http_complex_value(r, alcf->realm, &realm) != NGX_OK) {
397 return NGX_ERROR;
398 }
399
400 if (realm.len == 3 && ngx_strncmp(realm.data, "off", 3) == 0) {
401 return NGX_DECLINED;
402 }
403
404 if (!id || !powshield_getcookie(r, &shield_challenge)) {
405
406 struct pow_challenge challenge = powshield_new_challenge();
407 char idtmp[32];
408
409 len = base64_encode(challenge.data, CHALLENGE_DATA_LENGTH,
410 (unsigned char*)base64, sizeof(base64));
411 if (len == -1) {
412 return NGX_HTTP_INTERNAL_SERVER_ERROR;
413 }
414 snprintf(idtmp, sizeof(idtmp), "%u", challenge.id);
415 if (powshield_setcookie(r, shield_id_str, idtmp,
416 idbuf, sizeof(idbuf))) {
417 return NGX_HTTP_INTERNAL_SERVER_ERROR;
418 }
419 if (powshield_setcookie(r, shield_challenge_str, base64, buf,
420 sizeof(buf))) {
421 return NGX_HTTP_INTERNAL_SERVER_ERROR;
422 }
423 #ifdef VERIFY_IP
424 challenge.ip = powshield_get_ip(r);
425 #endif
426 powshield_insert_challenge(challenge);
427 }
428
429 r->headers_out.status = NGX_HTTP_OK;
430 r->headers_out.content_length_n = sizeof(html_page) - 1;
431 r->headers_out.content_type.len = sizeof("text/html") - 1;
432 r->headers_out.content_type.data = (u_char *) "text/html";
433
434 ngx_http_send_header(r);
435
436 b = ngx_pcalloc(r->pool, sizeof(ngx_buf_t));
437 if (b == NULL) {
438 ngx_log_error(NGX_LOG_ERR, r->connection->log, 0,
439 "Failed to allocate response buffer.");
440 return NGX_HTTP_INTERNAL_SERVER_ERROR;
441 }
442
443 b->pos = (u_char*)html_page;
444 b->last = (u_char*)html_page + sizeof(html_page);
445 b->memory = 1; /* content is in read-only memory */
446 b->last_buf = 1; /* there will be no more buffers in the request */
447
448 out.buf = b;
449 out.next = NULL;
450
451 return ngx_http_output_filter(r, &out);
452 }
453
454 static void *
455 ngx_http_powshield_create_loc_conf(ngx_conf_t *cf)
456 {
457 ngx_http_powshield_loc_conf_t *conf;
458
459 conf = ngx_pcalloc(cf->pool, sizeof(ngx_http_powshield_loc_conf_t));
460 if (conf == NULL) {
461 return NULL;
462 }
463
464 return conf;
465 }
466
467 static char *
468 ngx_http_powshield_merge_loc_conf(ngx_conf_t *cf, void *parent, void *child)
469 {
470 ngx_http_powshield_loc_conf_t *prev = parent;
471 ngx_http_powshield_loc_conf_t *conf = child;
472
473 if (conf->realm == NULL) {
474 conf->realm = prev->realm;
475 }
476
477 return NGX_CONF_OK;
478 }
479
480 static ngx_int_t
481 ngx_http_powshield_init(ngx_conf_t *cf)
482 {
483 ngx_http_handler_pt *h;
484 ngx_http_core_main_conf_t *cmcf;
485
486 cmcf = ngx_http_conf_get_module_main_conf(cf, ngx_http_core_module);
487
488 h = ngx_array_push(&cmcf->phases[NGX_HTTP_ACCESS_PHASE].handlers);
489 if (h == NULL) {
490 return NGX_ERROR;
491 }
492
493 *h = ngx_http_powshield_handler;
494
495 challenges = ngx_pcalloc(cf->pool, sizeof(*challenges) * TABLE_SIZE);
496 if (!challenges) {
497 return NGX_ERROR;
498 }
499 cf_pool = cf->pool;
500 for (int i = 0; i < TABLE_SIZE; i++) {
501 ngx_rbtree_init(&challenges[i].rbtree, &challenges[i].sentinel,
502 ngx_rbtree_insert_value);
503 }
504
505 return NGX_OK;
506 }
507