/* ** Copyright (c) 2025 D. Richard Hipp ** ** This program is free software; you can redistribute it and/or ** modify it under the terms of the Simplified BSD License (also ** known as the "2-Clause License" or "FreeBSD License".) ** This program is distributed in the hope that it will be useful, ** but without any warranty; without even the implied warranty of ** merchantability or fitness for a particular purpose. ** ** Author contact information: ** drh@hwaci.com ** http://www.hwaci.com/drh/ ** ******************************************************************************* ** ** This file contains code that attempts to prevent robots and ** especially bot-nets from consume excess CPU and bandwidth when ** Fossil is run as a service. */ #include "config.h" #include "robot.h" #include #include /* ** The name of the cookie used to demonstrate that the client has been ** tested and is believed to be operated by a human, not by a robot. */ #if INTERFACE #define ROBOT_COOKIE "fossil-client-ok" #endif /* ** Rewrite the current page with a robot squelch captcha and return 1. ** ** Or, if valid proof-of-work is present as either a query parameter or ** as a cookie, then return 0. */ static int robot_proofofwork(void){ sqlite3_int64 tm; unsigned h1, h2; int k; const char *z; const char *az[2]; /* Construct a proof-of-work value based on the IP address of the ** sender and the sender's user-agent string. The current time also ** affects the pow value, so actually compute two values, one for the ** current 900-second interval and one for the previous. Either can ** match. The pow-value is an integer between 100,000,000 and ** 999,999,999. */ az[0] = P("REMOTE_ADDR"); az[1] = P("HTTP_USER_AGENT"); tm = time(0); h1 = (unsigned)((tm&0xffffffff) / 900); h2 = h1 - 1; for(k=0; k<2; k++){ z = az[k]; if( z==0 ) continue; while( *z ){ h1 = (h1 + *(unsigned char*)z)*0x9e3779b1; h2 = (h2 + *(unsigned char*)z)*0x9e3779b1; z++; } } h1 = (h1 % 900000000) + 100000000; h2 = (h2 % 900000000) + 100000000; /* If there is already a proof-of-work cookie with this value ** that means that the user agent has already authenticated. */ z = P(ROBOT_COOKIE); if( z && (atoi(z)==h1 || atoi(z)==h2) && !cgi_is_qp(ROBOT_COOKIE) ){ return 0; } /* Check for a proof query parameter. If found, that means that ** the captcha has just now passed, so set the proof-of-work cookie ** in addition to letting the request through. */ z = P("proof"); if( z && (atoi(z)==h1 || atoi(z)==h2) ){ cgi_set_cookie(ROBOT_COOKIE,z,"/",900); return 0; } cgi_tag_query_parameter("proof"); /* Ask the client to present proof-of-work */ cgi_reset_content(); cgi_set_content_type("text/html"); style_header("Browser Verification"); @

Checking to see if you are a robot

@
@ cgi_query_parameters_to_hidden(); @ @
@ style_finish_page(); return 1; } /* ** SETTING: robot-restrict width=40 block-text ** The VALUE of this setting is a list of GLOB patterns that match ** pages for which complex HTTP requests from unauthenicated clients ** should be disallowed. "Unauthenticated" means the user is "nobody". ** The recommended value for this setting is: ** ** timelineX,diff,annotate,zip,fileage,file ** ** The "diff" tag covers all diffing pages such as /vdiff, /fdiff, and ** /vpatch. The "annotate" tag also covers /blame and /praise. "zip" ** also covers /tarball and /sqlar. If a tag has an "X" character appended, ** then it only applies if query parameters are such that the page is ** particularly difficult to compute. ** ** In all other case, the tag should exactly match the page name. */ /* ** Return the default restriction GLOB */ const char *robot_restrict_default(void){ return "timelineX,diff,annotate,zip,fileage,file"; } /* ** Check to see if the page named in the argument is on the ** robot-restrict list. If it is on the list and if the user ** is "nobody" then bring up a captcha to test to make sure that ** client is not a robot. ** ** This routine returns true if a captcha was rendered and if subsequent ** page generation should be aborted. It returns false if the page ** should not be restricted and should be rendered normally. */ int robot_restrict(const char *zPage){ const char *zGlob; const char *zToken; static int bKnownPass = 0; if( g.zLogin ) return 0; /* Logged in users always get through */ if( bKnownPass ) return 0; /* Already known to pass robot restrictions */ zGlob = db_get("robot-restrict",robot_restrict_default()); if( zGlob==0 || zGlob[0]==0 ){ bKnownPass = 1; return 0; } if( !glob_multi_match(zGlob, zPage) ) return 0; zToken = P("token"); if( zToken!=0 && db_exists("SELECT 1 FROM config WHERE name='token-%q'", zToken) ){ bKnownPass = 1; return 0; /* There is a valid token= query parameter */ } if( robot_proofofwork() ){ /* A captcha was generated. Abort this page. A redirect will occur ** if the captcha passes. */ return 1; } bKnownPass = 1; return 0; } /* ** WEBPAGE: test-robotck ** ** Run the robot_restrict() function using the value of the "name=" ** query parameter as an argument. Used for testing the robot_restrict() ** logic. ** ** Whenever this page is successfully rendered (when it doesn't go to ** the captcha) it deletes the proof-of-work cookie. So reloading the ** page will reset the cookie and restart the verification. */ void robot_restrict_test_page(void){ const char *zName = P("name"); const char *zP1 = P("proof"); const char *zP2 = P(ROBOT_COOKIE); const char *z; if( zName==0 || zName[0]==0 ) zName = g.zPath; login_check_credentials(); if( g.zLogin==0 ){ login_needed(1); return; } g.zLogin = 0; if( robot_restrict(zName) ) return; style_set_current_feature("test"); style_header("robot_restrict() test"); @

Captcha passed

@ @

if( zP1 && zP1[0] ){ @ proof=%h(zP1)
} if( zP2 && zP2[0] ){ @ %h(ROBOT_COOKIE)=%h(zP2)
cgi_set_cookie(ROBOT_COOKIE,"",0,-1); } z = db_get("robot-restrict",robot_restrict_default()); if( z && z[0] ){ @ robot-restrict=%h(z)
} @

@

Retry style_finish_page(); }