TPSGI.pm 47 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505
  1. package TPSGI;
  2. # Abstract: Abstraction layer around starman to become a general purpose HTTP server & static renderer.
  3. use strict;
  4. use warnings;
  5. use feature qw{state};
  6. use FindBin;
  7. use Carp::Always;
  8. use UUID();
  9. use POSIX();
  10. use Mojo::File;
  11. use IO::Compress::Gzip;
  12. use Time::HiRes qw{usleep gettimeofday tv_interval};
  13. # For CGI features
  14. use HTTP::Body;
  15. use HTTP::Parser::XS qw{HEADERS_AS_HASHREF};
  16. use CGI::Emulate::PSGI;
  17. use Date::Format qw{strftime};
  18. use List::Util();
  19. use File::Find;
  20. use Sys::Hostname();
  21. use DateTime::Format::HTTP();
  22. use URL::Encode();
  23. use File::Touch;
  24. use File::Path;
  25. use File::Copy;
  26. use Cwd qw{abs_path};
  27. use File::Basename qw{dirname basename};
  28. use Module::Runtime();
  29. use Log::Dispatch;
  30. use Log::Dispatch::Screen;
  31. use Log::Dispatch::FileRotate;
  32. use Config::Simple;
  33. use TPSGI::Startup;
  34. # We have a DEBUG var which is plus ultra for extra sensitive stuff beyond just passing verbose
  35. our $debug = $ENV{TPSGI_DEBUG};
  36. #1MB chunks
  37. our $CHUNK_SEP = 'perlfsSep666YOLO42069';
  38. our $CHUNK_SIZE = 1024000;
  39. our %content_types = (
  40. text => "text/plain",
  41. html => "text/html",
  42. json => "application/json",
  43. blob => "application/octet-stream",
  44. xml => "text/xml",
  45. xsl => "text/xsl",
  46. css => "text/css",
  47. rss => "application/rss+xml",
  48. email => "multipart/related",
  49. );
  50. our %byct = reverse %content_types;
  51. our %cache_control = (
  52. revalidate => "no-cache, max-age=0",
  53. nocache => "no-store",
  54. static => "public, max-age=604800, immutable",
  55. );
  56. #TODO consider integrating libfile
  57. #Stuff that isn't in upstream finders
  58. my %extra_types = (
  59. '.docx' => 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
  60. );
  61. my $ct = 'Content-type';
  62. #memoize
  63. my $rq;
  64. my $generic_handler;
  65. sub request_id {
  66. my ( $self, $regenerate ) = @_;
  67. return $self->{uuid} if $self->{uuid} && !$regenerate;
  68. $self->{uuid} = UUID::uuid();
  69. return $self->{uuid};
  70. }
  71. sub log {
  72. my $self = shift;
  73. state $log;
  74. return $log //= $self->_build_log();
  75. }
  76. sub _build_log {
  77. my $self = shift;
  78. my $LOGNAME = $self->{log_name};
  79. my $LOGDIR = $self->{log_dir};
  80. File::Path::make_path($LOGDIR) unless -d $LOGDIR;
  81. File::Touch::touch($LOGNAME) unless -f $LOGNAME;
  82. my $LEVEL = $self->{verbose} ? 'debug' : 'info';
  83. # By default only log requests & warnings.
  84. # Otherwise emit debug messages.
  85. my $rotate = Log::Dispatch::FileRotate->new(
  86. name => 'tcms',
  87. filename => $LOGNAME,
  88. min_level => $LEVEL,
  89. 'mode' => 'append',
  90. size => 10 * 1024 * 1024,
  91. max => 6,
  92. );
  93. # Only send fatal events/errors to stdout
  94. my $screen = Log::Dispatch::Screen->new(
  95. name => 'screen',
  96. min_level => $LEVEL,
  97. );
  98. my $log = Log::Dispatch->new();
  99. $log->add($rotate);
  100. $log->add($screen);
  101. # The first line is logged while new() is still loading the routers, so a
  102. # logger that an application ships beside its router is not loaded yet.
  103. foreach my $logger ( @{ $self->{loggers} } ) {
  104. $log->add( Module::Runtime::use_module($logger)->new( min_level => $LEVEL, log_dir => $LOGDIR ) );
  105. }
  106. $log->info( $self->_log("Opening Log $LOGNAME at $LEVEL level") );
  107. return $log;
  108. }
  109. sub _log {
  110. my ( $self, $msg ) = @_;
  111. $msg //= "No message passed. This is almost certainly a bug. ";
  112. #XXX Log lines must start as an ISO8601 date, anything else breaks fail2ban's beautiful mind
  113. my $tstamp = POSIX::strftime "%Y-%m-%dT%H:%M:%SZ", gmtime;
  114. my $uuid = $self->request_id();
  115. my $udata = $self->{user} ? "[$self->{user}]" : "[nobody]";
  116. return "[Worker $$] {Request $uuid} $udata $tstamp : $self->{ip} $msg\n";
  117. }
  118. # Logger short cuts
  119. sub DEBUG {
  120. my $self = shift;
  121. $self->log->debug( $self->_log(shift) );
  122. }
  123. sub INFO {
  124. my $self = shift;
  125. $self->log->info( $self->_log(shift) );
  126. }
  127. sub NOTE {
  128. my $self = shift;
  129. $self->log->notice( $self->_log(shift) );
  130. }
  131. sub WARN {
  132. my $self = shift;
  133. $self->log->warning( $self->_log(shift) );
  134. }
  135. sub ERROR {
  136. my $self = shift;
  137. $self->log->error( $self->_log(shift) );
  138. }
  139. sub CRIT {
  140. my $self = shift;
  141. $self->log->critical( $self->_log(shift) );
  142. }
  143. sub ALERT {
  144. my $self = shift;
  145. $self->log->alert( $self->_log(shift) );
  146. }
  147. sub EMERG {
  148. my $self = shift;
  149. $self->log->emergency( $self->_log(shift) );
  150. }
  151. sub FATAL {
  152. my $self = shift;
  153. $self->log->log_and_die( level => 'emergency', message => $self->_log(shift) );
  154. }
  155. =head1 new(%options)
  156. Options are:
  157. custom_log
  158. verbose
  159. routers
  160. loggers
  161. auth
  162. =cut
  163. sub new {
  164. my ( $class, %options ) = @_;
  165. # Refuse to run as wrong user, all config will be borked otherwise
  166. my $pname = getpwuid($>);
  167. die "Must run as configured user (got: $pname, want: $options{user})!" unless $pname eq ( $options{user} // '' );
  168. die "Must set http_user in options" unless $options{http_user};
  169. die "Must set tpsgi_dir in options" unless $options{tpsgi_dir};
  170. my $gid = getgrnam( $options{http_user} );
  171. die "No such user $options{http_user}" unless $gid;
  172. $options{gid} = $gid;
  173. my $self = bless( \%options, $class );
  174. $self->{ip} = '0.0.0.0';
  175. my @routes;
  176. my %aliases;
  177. # XXX TODO make these routes able to be fully qualified namespaces (::)!
  178. no strict 'refs';
  179. foreach my $route ( @{ $self->{routers} } ) {
  180. die "No such routing module $route" unless -f "$options{tpsgi_dir}/$route";
  181. # The router needs to exist and have nonzero numbers of routes.
  182. my ($package) = basename($route) =~ m/(\S+)\.pm$/;
  183. my $r = "$package\:\:routes";
  184. my $a = "$package\:\:aliases";
  185. my $w = "$package\:\:watches";
  186. # It also should be a top-level namespace, not somewhere deep down. KISS.
  187. my $libdir = dirname("$options{tpsgi_dir}/$route");
  188. push( @INC, $libdir );
  189. local $@;
  190. $self->DEBUG("require $options{tpsgi_dir}/$route");
  191. my $success = eval { require "$options{tpsgi_dir}/$route"; 1; };
  192. if ($success) {
  193. my $pkg_routes = *$r{ARRAY};
  194. for ( my $i = 0; $i < @$pkg_routes; $i += 2 ) {
  195. $self->DEBUG("Registered route $pkg_routes->[$i]");
  196. }
  197. push( @routes, @$pkg_routes );
  198. my $pkg_aliases = *$a{HASH};
  199. foreach my $al ( keys(%$pkg_aliases) ) {
  200. $self->DEBUG("aliased route $al to $pkg_aliases->{$al}");
  201. }
  202. @aliases{ keys(%$pkg_aliases) } = values(%$pkg_aliases);
  203. # Anything the router wants to watch, along with what to do when it changes.
  204. # Registration is idempotent, which matters because new() runs per request.
  205. my $pkg_watches = *$w{HASH};
  206. foreach my $watched ( keys(%$pkg_watches) ) {
  207. my $spec = $pkg_watches->{$watched};
  208. # Copied rather than used directly, as we don't want to eat the router's own data.
  209. my %args = ref $spec eq 'HASH' ? %$spec : ( callback => $spec );
  210. $self->add_watch( $watched, delete $args{callback}, %args );
  211. }
  212. # First-come first-served error template overrides
  213. $generic_handler = "$package\:\:generic_route";
  214. $generic_handler = undef unless exists &{$generic_handler};
  215. }
  216. else {
  217. die "Could not load $route!\n$@\n";
  218. }
  219. }
  220. use strict 'refs';
  221. # Set the pattern used to discover the route if needed later.
  222. for ( my $i = 0; $i < scalar(@routes); $i += 2 ) {
  223. $routes[ $i + 1 ]{pattern} = $routes[$i];
  224. }
  225. $self->{indices} //= [];
  226. $self->{indices} = [ @{ $self->{indices} }, qw{index.html index.htm index.cgi} ];
  227. $self->{routes} = \@routes;
  228. $self->{aliases} = \%aliases;
  229. $self->{callbacks} = [];
  230. return $self;
  231. }
  232. sub indices {
  233. my $self = shift;
  234. return $self->{indices};
  235. }
  236. sub routes {
  237. my $self = shift;
  238. return $self->{routes};
  239. }
  240. =head2 serve
  241. Serve a file, with options to stream and cache the output.
  242. =cut
  243. sub serve {
  244. my ( $self, $fullpath, $path, $start, $streaming, $ranges, $last_fetch, $deflate ) = @_;
  245. $last_fetch ||= 0;
  246. $deflate ||= 0;
  247. my $mf = Mojo::File->new($path);
  248. my $ext = '.' . $mf->extname();
  249. my $ft;
  250. if ($ext) {
  251. $ft = Plack::MIME->mime_type($ext) if $ext;
  252. $ft ||= $extra_types{$ext} if exists $extra_types{$ext};
  253. }
  254. $ft ||= $content_types{text};
  255. my @headers = ( $ct => $ft );
  256. #TODO use static Cache-Control for everything but JS/CSS?
  257. push( @headers, 'Cache-control' => $cache_control{revalidate} );
  258. push( @headers, 'Accept-Ranges' => 'bytes' );
  259. $self->DEBUG("FETCH $path");
  260. my $mt = ( stat($path) )[9];
  261. my $sz = ( stat(_) )[7];
  262. my @gm = gmtime($mt);
  263. my $now_string = strftime( "%a, %d %b %Y %H:%M:%S GMT", @gm );
  264. my $code = $mt > $last_fetch ? 200 : 304;
  265. push( @headers, "Last-Modified" => $now_string );
  266. push( @headers, 'Vary' => 'Accept-Encoding' );
  267. if ( open( my $fh, '<', $path ) ) {
  268. return $self->_range( $fullpath, $fh, $ranges, $sz, @headers ) if @$ranges && $streaming;
  269. # Transfer-encoding: chunked
  270. return sub {
  271. my $responder = shift;
  272. push( @headers, 'Content-Length' => $sz );
  273. my $writer = $responder->( [ $code, \@headers ] );
  274. while ( $fh->read( my $buf, $CHUNK_SIZE ) ) {
  275. $writer->write($buf);
  276. }
  277. close $fh;
  278. $writer->close;
  279. }
  280. if $streaming && $sz > $CHUNK_SIZE;
  281. #Return data in the event the caller does not support deflate
  282. if ( !$deflate ) {
  283. push( @headers, "Content-Length" => $sz );
  284. # Append server-timing headers
  285. my $tot = tv_interval($start) * 1000;
  286. push( @headers, 'Server-Timing' => "file;dur=$tot" );
  287. return [ $code, \@headers, $fh ];
  288. }
  289. #Compress everything less than 1MB
  290. push( @headers, "Content-Encoding" => "gzip" );
  291. my $dfh;
  292. IO::Compress::Gzip::gzip( $fh => \$dfh );
  293. print $IO::Compress::Gzip::GzipError if $IO::Compress::Gzip::GzipError;
  294. push( @headers, "Content-Length" => length($dfh) );
  295. # Copy to the statics if it's not already there
  296. my $static_path = $path;
  297. $static_path =~ s|^[/]*www/||;
  298. $static_path = "$self->{tpsgi_dir}/www/static/$static_path";
  299. if ( !-f $static_path ) {
  300. my $target_dir = dirname($static_path);
  301. $self->INFO("Copying $path to $static_path");
  302. if ( !-d $target_dir ) {
  303. File::Path::make_path( $target_dir, { user => $<, group => $self->{gid}, chmod => 0755 } ) or die "Could not make dir $target_dir";
  304. }
  305. # hardlink to save disk space
  306. link $path, $static_path;
  307. # TODO figure out cache invalidation, I guess check mtime/hash
  308. }
  309. $self->INFO("GET 200 $fullpath");
  310. # Append server-timing headers
  311. my $tot = tv_interval($start) * 1000;
  312. push( @headers, 'Server-Timing' => "file;dur=$tot" );
  313. return [ $code, \@headers, [$dfh] ];
  314. }
  315. $self->INFO("GET 403 $fullpath");
  316. return [ 403, [ $ct => $content_types{text} ], ["Forbidden"] ];
  317. }
  318. sub _range {
  319. my ( $self, $fullpath, $fh, $ranges, $sz, %headers ) = @_;
  320. # Set mode
  321. my $primary_ct = "Content-Type: $headers{'Content-type'}";
  322. my $is_multipart = scalar(@$ranges) > 1;
  323. if ($is_multipart) {
  324. $headers{'Content-type'} = "multipart/byteranges; boundary=$CHUNK_SEP";
  325. }
  326. my $code = 206;
  327. my $fc = '';
  328. # Calculate the content-length up-front. We have to fix unspecified lengths first, and reject bad requests.
  329. foreach my $range (@$ranges) {
  330. $range->[1] //= $sz - 1;
  331. $self->INFO("GET 416 $fullpath");
  332. return [ 416, [%headers], ["Requested range not satisfiable"] ] if $range->[0] > $sz || $range->[0] < 0 || $range->[1] < 0 || $range->[0] > $range->[1];
  333. }
  334. $headers{'Content-Length'} = List::Util::sum( map { my $arr = $_; $arr->[1] + 1, -$arr->[0] } @$ranges );
  335. #XXX Add the entity header lengths to the value - should hash-ify this to DRY
  336. if ($is_multipart) {
  337. foreach my $range (@$ranges) {
  338. $headers{'Content-Length'} += length("$fc--$CHUNK_SEP\n$primary_ct\nContent-Range: bytes $range->[0]-$range->[1]/$sz\n\n");
  339. $fc = "\n";
  340. }
  341. $headers{'Content-Length'} += length("\n--$CHUNK_SEP\--\n");
  342. $fc = '';
  343. }
  344. return sub {
  345. my $responder = shift;
  346. my $writer;
  347. foreach my $range (@$ranges) {
  348. $headers{'Content-Range'} = "bytes $range->[0]-$range->[1]/$sz" unless $is_multipart;
  349. $writer //= $responder->( [ $code, [%headers] ] );
  350. $writer->write("$fc--$CHUNK_SEP\n$primary_ct\nContent-Range: bytes $range->[0]-$range->[1]/$sz\n\n") if $is_multipart;
  351. $fc = "\n";
  352. my $len = List::Util::min( $sz, $range->[1] + 1 ) - $range->[0];
  353. $fh->seek( $range->[0], 0 );
  354. while ($len) {
  355. $fh->read( my $buf, List::Util::min( $len, $CHUNK_SIZE ) );
  356. $writer->write($buf);
  357. # Adjust for amount written
  358. $len = List::Util::max( $len - $CHUNK_SIZE, 0 );
  359. }
  360. }
  361. $fh->close();
  362. $writer->write("\n--$CHUNK_SEP--\n") if $is_multipart;
  363. $writer->close;
  364. };
  365. }
  366. sub _generic {
  367. my ( $type, $code, $query ) = @_;
  368. if ($generic_handler) {
  369. my $rname = "/$code";
  370. my $title = $type;
  371. no strict 'refs';
  372. return $generic_handler->( $rname, $code, $title, $query );
  373. }
  374. $type .= Carp::longmess() if $debug;
  375. return [ $code, [ $ct => $content_types{html} ], ["$type"] ];
  376. }
  377. =head2 redirect, redirect_permanent, see_also
  378. Redirects to the provided page.
  379. =cut
  380. sub redirect {
  381. my ( $self, $to ) = @_;
  382. $self->INFO("redirect: $to");
  383. return [ 302, [ "Location" => $to, "Content-Length" => 0 ], [''] ];
  384. }
  385. sub redirect_permanent {
  386. my ( $self, $to ) = @_;
  387. $self->INFO("permanent redirect: $to");
  388. return [ 301, [ "Location" => $to, "Content-Length" => 0 ], [''] ];
  389. }
  390. sub see_also {
  391. my ( $self, $to ) = @_;
  392. $self->INFO("see also: $to");
  393. return [ 303, [ "Location" => $to, "Content-Length" => 0 ], [''] ];
  394. }
  395. =head2 ok
  396. Return a generic HTTP 200 OK
  397. =cut
  398. sub ok {
  399. my ( $self, $query, $body ) = @_;
  400. $self->INFO("$query->{method} 200 $query->{fullpath}");
  401. $body //= 'Ok';
  402. return _generic( $body, 200, $query );
  403. }
  404. =head2 notfound, forbidden, badrequest, toolong, error
  405. If you need to return these HTTP errors, return these within a route:
  406. sub route {
  407. my ($self, $query) = @_;
  408. ...
  409. return $self->notfound($query);
  410. }
  411. =cut
  412. sub notfound {
  413. my ( $self, $query, $body ) = @_;
  414. $self->INFO("$query->{method} 404 $query->{fullpath}");
  415. $body //= 'Not Found';
  416. return _generic( $body, 404, $query );
  417. }
  418. sub forbidden {
  419. my ( $self, $query, $body ) = @_;
  420. $self->INFO("$query->{method} 403 $query->{fullpath}");
  421. $body //= 'Forbidden';
  422. return _generic( $body, 403, $query );
  423. }
  424. sub badrequest {
  425. my ( $self, $query, $body ) = @_;
  426. $self->INFO("$query->{method} 400 $query->{fullpath}");
  427. $body //= 'Bad Request';
  428. return _generic( $body, 400, $query );
  429. }
  430. sub toolong {
  431. my ( $self, $query, $body ) = @_;
  432. $self->INFO("$query->{method} 419 $query->{fullpath}");
  433. $body //= 'URI too long';
  434. return _generic( $body, 419, $query );
  435. }
  436. sub error {
  437. my ( $self, $query, $body ) = @_;
  438. my $method = $query->{method} // "?";
  439. my $fp = $query->{fullpath} // "?";
  440. $body //= 'Internal Server Error';
  441. $self->INFO("$method 500 $fp");
  442. return _generic( $body, 500, $query );
  443. }
  444. sub unavailable {
  445. my ( $self, $query, $body ) = @_;
  446. $self->INFO("$query->{method} 503 $query->{fullpath}");
  447. $body //= 'Service Unavailable';
  448. return _generic( $body, 503, $query );
  449. }
  450. my $cur_query = {};
  451. sub app {
  452. my $self = shift;
  453. return eval { _app( $self, @_ ) } || do {
  454. my $err = $@;
  455. my $env = shift;
  456. $env->{'psgi.errors'}->print($err) if $env->{'psgi.errors'};
  457. # psgi.errors is the server's stderr, which a daemonized server sends
  458. # nowhere unless it was started with --error-log. The log is the one
  459. # place that is always there, so the reason goes in it whole.
  460. $self->ERROR("Request failed: $err");
  461. # Redact the stack trace past line 1, it usually has things which should not be shown
  462. $cur_query->{message} = $err;
  463. $cur_query->{message} =~ s/\n.*//g if $cur_query->{message} && !$debug;
  464. return $self->error($cur_query);
  465. };
  466. }
  467. =head2 cannot_start($error, $env, %options) = ARRAYREF
  468. What to answer with when new() died, so there is no object to answer with.
  469. Usually a router that would not load: a missing module, a syntax error.
  470. The error goes to psgi.errors, and to the log by way of a stand-in built from
  471. the same options, so it lands in the same places a request's own errors do. If
  472. building the log is what failed -- a logger that will not load -- psgi.errors
  473. is all there is, and the answer is still a 500 rather than a second exception.
  474. The answer is a page of our own, not the router's error page: the router is
  475. what failed. With TPSGI_DEBUG set it carries the error itself, trace and all,
  476. for the same reason _generic() adds one; without, it says nothing a visitor
  477. could use to map the installation.
  478. The worker retires once it has answered, on a server that offers
  479. psgix.harakiri, which Starman does. A router that died while it compiled
  480. leaves its file in %INC as a failure, and every later require of it in this
  481. process says only "Attempt to reload ... aborted" -- so a load that failed
  482. once, to a race or a lock, would otherwise fail for every request the worker
  483. ever serves. A fresh worker tries again from nothing.
  484. =cut
  485. sub cannot_start {
  486. my ( $error, $env, %options ) = @_;
  487. $env->{'psgi.errors'}->print($error) if ref $env eq 'HASH' && $env->{'psgi.errors'};
  488. eval { bless( { %options, ip => '0.0.0.0' }, __PACKAGE__ )->ERROR("Could not start: $error"); 1 };
  489. $env->{'psgix.harakiri.commit'} = 1 if ref $env eq 'HASH' && $env->{'psgix.harakiri'};
  490. my $detail = '';
  491. if ($debug) {
  492. ( my $escaped = $error ) =~ s/([&<>"])/'&#' . ord($1) . ';'/ge;
  493. $detail = "<pre>$escaped</pre>";
  494. }
  495. my $page = "<!doctype html><html><head><meta charset=\"utf-8\"><title>Internal Server Error</title></head>"
  496. . "<body><h1>Internal Server Error</h1><p>The application could not start. The reason is in the server's log.</p>$detail</body></html>";
  497. return [ 500, [ $ct => $content_types{html} ], [$page] ];
  498. }
  499. my %etags;
  500. sub _app {
  501. my $self = shift;
  502. # Start the server timing clock
  503. my $start = [gettimeofday];
  504. $cur_query = {};
  505. my $env = shift;
  506. $self->{filehandle} = $env->{'psgix.io'} // *STDOUT;
  507. $self->{input} = $env->{'psgi.input'};
  508. # Setup the unique ID for the request
  509. $env->{REQUEST_ID} = $self->request_id(1);
  510. # Discard the path used in the log, it's too long and enough 4xx error code = ban
  511. return $self->toolong( { method => $env->{REQUEST_METHOD}, fullpath => '...' } ) if length( $env->{REQUEST_URI} ) > 2048;
  512. # Various stuff important for logging requests
  513. my $domain = $env->{HTTP_X_FORWARDED_HOST} || $env->{HTTP_HOST} // eval { Sys::Hostname::hostname() };
  514. my $path = $env->{PATH_INFO} || '/';
  515. # de-pooplicate the path
  516. $path =~ s|//|/|g;
  517. my $port = $env->{HTTP_X_FORWARDED_PORT} // $env->{HTTP_PORT};
  518. my $pport = defined $port ? ":$port" : "";
  519. my $scheme = $env->{'psgi.url_scheme'} // 'http';
  520. my $method = $env->{REQUEST_METHOD};
  521. # It's important that we log what the user ACTUALLY requested rather than the rewritten path later on.
  522. my $fullpath = "$scheme://$domain$pport$path";
  523. # So we can log it for fail2ban
  524. my $ip = $env->{HTTP_X_FORWARDED_FOR} || $env->{REMOTE_ADDR};
  525. # set the referer & ua to go into DB logs, but not logs in general.
  526. # The referer/ua largely has no importance beyond being a proto bug report for log messages.
  527. my $referer = $env->{HTTP_REFERER};
  528. my $ua = $env->{HTTP_UA};
  529. $cur_query = {
  530. route => $path,
  531. fullpath => $path,
  532. method => $method,
  533. ip => $ip,
  534. ua => $ua,
  535. referer => $referer,
  536. tpsgi => $self,
  537. };
  538. # Disallow any paths that are naughty - this appears to be done by starman automatically.
  539. #if ( index($path, '..') != -1 ) {
  540. # return $self->forbidden($cur_query);
  541. #}
  542. # Support aliased paths
  543. my $aliases = $self->{aliases};
  544. $path = $aliases->{$path} if exists $aliases->{$path};
  545. # Check eTags. If we don't know about it, just assume it's good and lazily fill the cache
  546. # XXX yes, this allows cache poisoning...but only for logged in users!
  547. # This also needs to be IN DB so that we coordinate properly across forks
  548. if ( $env->{HTTP_IF_NONE_MATCH} ) {
  549. $self->INFO("$env->{REQUEST_METHOD} 304 $fullpath");
  550. return [ 304, [], [''] ] if $env->{HTTP_IF_NONE_MATCH} eq ( $etags{ $env->{REQUEST_URI} } || '' );
  551. $etags{ $env->{REQUEST_URI} } = $env->{HTTP_IF_NONE_MATCH} unless exists $etags{ $env->{REQUEST_URI} };
  552. }
  553. # TODO: Actually do something with the language passed to the renderer
  554. my $lang = $env->{HTTP_ACCEPT_LANGUAGE};
  555. #TODO: Actually do something with the acceptable output formats in the renderer
  556. my $accept = $env->{HTTP_ACCEPT};
  557. my $last_fetch = 0;
  558. if ( $env->{HTTP_IF_MODIFIED_SINCE} ) {
  559. $last_fetch = DateTime::Format::HTTP->parse_datetime( $env->{HTTP_IF_MODIFIED_SINCE} )->epoch();
  560. }
  561. # Figure out if we want compression or not
  562. my $alist = $env->{HTTP_ACCEPT_ENCODING} || '';
  563. $alist =~ s/\s//g;
  564. my @accept_encodings;
  565. @accept_encodings = split( /,/, $alist );
  566. my $deflate = grep { 'gzip' eq $_ } @accept_encodings;
  567. # Set the IP of the request so we can fail2ban
  568. $self->{ip} = $env->{HTTP_X_FORWARDED_FOR} || $env->{REMOTE_ADDR} || $self->{ip};
  569. # Make sure this works further down the line
  570. $env->{REMOTE_ADDR} = $env->{HTTP_X_FORWARDED_FOR} if $env->{HTTP_X_FORWARDED_FOR};
  571. my $streaming = $env->{'psgi.streaming'};
  572. # If we have an actual route, just use it.
  573. my $r = $self->routes;
  574. my $route_index = List::Util::first { ( $r->[$_] // '' ) eq $path } 0 .. scalar(@$r);
  575. my $route_actual;
  576. $route_actual = $r->[ $route_index + 1 ] if defined($route_index);
  577. # Might be a regexed route. Sort reversed so we try the longest routes first.
  578. if ( !$route_actual && @$r ) {
  579. my $matched = List::Util::first {
  580. # Here's where you want to use Regexp::Debugger in the context of call.pl to debug routes... EX:
  581. # perl ./call.pl GET /path/to/route
  582. #print $r->[$_]."\n";
  583. $path =~ m/^$r->[$_]$/;
  584. }
  585. 0 .. scalar(@$r) - 1;
  586. $route_actual = $r->[ $matched + 1 ] if defined($matched);
  587. }
  588. return $self->route( $route_actual, $env, $fullpath, $path, $start, $last_fetch, $deflate, $domain, $port ) if $route_actual;
  589. # Do a case insensitive match, because osx and windows
  590. my $file_possible = $self->mangle_filename("www/$path");
  591. my $file_actual = $file_possible if -f $file_possible;
  592. # Dirindices
  593. if ( -d $file_possible ) {
  594. foreach my $index ( @{ $self->indices } ) {
  595. next if $file_actual;
  596. my $dirindex = $self->mangle_filename("www$path/$index");
  597. $dirindex =~ s|//|/|g;
  598. $file_actual = $dirindex if -f $dirindex;
  599. }
  600. }
  601. my $file_is_cgi = appears_executable($file_actual);
  602. return $self->cgi( $env, $fullpath, $file_actual, $last_fetch, $deflate ) if $file_is_cgi;
  603. $self->INFO( "Attempting to serve $fullpath [" . ( $file_possible // "" ) . "]" );
  604. my @ranges = parse_ranges($env);
  605. return $self->serve( $fullpath, $file_actual, $start, $streaming, \@ranges, $last_fetch, $deflate ) if $file_actual;
  606. return $self->notfound($cur_query);
  607. }
  608. sub mangle_filename {
  609. my ( $self, $path ) = @_;
  610. # XXX yes, you can have case sensitive OSX, but basically nobody enables that ever
  611. $path = lc($path) if List::Util::any { $^O eq $_ } qw{darwin MSWin32 dos};
  612. $self->DEBUG("Path mangled to $path");
  613. return $path;
  614. }
  615. sub parse_ranges {
  616. my $env = shift;
  617. # Handle HTTP range/streaming requests
  618. my $range = $env->{HTTP_RANGE} || "bytes=0-" if $env->{HTTP_RANGE} || $env->{HTTP_IF_RANGE};
  619. my @ranges;
  620. if ($range) {
  621. $range =~ s/bytes=//g;
  622. push(
  623. @ranges,
  624. map { [ split( /-/, $_ ) ]; } split( /,/, $range )
  625. );
  626. }
  627. return @ranges;
  628. }
  629. my @executable_extensions = qw{cgi sh exe pl php py};
  630. sub appears_executable {
  631. my $subj = shift;
  632. # Definitely not executable if it does not exist!
  633. return 0 unless $subj;
  634. return 0 unless -x $subj;
  635. my ($extension) = $subj =~ m/\.(\S+)$/;
  636. return 1 if !$extension;
  637. return 1 if grep { $extension eq $_ } @executable_extensions;
  638. return 0;
  639. }
  640. # Handle actual routes
  641. sub route {
  642. my ( $self, $route, $env, $fullpath, $path, $start, $last_fetch, $deflate, $domain, $port ) = @_;
  643. $self->DEBUG( 'Executing route ' . $route->{pattern} );
  644. $route->{method} = $env->{REQUEST_METHOD} if $route->{method} eq '*';
  645. my $content_type = $env->{CONTENT_TYPE} || 'text/html'; # If not set, that's the assumption.
  646. return $self->badrequest($cur_query) unless List::Util::any { $_ eq $env->{REQUEST_METHOD} } grep { defined $_ } ( 'HEAD', $route->{method} );
  647. my $callback;
  648. if ( exists $route->{callbacks}{'*'} ) {
  649. $callback = $route->{callbacks}{'*'};
  650. }
  651. else {
  652. return $self->badrequest($cur_query) unless exists $route->{callbacks}{$content_type};
  653. $callback = $route->{callbacks}{$content_type};
  654. }
  655. my $streaming = $env->{'psgi.streaming'};
  656. # Build the query data for passing to a route.
  657. # GET, POST, URI captures, then explicit data overrides.
  658. my $query = {};
  659. # If we're running in CGI mode, leave everything as-is.
  660. $query = $self->extract_query( $path, $route, $env ) unless $route->{env};
  661. # We failed validation if $query isn't a hashref.
  662. return $query if ref $query eq 'ARRAY';
  663. # allow this stuff to survive down to the end of some routes
  664. $query->{last_fetched} = $last_fetch;
  665. $query->{deflate} = $deflate;
  666. $query->{streaming} = $streaming;
  667. my @ranges = parse_ranges($env);
  668. $query->{ranges} = \@ranges;
  669. $query->{start} = $start;
  670. # Put things to tv_interval in here for Server-Timing
  671. $query->{fullpath} = $fullpath;
  672. $query->{method} = $route->{method};
  673. $query->{route} = $path;
  674. $query->{cookies} = $env->{HTTP_COOKIE};
  675. $query->{dnt} = $env->{HTTP_DNT};
  676. $query->{nosellinfo} = $env->{HTTP_SEC_GPC};
  677. $query->{port} = $port;
  678. $query->{scheme} = $env->{'psgi.url_scheme'} // 'http';
  679. $query->{method} = $env->{REQUEST_METHOD};
  680. $query->{lang} = $env->{HTTP_ACCEPT_LANGUAGE};
  681. $query->{accept} = $env->{HTTP_ACCEPT};
  682. $query->{has_query} = !!$env->{QUERY_STRING};
  683. $query->{domain} = $domain;
  684. $query->{dispatcher} = $route;
  685. $query->{ip} = $cur_query->{ip};
  686. $query->{ua} = $cur_query->{ua};
  687. $query->{referer} = $cur_query->{referer};
  688. # This allows for better error handlers if we die in the route.
  689. $cur_query = $query;
  690. # Setup the CGI vars they expect IF requested
  691. local %ENV = ( %ENV, CGI::Emulate::PSGI->emulate_environment($env) ) if $route->{env};
  692. # Emulate mod_unique_id
  693. $ENV{UNIQUE_ID} = UUID::uuid() if $route->{env};
  694. # Run any watch callbacks, and make this a truly 'dynamic' application if requested.
  695. $self->handle_changes();
  696. {
  697. my $output = $callback->( $self, $query );
  698. # If it's streaming, just hand it off.
  699. # It's up to the caller to handle things like running post-close callbacks in this event.
  700. return $output if ref $output eq 'CODE';
  701. die "$fullpath returned no or malformed data!" unless ref $output eq 'ARRAY' && @$output == 3;
  702. my $pport = defined $query->{port} ? ":$query->{port}" : "";
  703. my %headers = @{ $output->[1] };
  704. my $bytes = $headers{'Content-Length'} // '?';
  705. $self->INFO("$env->{REQUEST_METHOD} $output->[0] $bytes $fullpath");
  706. # Append server-timing headers if they aren't present
  707. my $tot = tv_interval($start) * 1000;
  708. push( @{ $output->[1] }, 'Server-Timing' => "app;dur=$tot" ) unless List::Util::any { $_ eq 'Server-Timing' } @{ $output->[1] };
  709. # In the event that we have post-close callbacks, go ahead and run them.
  710. return $self->stream_raw_psgi( $output, $query ) if @{ $self->{callbacks} };
  711. return $output;
  712. }
  713. }
  714. sub extract_query {
  715. my ( $self, $path, $route, $env ) = @_;
  716. my $query = URL::Encode::url_params_mixed( $env->{QUERY_STRING} ) if $env->{QUERY_STRING};
  717. #Actually parse the POSTDATA and dump it into the QUERY object if this is a POST
  718. if ( $env->{REQUEST_METHOD} eq 'POST' ) {
  719. my $body = HTTP::Body->new( $env->{CONTENT_TYPE}, $env->{CONTENT_LENGTH} );
  720. while ( $env->{'psgi.input'}->read( my $buf, $CHUNK_SIZE ) ) {
  721. $body->add($buf);
  722. }
  723. @$query{ keys( %{ $body->param } ) } = values( %{ $body->param } );
  724. @$query{ keys( %{ $body->upload } ) } = values( %{ $body->upload } );
  725. }
  726. if ( ref $route->{captures} eq 'ARRAY' ) {
  727. my @captures = $path =~ m/^$route->{pattern}$/;
  728. @$query{ @{ $route->{captures} } } = @captures;
  729. }
  730. if ( ref $route->{data} eq 'HASH' ) {
  731. @$query{ keys( %{ $route->{data} } ) } = values( %{ $route->{data} } );
  732. }
  733. # Now that we've parsed the query and know where we want to go,
  734. # we should (optionally) murder everything the route does not explicitly want, and validate what it does
  735. my $parameters = $route->{parameters};
  736. if ( ref $parameters eq 'HASH' && %$parameters ) {
  737. my @known_params = keys(%$parameters);
  738. for my $param (@known_params) {
  739. die "Invalid route definition for $path: parameter $param must correspond to a validation CODEREF." unless ref $parameters->{$param} eq 'CODE';
  740. # A missing parameter is not necessarily a problem.
  741. next unless $query->{$param};
  742. # But if we have it, and it's bad, nack it, so that scanners get fail2banned.
  743. $self->DEBUG("Rejected $path for bad query param $param");
  744. return $self->badrequest($query) unless $parameters->{$param}->( $query->{$param} );
  745. }
  746. # Without this logging will break.
  747. push( @known_params, qw{tpsgi ip ua user referer route dispatcher} );
  748. # Smack down passing of unnecessary fields; this catches bugs
  749. foreach my $field ( keys(%$query) ) {
  750. next if List::Util::any { $field eq $_ } @known_params;
  751. $self->WARN("Unexpected parameter $field passed");
  752. return $self->badrequest($query);
  753. }
  754. }
  755. return $query;
  756. }
  757. # Read until we are done with headers, then pass off the filehandle to PSGI for streaming.
  758. # TODO support compression
  759. sub cgi {
  760. my ( $self, $env, $fullpath, $file_actual, $last_fetch, $deflate ) = @_;
  761. $self->INFO("Handoff $fullpath to $file_actual for cgi exec");
  762. # Setup the CGI vars they expect
  763. local %ENV = ( %ENV, CGI::Emulate::PSGI->emulate_environment($env) );
  764. my $pid = open( my $out, '-|', "$file_actual" );
  765. my ( $code, $offset, %headers ) = extract_headers( $out, $last_fetch );
  766. $code //= 500;
  767. return sub {
  768. my $responder = shift;
  769. my $writer = $responder->( [ $code, [%headers] ] );
  770. while ( $out->read( my $buf, $CHUNK_SIZE ) ) {
  771. $writer->write($buf);
  772. }
  773. $writer->close;
  774. # Wait on the CGI script to do whatever it's doing after closing stdout
  775. close $out;
  776. waitpid( $pid, 0 );
  777. };
  778. }
  779. # Use when you just have a .cgi file you want to run without pipe-open
  780. # XXX this is extraordinarily unsafe, inefficient and kills workers.
  781. # XXX it also has an 'apache-ism' of just assigning the HTTP status line for you.
  782. # But sometimes you have to bite the bullet and do this to migrate stuff effectively.
  783. sub stream_raw_cgi {
  784. my ( $self, $cgi ) = @_;
  785. # Discard STDIN in favor of the HTTP body
  786. close(STDIN);
  787. *STDIN = $self->{input};
  788. # Similarly make 'print' do what CGIs expect
  789. my $fh = $self->{filehandle};
  790. select $fh;
  791. do($cgi);
  792. # We may or may not actually get here.
  793. close($fh) if $fh;
  794. exit 0;
  795. }
  796. # Used primarily when we have post-close callbacks
  797. sub stream_raw_psgi {
  798. my ( $self, $response, $data ) = @_;
  799. my $time_to_here = tv_interval( $data->{start} );
  800. $self->DEBUG("Routing took $time_to_here s");
  801. my $post_routing = [gettimeofday];
  802. my $fh = $self->{filehandle};
  803. print $fh "HTTP/1.1 $response->[0]\n";
  804. # Emit the rest of the headers
  805. foreach ( my $i = 0; $i < @{ $response->[1] }; $i += 2 ) {
  806. my $header = $response->[1][$i];
  807. my $value = $response->[1][ $i + 1 ];
  808. print $fh "$header: $value\n";
  809. }
  810. print $fh "\n";
  811. # Emit the body
  812. print $fh $response->[2][0];
  813. close($fh);
  814. $self->DEBUG( "Response written in " . ( tv_interval($post_routing) ) . " s" );
  815. while ( my $callback = shift @{ $self->{callbacks} } ) {
  816. if ( ref $callback eq 'CODE' ) {
  817. my $post_start = [gettimeofday];
  818. local $@;
  819. eval { $callback->() };
  820. $self->ERROR("Post-close callback encountered exception: $@") if $@;
  821. $self->DEBUG( "Post-close callback took " . ( tv_interval($post_start) ) . " s" );
  822. }
  823. }
  824. exit 0;
  825. }
  826. sub stream_raw_http {
  827. my ( $self, $data, $last_fetch, $to_fork, $callback, $error_handler ) = @_;
  828. my $time_to_here = tv_interval( $data->{start} );
  829. $self->DEBUG("Routing took $time_to_here s");
  830. my $post_routing = [gettimeofday];
  831. my $fh = $self->{filehandle};
  832. # The CGIs you are executing here SHOULD NOT emit a status line.
  833. # Most apache CGIs you find don't anyways, so this is usually not a big deal.
  834. print $fh "HTTP/1.1 200 OK\n";
  835. # Emit our server-timing header.
  836. print $fh "Server-Timing: " . $self->build_server_timing($data) . "\n"; #app;dur=".($time_to_here*1000)."\n";
  837. # We *must* fork because we can't rely on the child to close stdout.
  838. my $pid = _fork(
  839. sub {
  840. $self->DEBUG( "Forking child took" . ( tv_interval($post_routing) ) . " s" );
  841. local $@;
  842. eval { $to_fork->() };
  843. $self->ERROR("Raw HTTTP streaming encountered exception: $@") if $@;
  844. if ( ref $error_handler eq 'CODE' ) {
  845. local $@;
  846. eval { $error_handler->() };
  847. $self->ERROR("HTTP streaming error handler encountered exception: $@") if $@;
  848. }
  849. },
  850. $fh
  851. );
  852. waitpid( $pid, 0 );
  853. close($fh);
  854. $self->DEBUG( "Response written in " . ( tv_interval($post_routing) ) . " s" );
  855. if ( ref $callback eq 'CODE' ) {
  856. my $post_start = [gettimeofday];
  857. local $@;
  858. eval { $callback->() };
  859. $self->ERROR("Post-close callback encountered exception: $@") if $@;
  860. $self->DEBUG( "Post-close callback took " . ( tv_interval($post_start) ) . " s" );
  861. }
  862. exit 0;
  863. }
  864. sub _fork {
  865. my ( $callback, $fh ) = @_;
  866. select $fh;
  867. $| = 1;
  868. my $pid = fork();
  869. die "Could not fork child" unless defined $pid;
  870. if ( $pid == 0 ) {
  871. $callback->();
  872. exit 0;
  873. }
  874. select STDOUT;
  875. return $pid;
  876. }
  877. =head2 extract_headers($fh, $last_fetch)
  878. Extract the headers from a filehandle. Set the 'Last Modified' header appropriately based on the output of `stat` on the filehandle.
  879. Returns a code and parsed headers hash. Code will be 304 if the file hasn't changed since $last_fetch, otherwise will be the code in the parsed headers.
  880. =cut
  881. sub extract_headers {
  882. my ( $fh, $last_fetch, $is_ref ) = @_;
  883. my $headers = '';
  884. # NOTE: this is relying on while advancing the file pointer
  885. while (<$fh>) {
  886. last if $_ eq "\n";
  887. $headers .= $_;
  888. }
  889. #XXX still a toctou
  890. my $offset = $fh->tell();
  891. my ( undef, undef, $status, undef, $headers_parsed ) = HTTP::Parser::XS::parse_http_response( "$headers\n", HEADERS_AS_HASHREF );
  892. my $code = $status // 200;
  893. $headers_parsed //= {};
  894. if ( !$is_ref ) {
  895. my $mt = ( stat($fh) )[9];
  896. my @gm = gmtime($mt);
  897. my $now_string = strftime( "%a, %d %b %Y %H:%M:%S GMT", @gm );
  898. $code = $mt > $last_fetch ? $status : 304;
  899. $headers_parsed->{"Last-Modified"} = $now_string;
  900. }
  901. return ( $code, $offset, %$headers_parsed );
  902. }
  903. sub static {
  904. my ( $self, $fullpath, $path, $start, $streaming, $last_fetch ) = @_;
  905. $self->DEBUG("Rendering static for $path");
  906. # XXX because of psgi I can't just vomit the file directly
  907. if ( open( my $fh, '<', "statics/$path" ) ) {
  908. my ( $code, $offset, $headers_parsed ) = ( 200, undef, {} );
  909. ( $code, $offset, %$headers_parsed ) = extract_headers( $fh, $last_fetch );
  910. # Append server-timing headers
  911. my $tot = tv_interval($start) * 1000;
  912. $headers_parsed->{'Server-Timing'} = "static;dur=$tot";
  913. #XXX uwsgi just opens the file *again* when we already have a filehandle if it has a path.
  914. # starman by comparison doesn't violate the principle of least astonishment here.
  915. # This is probably a performance optimization, but makes the kind of micromanagement I need to do inconvenient.
  916. # As such, we will just return a stream.
  917. $self->INFO("GET 200 $headers_parsed->{'Content-Length'} $fullpath");
  918. return sub {
  919. my $responder = shift;
  920. #push(@headers, 'Content-Length' => $sz);
  921. my $writer = $responder->( [ $code, [%$headers_parsed] ] );
  922. while ( $fh->read( my $buf, $CHUNK_SIZE ) ) {
  923. $writer->write($buf);
  924. }
  925. close $fh;
  926. $writer->close;
  927. }
  928. if $streaming;
  929. return [ $code, [%$headers_parsed], $fh ];
  930. }
  931. return $self->forbidden( $self->{current_query} );
  932. }
  933. sub get_config { return TPSGI::Startup::get_config(@_) }
  934. # Convenience method to keep track of server-timing
  935. sub checkpoint {
  936. my ( $self, $data, $name ) = @_;
  937. $data->{checkpoints} //= [ [ start => $data->{start} ] ];
  938. push( @{ $data->{checkpoints} }, [ $name => [gettimeofday] ] );
  939. }
  940. sub build_server_timing {
  941. my ( $self, $data ) = @_;
  942. my @st;
  943. my $last_interval;
  944. foreach my $checkpoint ( @{ $data->{checkpoints} } ) {
  945. if ($last_interval) {
  946. my $dur = tv_interval( $last_interval, $checkpoint->[1] ) * 1000;
  947. push( @st, "$checkpoint->[0];dur=$dur" );
  948. }
  949. $last_interval = $checkpoint->[1];
  950. }
  951. my $tot = tv_interval( $data->{start} ) * 1000;
  952. push( @st, "tot;dur=$tot" );
  953. delete $data->{checkpoints};
  954. return join( ', ', @st );
  955. }
  956. =head2 add_post_close_callback()
  957. Add a thing to do after closing stdout; useful for housekeeping that doesn't need to hold up pageload.
  958. =cut
  959. sub add_post_close_callback {
  960. my ( $self, $cb ) = @_;
  961. push( @{ $self->{callbacks} }, $cb );
  962. }
  963. =head2 signal_restart_parent()
  964. Instruct tPSGI to reload after closing stdout.
  965. =cut
  966. sub signal_restart_parent {
  967. my ($self) = @_;
  968. $self->add_post_close_callback( \&_restart_parent );
  969. }
  970. # Instruct tPSGI to reload.
  971. sub _restart_parent {
  972. my $parent = getppid;
  973. kill 'HUP', $parent;
  974. }
  975. =head2 save_render($path, $extension, $body)
  976. Save a static render of the page.
  977. Good to queue after closing stdout.
  978. Passed file extension slapped on the end of the path.
  979. You can then serve them as statics with an appropriately done try_files block in nginx.
  980. =cut
  981. # fixup matrix
  982. my %fixup = (
  983. text => 'txt',
  984. blob => 'bin',
  985. );
  986. sub save_render {
  987. my ( $self, $path, $extension, $body ) = @_;
  988. return unless $path && $extension;
  989. # If this is an index, let's make it so
  990. if ( $path =~ m|/$| ) {
  991. $path = $path . 'index';
  992. }
  993. # Fixup the extension if needed.
  994. $extension = $fixup{$extension} if exists $fixup{$extension};
  995. my $path_fixed = $path;
  996. $path_fixed =~ s/\.\Q$extension\E$//;
  997. $path_fixed = "$path_fixed.$extension";
  998. my $path2file = "$self->{tpsgi_dir}/www/static/" . dirname($path_fixed);
  999. if ( !-d $path2file ) {
  1000. File::Path::make_path( $path2file, { user => $<, group => $self->{gid}, chmod => 0755 } ) or die "Could not create directory $path2file";
  1001. }
  1002. my $file = "$self->{tpsgi_dir}/www/static/$path_fixed";
  1003. my $verb = -f $file ? 'Overwrite' : 'Write';
  1004. $self->INFO("$verb $path as static/$path_fixed");
  1005. open( my $fh, '>', $file ) or die "Could not open $file for writing";
  1006. print $fh $body;
  1007. close $fh;
  1008. chmod( 0755, $file );
  1009. chown( $<, $self->{gid}, $file );
  1010. }
  1011. =head2 invalidate_render($path, $extension)
  1012. Remove an existing static render of a path.
  1013. =cut
  1014. sub invalidate_render {
  1015. my ( $self, $path, $extension ) = @_;
  1016. return unless $path && $extension;
  1017. # Fixup the extension if needed.
  1018. $extension = $fixup{$extension} if exists $fixup{$extension};
  1019. my $path_fixed = $path;
  1020. $path_fixed =~ s/\.\Q$extension\E$//;
  1021. $path_fixed = "$path_fixed.$extension";
  1022. my $file = "$self->{tpsgi_dir}/www/static/$path_fixed";
  1023. return unless -f $file;
  1024. $self->_invalidate($file);
  1025. }
  1026. sub _invalidate {
  1027. my ( $self, $file ) = @_;
  1028. $self->INFO("Delete $file");
  1029. unlink "$file";
  1030. }
  1031. =head2 invalidate_renders($extension)
  1032. Remove all existing static renders with the provided extension.
  1033. Useful when it's not easy to figure out what to re-render due to including templates in other templates, etc.
  1034. =cut
  1035. sub invalidate_renders {
  1036. my ( $self, $extension ) = @_;
  1037. File::Find::find(
  1038. {
  1039. wanted => sub {
  1040. my $object = $_;
  1041. $self->_invalidate($object) if ( -f $object && $object =~ m/\.\Q$extension\E$/ );
  1042. },
  1043. no_chdir => 1,
  1044. bydepth => 1,
  1045. },
  1046. "$self->{tpsgi_dir}/www/static/"
  1047. );
  1048. }
  1049. sub watch_for_changes { return TPSGI::Startup::watch_for_changes(@_) }
  1050. =head2 add_watch($path, $callback, %options)
  1051. Add an arbitrary file or directory to the inotify watchlist, along with a
  1052. callback to run when it changes. Returns the inotify watch descriptor.
  1053. The callback is invoked as:
  1054. $callback->( $tpsgi, $change );
  1055. ...where $change is a HASH reference describing what happened:
  1056. path => full path of the thing which changed
  1057. watched => the path the watch was registered against
  1058. events => ARRAY reference of event names, e.g. [ 'MODIFY' ]
  1059. name => entry within a watched directory, if the watch was on a directory
  1060. wd => inotify watch descriptor
  1061. mask => raw inotify event mask
  1062. Which makes the common case - blowing away a static render because the thing it
  1063. was generated from changed - look like:
  1064. $tpsgi->add_watch( "data/posts.json", sub {
  1065. my ( $tpsgi, $change ) = @_;
  1066. $tpsgi->invalidate_renders('html');
  1067. });
  1068. %options are passed through to TPSGI::Startup::add_watch, so you may also pass
  1069. 'events' (defaults to CREATE, MODIFY, DELETE and MOVE) and 'key' (an explicit
  1070. dedupe key for the callback).
  1071. Some caveats worth knowing about:
  1072. =over
  1073. =item * Watches live in the inotify instance tarbaby sets up before forking, and
  1074. are therefore shared by every worker. Whichever worker notices a change first
  1075. consumes it for all of them, so register the same callbacks in every worker
  1076. (declare them in your router module, which every worker loads) and make sure
  1077. they're safe to run in any worker.
  1078. =item * Changes are only noticed when a request comes in, since nothing is
  1079. sitting in a select loop over the inotify FD. Renders are invalidated promptly
  1080. in practice, but not instantly.
  1081. =item * Watching a directory rather than a single file is more robust, as
  1082. watching a file which is later replaced wholesale (the usual write-tempfile-then-
  1083. rename dance) only survives because we re-establish the watch afterwards. If
  1084. the file is deleted and not immediately recreated, the watch is gone for good.
  1085. =back
  1086. =cut
  1087. sub add_watch {
  1088. my ( $self, $path, $callback, %options ) = @_;
  1089. my $existing = TPSGI::Startup::is_watched($path);
  1090. # A watch we can't establish (the usual cause being a path which isn't there yet)
  1091. # shouldn't be fatal to the request which happened to be the one registering it.
  1092. local $@;
  1093. my $wd = eval {
  1094. TPSGI::Startup::add_watch(
  1095. %options,
  1096. path => $path,
  1097. callback => $callback,
  1098. );
  1099. };
  1100. if ( !defined $wd ) {
  1101. my $why = $@;
  1102. $why =~ s/\n.*//s if $why;
  1103. $self->WARN("Could not watch $path for changes: $why");
  1104. return undef;
  1105. }
  1106. $self->DEBUG("Watching $path for changes") unless $existing;
  1107. return $wd;
  1108. }
  1109. =head2 remove_watch($path)
  1110. Drop a path, and any callbacks registered against it, from the watchlist.
  1111. =cut
  1112. sub remove_watch {
  1113. my ( $self, $path ) = @_;
  1114. return TPSGI::Startup::remove_watch($path);
  1115. }
  1116. =head2 handle_changes
  1117. Drain the inotify queue, running the callbacks registered for whatever changed,
  1118. and restart tPSGI if any of the relevant libdir files changed (assuming
  1119. autoreload is on). Powers both the autorestart feature and app-registered
  1120. watches.
  1121. =cut
  1122. sub handle_changes {
  1123. my $self = shift;
  1124. my @changes = TPSGI::Startup::pending_changes();
  1125. return 0 unless @changes;
  1126. my $had_changes = 0;
  1127. my @rewatch;
  1128. foreach my $change (@changes) {
  1129. my $watch = $change->{watch};
  1130. # A watch descriptor we know nothing about belongs to a worker which registered
  1131. # watches we didn't. All we can sensibly do with those is the old behavior.
  1132. if ( !$watch ) {
  1133. $had_changes++ if $change->{path} =~ m/\.pm$/;
  1134. next;
  1135. }
  1136. $had_changes++ if $watch->{restart} && $change->{path} =~ m/\.pm$/;
  1137. foreach my $cb ( @{ $watch->{callbacks} } ) {
  1138. local $@;
  1139. my $ok = eval { $cb->{code}->( $self, $change ); 1 };
  1140. $self->WARN("Watch callback for $change->{path} died: $@") unless $ok;
  1141. }
  1142. # The kernel drops watches on files which get replaced or deleted, so put them back.
  1143. push( @rewatch, $change->{wd} ) if grep { $_ eq 'IGNORED' } @{ $change->{events} };
  1144. }
  1145. TPSGI::Startup::rewatch($_) foreach @rewatch;
  1146. return 0 unless $had_changes && $self->{autoreload};
  1147. # We don't have to clean up the wds, that is handled in the destructor for Inotify
  1148. $self->INFO("Relevant Change in libdirs detected, reloading\n");
  1149. $self->signal_restart_parent();
  1150. return 0;
  1151. }
  1152. =head2 restart_if_changes
  1153. Deprecated alias for handle_changes().
  1154. =cut
  1155. sub restart_if_changes { goto &handle_changes }
  1156. 1;