TPSGI.pm 66 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979198019811982198319841985198619871988198919901991199219931994199519961997199819992000200120022003200420052006200720082009201020112012201320142015201620172018201920202021202220232024202520262027202820292030
  1. package TPSGI;
  2. # ABSTRACT: Abstraction layer around starman to become a general purpose HTTP server & static renderer.
  3. use 5.040;
  4. use warnings FATAL => 'all';
  5. use re '/aasx';
  6. use Carp::Always;
  7. use UUID();
  8. use POSIX();
  9. use Mojo::File;
  10. use IO::Compress::Gzip;
  11. use Plack::MIME;
  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::Basename qw{dirname basename};
  26. use Module::Runtime();
  27. use Log::Dispatch;
  28. use Log::Dispatch::Screen;
  29. use Log::Dispatch::FileRotate;
  30. use TPSGI::Startup;
  31. use TPSGI::Router;
  32. # We have a DEBUG var which is plus ultra for extra sensitive stuff beyond just passing verbose
  33. our $debug = $ENV{TPSGI_DEBUG};
  34. #1MB chunks
  35. our $CHUNK_SEP = 'perlfsSep666YOLO42069';
  36. our $CHUNK_SIZE = 1024000;
  37. our %content_types = (
  38. text => "text/plain",
  39. html => "text/html",
  40. json => "application/json",
  41. blob => "application/octet-stream",
  42. xml => "text/xml",
  43. xsl => "text/xsl",
  44. css => "text/css",
  45. rss => "application/rss+xml",
  46. email => "multipart/related",
  47. );
  48. our %byct = reverse %content_types;
  49. our %cache_control = (
  50. revalidate => "no-cache, max-age=0",
  51. nocache => "no-store",
  52. static => "public, max-age=604800, immutable",
  53. );
  54. #TODO consider integrating libfile
  55. #Stuff that isn't in upstream finders
  56. my %extra_types = (
  57. '.docx' => 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
  58. );
  59. my $ct = 'Content-type';
  60. my $generic_handler;
  61. =head2 request_id($regenerate) = STRING
  62. The UUID of the request being answered, which every log line carries. A true
  63. $regenerate makes a new one, which app() does as each request comes in.
  64. =cut
  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. # The line each of the logging methods writes.
  72. my sub log_line {
  73. my ( $self, $msg ) = @_;
  74. $msg //= "No message passed. This is almost certainly a bug. ";
  75. #XXX Log lines must start as an ISO8601 date, anything else breaks fail2ban's beautiful mind
  76. my $tstamp = POSIX::strftime "%Y-%m-%dT%H:%M:%SZ", gmtime;
  77. my $uuid = $self->request_id();
  78. my $udata = $self->{user} ? "[$self->{user}]" : "[nobody]";
  79. return "[Worker $$] {Request $uuid} $udata $tstamp : $self->{ip} $msg\n";
  80. }
  81. =head2 log() = Log::Dispatch
  82. The process's Log::Dispatch, made by build_log() the first time it is asked
  83. for.
  84. =cut
  85. sub log { ## no critic (ProhibitBuiltinHomonyms) -- a method, which nothing calls as the builtin
  86. my $self = shift;
  87. state $log;
  88. return $log //= $self->build_log();
  89. }
  90. =head2 build_log() = Log::Dispatch
  91. A new Log::Dispatch from the options: the rotated file at C<log_name>, the
  92. screen, and each class that C<loggers> names, which is loaded by name. log()
  93. keeps the first one this makes.
  94. =cut
  95. sub build_log {
  96. my $self = shift;
  97. my $LOGNAME = $self->{log_name};
  98. my $LOGDIR = $self->{log_dir};
  99. File::Path::make_path($LOGDIR) unless -d $LOGDIR;
  100. File::Touch::touch($LOGNAME) unless -e $LOGNAME;
  101. my $LEVEL = $self->{verbose} ? 'debug' : 'info';
  102. # By default only log requests & warnings.
  103. # Otherwise emit debug messages.
  104. my $rotate = Log::Dispatch::FileRotate->new(
  105. name => 'tcms',
  106. filename => $LOGNAME,
  107. min_level => $LEVEL,
  108. 'mode' => 'append',
  109. size => 10_485_760, # 10 MiB
  110. max => 6,
  111. );
  112. # Only send fatal events/errors to stdout
  113. my $screen = Log::Dispatch::Screen->new(
  114. name => 'screen',
  115. min_level => $LEVEL,
  116. );
  117. my $log = Log::Dispatch->new();
  118. $log->add($rotate);
  119. $log->add($screen);
  120. # The first line is logged while new() is still loading the routers, so a
  121. # logger that an application ships beside its router is not loaded yet.
  122. foreach my $logger ( @{ $self->{loggers} } ) {
  123. $log->add( Module::Runtime::use_module($logger)->new( min_level => $LEVEL, log_dir => $LOGDIR ) );
  124. }
  125. $log->info( log_line( $self, "Opening Log $LOGNAME at $LEVEL level" ) );
  126. return $log;
  127. }
  128. =head2 DEBUG, INFO, NOTE, WARN, ERROR, CRIT, ALERT, EMERG, FATAL($message)
  129. Log $message at that level, stamped with the time, the worker, the request and
  130. the user. FATAL logs at the emergency level and then dies with the line.
  131. =cut
  132. sub DEBUG {
  133. my ( $self, $msg ) = @_;
  134. return $self->log->debug( log_line( $self, $msg ) );
  135. }
  136. sub INFO {
  137. my ( $self, $msg ) = @_;
  138. return $self->log->info( log_line( $self, $msg ) );
  139. }
  140. sub NOTE {
  141. my ( $self, $msg ) = @_;
  142. return $self->log->notice( log_line( $self, $msg ) );
  143. }
  144. sub WARN {
  145. my ( $self, $msg ) = @_;
  146. return $self->log->warning( log_line( $self, $msg ) );
  147. }
  148. sub ERROR {
  149. my ( $self, $msg ) = @_;
  150. return $self->log->error( log_line( $self, $msg ) );
  151. }
  152. sub CRIT {
  153. my ( $self, $msg ) = @_;
  154. return $self->log->critical( log_line( $self, $msg ) );
  155. }
  156. sub ALERT {
  157. my ( $self, $msg ) = @_;
  158. return $self->log->alert( log_line( $self, $msg ) );
  159. }
  160. sub EMERG {
  161. my ( $self, $msg ) = @_;
  162. return $self->log->emergency( log_line( $self, $msg ) );
  163. }
  164. sub FATAL {
  165. my ( $self, $msg ) = @_;
  166. return $self->log->log_and_die( level => 'emergency', message => log_line( $self, $msg ) );
  167. }
  168. =head2 new(%options)
  169. Load the routers, and build the TPSGI::Router of their C<%openapi> documents.
  170. It dies unless it runs as C<user>, and C<http_user> and C<tpsgi_dir> are set.
  171. It also dies when a router does not load, when its document is not one that
  172. TPSGI::Router accepts, and when a router still defines C<@routes> instead of
  173. C<%openapi>.
  174. new() runs for every request, and compiling a document takes much longer than
  175. answering one. So the router is built once in each worker, for each list of
  176. router files, and kept. A HUP replaces the workers, so a changed router is
  177. compiled again.
  178. get_config() returns the options, and among them are:
  179. custom_log
  180. verbose
  181. routers
  182. loggers
  183. auth
  184. =cut
  185. sub new {
  186. my ( $class, %options ) = @_;
  187. # Refuse to run as wrong user, all config will be borked otherwise
  188. my $pname = getpwuid($>);
  189. die "Must run as configured user (got: $pname, want: $options{user})!" unless $pname eq ( $options{user} // '' );
  190. die "Must set http_user in options" unless $options{http_user};
  191. die "Must set tpsgi_dir in options" unless $options{tpsgi_dir};
  192. my $gid = getgrnam( $options{http_user} );
  193. die "No such user $options{http_user}" unless $gid;
  194. $options{gid} = $gid;
  195. my $self = bless( \%options, $class );
  196. $self->{ip} = '0.0.0.0';
  197. my @documents;
  198. my %aliases;
  199. # XXX TODO make these routes able to be fully qualified namespaces (::)!
  200. foreach my $route ( @{ $self->{routers} } ) {
  201. die "No such routing module $route" unless -e "$options{tpsgi_dir}/$route";
  202. # The router needs to exist and have nonzero numbers of routes.
  203. my ($package) = basename($route) =~ m/(\S+)[.]pm$/;
  204. # It also should be a top-level namespace, not somewhere deep down. KISS.
  205. my $libdir = dirname("$options{tpsgi_dir}/$route");
  206. push( @INC, $libdir );
  207. local $@;
  208. $self->DEBUG("require $options{tpsgi_dir}/$route");
  209. my $success = eval { require "$options{tpsgi_dir}/$route"; 1; }; ## no critic (RequireBarewordIncludes) -- a router is named by its file
  210. if ($success) {
  211. my ( $pkg_openapi, $pkg_routes, $pkg_aliases, $pkg_watches ) = do {
  212. no strict 'refs'; # the router's own package variables, by name
  213. ( \%{"${package}::openapi"}, \@{"${package}::routes"}, \%{"${package}::aliases"}, \%{"${package}::watches"} );
  214. };
  215. die "$route defines \@routes and no %openapi. tPSGI routes by an OpenAPI 3 document now: see CUSTOM ROUTING in perldoc TPSGI.\n"
  216. if @$pkg_routes && !%$pkg_openapi;
  217. push( @documents, $route => $pkg_openapi ) if %$pkg_openapi;
  218. foreach my $al ( keys(%$pkg_aliases) ) {
  219. $self->DEBUG("aliased route $al to $pkg_aliases->{$al}");
  220. }
  221. @aliases{ keys(%$pkg_aliases) } = values(%$pkg_aliases);
  222. # Anything the router wants to watch, along with what to do when it changes.
  223. # Registration is idempotent, which matters because new() runs per request.
  224. foreach my $watched ( keys(%$pkg_watches) ) {
  225. my $spec = $pkg_watches->{$watched};
  226. # Copied rather than used directly, as we don't want to eat the router's own data.
  227. my %args = ref $spec eq 'HASH' ? %$spec : ( callback => $spec );
  228. $self->add_watch( $watched, delete $args{callback}, %args );
  229. }
  230. # First-come first-served error template overrides
  231. $generic_handler = $package->can('generic_route');
  232. }
  233. else {
  234. die "Could not load $route!\n$@\n";
  235. }
  236. }
  237. state %routers;
  238. my $router_key = join( "\0", $options{tpsgi_dir}, @{ $self->{routers} } );
  239. $self->{router} = $routers{$router_key} //= do {
  240. my $router = TPSGI::Router->new(@documents);
  241. $self->DEBUG("Registered route \U$_->{method}\E $_->{template}") foreach $router->routes();
  242. $router;
  243. };
  244. $self->{indices} //= [];
  245. $self->{indices} = [ @{ $self->{indices} }, qw{index.html index.htm index.cgi} ];
  246. $self->{aliases} = \%aliases;
  247. $self->{callbacks} = [];
  248. return $self;
  249. }
  250. =head2 indices() = ARRAYREF
  251. The directory index files, in the order they are tried: the C<indices> option,
  252. then F<index.html>, F<index.htm> and F<index.cgi>.
  253. =cut
  254. sub indices {
  255. my $self = shift;
  256. return $self->{indices};
  257. }
  258. =head2 routes() = TPSGI::Router
  259. The router that new() built from the C<%openapi> of every router.
  260. =cut
  261. sub routes {
  262. my $self = shift;
  263. return $self->{router};
  264. }
  265. =head2 serve($fullpath, $path, $start, $streaming, $ranges, $last_fetch, $deflate)
  266. Serve a file, with options to stream and cache the output.
  267. =cut
  268. sub serve { ## no critic (ProhibitManyArgs) -- routers call it with these, in this order
  269. my ( $self, $fullpath, $path, $start, $streaming, $ranges, $last_fetch, $deflate ) = @_;
  270. $last_fetch ||= 0;
  271. $deflate ||= 0;
  272. my $mf = Mojo::File->new($path);
  273. my $ext = '.' . $mf->extname();
  274. my $ft;
  275. if ($ext) {
  276. $ft = Plack::MIME->mime_type($ext) if $ext;
  277. $ft ||= $extra_types{$ext} if exists $extra_types{$ext};
  278. }
  279. $ft ||= $content_types{text};
  280. my @headers = ( $ct => $ft );
  281. #TODO use static Cache-Control for everything but JS/CSS?
  282. push( @headers, 'Cache-control' => $cache_control{revalidate} );
  283. push( @headers, 'Accept-Ranges' => 'bytes' );
  284. $self->DEBUG("FETCH $path");
  285. my $mt = ( stat($path) )[9];
  286. my $sz = ( stat(_) )[7];
  287. my @gm = gmtime($mt);
  288. my $now_string = strftime( "%a, %d %b %Y %H:%M:%S GMT", @gm );
  289. my $code = $mt > $last_fetch ? 200 : 304;
  290. push( @headers, "Last-Modified" => $now_string );
  291. push( @headers, 'Vary' => 'Accept-Encoding' );
  292. # The handle is the body of the response, so the server closes it.
  293. if ( open( my $fh, '<', $path ) ) { ## no critic (RequireBriefOpen)
  294. return $self->serve_ranges( $fh, $ranges, size => $sz, headers => {@headers}, fullpath => $fullpath ) if @$ranges && $streaming;
  295. # Transfer-encoding: chunked
  296. return sub {
  297. my $responder = shift;
  298. push( @headers, 'Content-Length' => $sz );
  299. my $writer = $responder->( [ $code, \@headers ] );
  300. while ( $fh->read( my $buf, $CHUNK_SIZE ) ) {
  301. $writer->write($buf);
  302. }
  303. close($fh) or $self->WARN("Could not close $path: $!");
  304. $writer->close;
  305. }
  306. if $streaming && $sz > $CHUNK_SIZE;
  307. #Return data in the event the caller does not support deflate
  308. if ( !$deflate ) {
  309. push( @headers, "Content-Length" => $sz );
  310. # Append server-timing headers
  311. my $tot = tv_interval($start) * 1000;
  312. push( @headers, 'Server-Timing' => "file;dur=$tot" );
  313. return [ $code, \@headers, $fh ];
  314. }
  315. #Compress everything less than 1MB
  316. push( @headers, "Content-Encoding" => "gzip" );
  317. my $dfh;
  318. IO::Compress::Gzip::gzip( $fh => \$dfh );
  319. print $IO::Compress::Gzip::GzipError if $IO::Compress::Gzip::GzipError;
  320. push( @headers, "Content-Length" => length($dfh) );
  321. # Copy to the statics if it's not already there
  322. my $static_path = $path;
  323. $static_path =~ s{^/*www/}{};
  324. $static_path = "$self->{tpsgi_dir}/www/static/$static_path";
  325. my $target_dir = dirname($static_path);
  326. if ( !-d $target_dir ) {
  327. File::Path::make_path( $target_dir, { user => $<, group => $self->{gid}, chmod => 0o755 } ) or die "Could not make dir $target_dir";
  328. }
  329. # hardlink to save disk space. One that is there already is this file.
  330. # TODO figure out cache invalidation, I guess check mtime/hash
  331. if ( link $path, $static_path ) {
  332. $self->INFO("Copying $path to $static_path");
  333. }
  334. elsif ( !$!{EEXIST} ) {
  335. $self->WARN("Could not link $path to $static_path: $!");
  336. }
  337. $self->INFO("GET 200 $fullpath");
  338. # Append server-timing headers
  339. my $tot = tv_interval($start) * 1000;
  340. push( @headers, 'Server-Timing' => "file;dur=$tot" );
  341. return [ $code, \@headers, [$dfh] ];
  342. }
  343. $self->INFO("GET 403 $fullpath");
  344. return [ 403, [ $ct => $content_types{text} ], ["Forbidden"] ];
  345. }
  346. =head2 serve_ranges($fh, $ranges, %args) = PSGI response
  347. Answer a Range request from $fh with a 206, or a 416 when a range is outside
  348. the file. $ranges is what parse_ranges() returns, and %args is C<size>, the
  349. size of the file, C<headers>, a HASH reference of the response headers so far,
  350. and C<fullpath>, the URL for the log. More than one range is answered as
  351. multipart/byteranges.
  352. =cut
  353. sub serve_ranges {
  354. my ( $self, $fh, $ranges, %args ) = @_;
  355. my $sz = $args{size};
  356. my %headers = %{ $args{headers} };
  357. my $fullpath = $args{fullpath};
  358. # Set mode
  359. my $primary_ct = "Content-Type: $headers{'Content-type'}";
  360. my $is_multipart = scalar(@$ranges) > 1;
  361. if ($is_multipart) {
  362. $headers{'Content-type'} = "multipart/byteranges; boundary=$CHUNK_SEP";
  363. }
  364. my $code = 206;
  365. my $fc = '';
  366. # Calculate the content-length up-front. We have to fix unspecified lengths first, and reject bad requests.
  367. foreach my $range (@$ranges) {
  368. $range->[1] //= $sz - 1;
  369. if ( $range->[0] > $sz || $range->[0] < 0 || $range->[1] < 0 || $range->[0] > $range->[1] ) {
  370. $self->INFO("GET 416 $fullpath");
  371. return [ 416, [%headers], ["Requested range not satisfiable"] ];
  372. }
  373. }
  374. $headers{'Content-Length'} = List::Util::sum( map { $_->[1] + 1 - $_->[0] } @$ranges );
  375. #XXX Add the entity header lengths to the value - should hash-ify this to DRY
  376. if ($is_multipart) {
  377. foreach my $range (@$ranges) {
  378. $headers{'Content-Length'} += length("$fc--$CHUNK_SEP\n$primary_ct\nContent-Range: bytes $range->[0]-$range->[1]/$sz\n\n");
  379. $fc = "\n";
  380. }
  381. $headers{'Content-Length'} += length("\n--$CHUNK_SEP\--\n");
  382. $fc = '';
  383. }
  384. return sub {
  385. my $responder = shift;
  386. my $writer;
  387. foreach my $range (@$ranges) {
  388. $headers{'Content-Range'} = "bytes $range->[0]-$range->[1]/$sz" unless $is_multipart;
  389. $writer //= $responder->( [ $code, [%headers] ] );
  390. $writer->write("$fc--$CHUNK_SEP\n$primary_ct\nContent-Range: bytes $range->[0]-$range->[1]/$sz\n\n") if $is_multipart;
  391. $fc = "\n";
  392. my $len = List::Util::min( $sz, $range->[1] + 1 ) - $range->[0];
  393. $fh->seek( $range->[0], 0 );
  394. while ($len) {
  395. $fh->read( my $buf, List::Util::min( $len, $CHUNK_SIZE ) );
  396. $writer->write($buf);
  397. # Adjust for amount written
  398. $len = List::Util::max( $len - $CHUNK_SIZE, 0 );
  399. }
  400. }
  401. $fh->close();
  402. $writer->write("\n--$CHUNK_SEP--\n") if $is_multipart;
  403. $writer->close;
  404. };
  405. }
  406. # The page for an error code, which the router's generic_route() makes when it
  407. # has one.
  408. my sub generic {
  409. my ( $type, $code, $query ) = @_;
  410. if ($generic_handler) {
  411. my $rname = "/$code";
  412. my $title = $type;
  413. return $generic_handler->( $rname, $code, $title, $query );
  414. }
  415. $type .= Carp::longmess() if $debug;
  416. return [ $code, [ $ct => $content_types{html} ], ["$type"] ];
  417. }
  418. =head2 redirect, redirect_permanent, see_also
  419. Redirects to the provided page.
  420. =cut
  421. sub redirect {
  422. my ( $self, $to ) = @_;
  423. $self->INFO("redirect: $to");
  424. return [ 302, [ "Location" => $to, "Content-Length" => 0 ], [''] ];
  425. }
  426. sub redirect_permanent {
  427. my ( $self, $to ) = @_;
  428. $self->INFO("permanent redirect: $to");
  429. return [ 301, [ "Location" => $to, "Content-Length" => 0 ], [''] ];
  430. }
  431. sub see_also {
  432. my ( $self, $to ) = @_;
  433. $self->INFO("see also: $to");
  434. return [ 303, [ "Location" => $to, "Content-Length" => 0 ], [''] ];
  435. }
  436. =head2 ok
  437. Return a generic HTTP 200 OK
  438. =cut
  439. sub ok {
  440. my ( $self, $query, $body ) = @_;
  441. $self->INFO("$query->{method} 200 $query->{fullpath}");
  442. $body //= 'Ok';
  443. return generic( $body, 200, $query );
  444. }
  445. =head2 notfound, forbidden, badrequest, notallowed, unsupported, toolong, error, unavailable
  446. If you need to return these HTTP errors, return these within a route:
  447. sub route {
  448. my ($self, $query) = @_;
  449. ...
  450. return $self->notfound($query);
  451. }
  452. =cut
  453. sub notfound {
  454. my ( $self, $query, $body ) = @_;
  455. $self->INFO("$query->{method} 404 $query->{fullpath}");
  456. $body //= 'Not Found';
  457. return generic( $body, 404, $query );
  458. }
  459. sub forbidden {
  460. my ( $self, $query, $body ) = @_;
  461. $self->INFO("$query->{method} 403 $query->{fullpath}");
  462. $body //= 'Forbidden';
  463. return generic( $body, 403, $query );
  464. }
  465. sub badrequest {
  466. my ( $self, $query, $body ) = @_;
  467. $self->INFO("$query->{method} 400 $query->{fullpath}");
  468. $body //= 'Bad Request';
  469. return generic( $body, 400, $query );
  470. }
  471. sub notallowed {
  472. my ( $self, $query, @allow ) = @_;
  473. $self->INFO("$query->{method} 405 $query->{fullpath}");
  474. my $res = generic( 'Method Not Allowed', 405, $query );
  475. push( @{ $res->[1] }, Allow => join( ', ', @allow ) ) if ref $res eq 'ARRAY';
  476. return $res;
  477. }
  478. sub unsupported {
  479. my ( $self, $query, $body ) = @_;
  480. $self->INFO("$query->{method} 415 $query->{fullpath}");
  481. $body //= 'Unsupported Media Type';
  482. return generic( $body, 415, $query );
  483. }
  484. sub toolong {
  485. my ( $self, $query, $body ) = @_;
  486. $self->INFO("$query->{method} 419 $query->{fullpath}");
  487. $body //= 'URI too long';
  488. return generic( $body, 419, $query );
  489. }
  490. sub error {
  491. my ( $self, $query, $body ) = @_;
  492. my $method = $query->{method} // "?";
  493. my $fp = $query->{fullpath} // "?";
  494. $body //= 'Internal Server Error';
  495. $self->INFO("$method 500 $fp");
  496. return generic( $body, 500, $query );
  497. }
  498. sub unavailable {
  499. my ( $self, $query, $body ) = @_;
  500. $self->INFO("$query->{method} 503 $query->{fullpath}");
  501. $body //= 'Service Unavailable';
  502. return generic( $body, 503, $query );
  503. }
  504. my $cur_query = {};
  505. =head2 app($env) = PSGI response
  506. The PSGI application: respond() to the request, or answer it with error() when
  507. that dies.
  508. =cut
  509. sub app {
  510. my ( $self, $env ) = @_;
  511. return eval { $self->respond($env) } || do {
  512. my $err = $@;
  513. $env->{'psgi.errors'}->print($err) if $env->{'psgi.errors'};
  514. # psgi.errors is the server's stderr, which a daemonized server sends
  515. # nowhere unless it was started with --error-log. The log is the one
  516. # place that is always there, so the reason goes in it whole.
  517. $self->ERROR("Request failed: $err");
  518. # Redact the stack trace past line 1, it usually has things which should not be shown
  519. $cur_query->{message} = $err;
  520. $cur_query->{message} =~ s/\n.*//g if $cur_query->{message} && !$debug;
  521. return $self->error($cur_query);
  522. };
  523. }
  524. =head2 cannot_start($error, $env, %options) = ARRAYREF
  525. What to answer with when new() died, so there is no object to answer with.
  526. Usually a router that would not load: a missing module, a syntax error.
  527. The error goes to psgi.errors, and to the log by way of a stand-in built from
  528. the same options, so it lands in the same places a request's own errors do. If
  529. building the log is what failed -- a logger that will not load -- psgi.errors
  530. is all there is, and the answer is still a 500 rather than a second exception.
  531. The answer is a page of our own, not the router's error page: the router is
  532. what failed. With TPSGI_DEBUG set it carries the error itself, trace and all,
  533. for the same reason the error pages add one; without, it says nothing a visitor
  534. could use to map the installation.
  535. The worker retires once it has answered, on a server that offers
  536. psgix.harakiri, which Starman does. A router that died while it compiled
  537. leaves its file in %INC as a failure, and every later require of it in this
  538. process says only "Attempt to reload ... aborted" -- so a load that failed
  539. once, to a race or a lock, would otherwise fail for every request the worker
  540. ever serves. A fresh worker tries again from nothing.
  541. =cut
  542. sub cannot_start {
  543. my ( $error, $env, %options ) = @_;
  544. $env->{'psgi.errors'}->print($error) if ref $env eq 'HASH' && $env->{'psgi.errors'};
  545. # When the log will not build either, psgi.errors above is all there is.
  546. eval { bless( { %options, ip => '0.0.0.0' }, __PACKAGE__ )->ERROR("Could not start: $error"); 1 }; ## no critic (RequireCheckingReturnValueOfEval)
  547. $env->{'psgix.harakiri.commit'} = 1 if ref $env eq 'HASH' && $env->{'psgix.harakiri'};
  548. my $detail = '';
  549. if ($debug) {
  550. ( my $escaped = $error ) =~ s/([&<>"])/'&#' . ord($1) . ';'/ge;
  551. $detail = "<pre>$escaped</pre>";
  552. }
  553. my $page = "<!doctype html><html><head><meta charset=\"utf-8\"><title>Internal Server Error</title></head>" . "<body><h1>Internal Server Error</h1>$detail</body></html>";
  554. return [ 500, [ $ct => $content_types{html} ], [$page] ];
  555. }
  556. my %etags;
  557. =head2 respond($env) = PSGI response
  558. Answer one request: from a route when one matches, by running a CGI when the
  559. file under F<www/> is executable, and otherwise by serving the file. A path
  560. that a route matches, with a method that no route of it has, gets
  561. notallowed().
  562. =cut
  563. sub respond {
  564. my ( $self, $env ) = @_;
  565. # Start the server timing clock
  566. my $start = [gettimeofday];
  567. $cur_query = {};
  568. $self->{filehandle} = $env->{'psgix.io'} // *STDOUT;
  569. $self->{input} = $env->{'psgi.input'};
  570. # Setup the unique ID for the request
  571. $env->{REQUEST_ID} = $self->request_id(1);
  572. # Discard the path used in the log, it's too long and enough 4xx error code = ban
  573. return $self->toolong( { method => $env->{REQUEST_METHOD}, fullpath => '...' } ) if length( $env->{REQUEST_URI} ) > 2048;
  574. # Various stuff important for logging requests
  575. my $domain = $env->{HTTP_X_FORWARDED_HOST} || $env->{HTTP_HOST} // eval { Sys::Hostname::hostname() };
  576. my $path = $env->{PATH_INFO} || '/';
  577. # de-pooplicate the path
  578. $path =~ s{//}{/}g;
  579. my $port = $env->{HTTP_X_FORWARDED_PORT} // $env->{HTTP_PORT};
  580. my $pport = defined $port ? ":$port" : "";
  581. my $scheme = $env->{'psgi.url_scheme'} // 'http';
  582. my $method = $env->{REQUEST_METHOD};
  583. # It's important that we log what the user ACTUALLY requested rather than the rewritten path later on.
  584. my $fullpath = "$scheme://$domain$pport$path";
  585. # So we can log it for fail2ban
  586. my $ip = $env->{HTTP_X_FORWARDED_FOR} || $env->{REMOTE_ADDR};
  587. # set the referer & ua to go into DB logs, but not logs in general.
  588. # The referer/ua largely has no importance beyond being a proto bug report for log messages.
  589. my $referer = $env->{HTTP_REFERER};
  590. my $ua = $env->{HTTP_UA};
  591. $cur_query = {
  592. route => $path,
  593. fullpath => $path,
  594. method => $method,
  595. ip => $ip,
  596. ua => $ua,
  597. referer => $referer,
  598. tpsgi => $self,
  599. };
  600. # Paths with .. in them are not refused here, because starman appears to
  601. # refuse them already.
  602. # Support aliased paths
  603. my $aliases = $self->{aliases};
  604. $path = $aliases->{$path} if exists $aliases->{$path};
  605. # Check eTags. If we don't know about it, just assume it's good and lazily fill the cache
  606. # XXX yes, this allows cache poisoning...but only for logged in users!
  607. # This also needs to be IN DB so that we coordinate properly across forks
  608. if ( $env->{HTTP_IF_NONE_MATCH} ) {
  609. $self->INFO("$env->{REQUEST_METHOD} 304 $fullpath");
  610. return [ 304, [], [''] ] if $env->{HTTP_IF_NONE_MATCH} eq ( $etags{ $env->{REQUEST_URI} } || '' );
  611. $etags{ $env->{REQUEST_URI} } = $env->{HTTP_IF_NONE_MATCH} unless exists $etags{ $env->{REQUEST_URI} };
  612. }
  613. # TODO: Actually do something with the language passed to the renderer, HTTP_ACCEPT_LANGUAGE
  614. # TODO: Actually do something with the acceptable output formats in the renderer, HTTP_ACCEPT
  615. my $last_fetch = 0;
  616. if ( $env->{HTTP_IF_MODIFIED_SINCE} ) {
  617. $last_fetch = DateTime::Format::HTTP->parse_datetime( $env->{HTTP_IF_MODIFIED_SINCE} )->epoch();
  618. }
  619. # Figure out if we want compression or not
  620. my $alist = $env->{HTTP_ACCEPT_ENCODING} || '';
  621. $alist =~ s/\s//g;
  622. my @accept_encodings = split( /,/, $alist );
  623. my $deflate = grep { 'gzip' eq $_ } @accept_encodings;
  624. # Set the IP of the request so we can fail2ban
  625. $self->{ip} = $env->{HTTP_X_FORWARDED_FOR} || $env->{REMOTE_ADDR} || $self->{ip};
  626. # Make sure this works further down the line
  627. $env->{REMOTE_ADDR} = $env->{HTTP_X_FORWARDED_FOR} if $env->{HTTP_X_FORWARDED_FOR};
  628. my $streaming = $env->{'psgi.streaming'};
  629. my $route_actual = $self->routes->match( $method, $path );
  630. return $self->notallowed( $cur_query, @{ $route_actual->{allow} } ) if $route_actual && $route_actual->{status};
  631. if ($route_actual) {
  632. return $self->route(
  633. $route_actual, $env,
  634. fullpath => $fullpath,
  635. path => $path,
  636. start => $start,
  637. last_fetch => $last_fetch,
  638. deflate => $deflate,
  639. domain => $domain,
  640. port => $port,
  641. );
  642. }
  643. # Do a case insensitive match, because osx and windows
  644. my $file_possible = $self->mangle_filename("www/$path");
  645. my $file_actual;
  646. $file_actual = $file_possible if -e $file_possible && !-d _;
  647. # Dirindices
  648. if ( -d $file_possible ) {
  649. foreach my $index ( @{ $self->indices } ) {
  650. next if $file_actual;
  651. my $dirindex = $self->mangle_filename("www$path/$index");
  652. $dirindex =~ s{//}{/}g;
  653. $file_actual = $dirindex if -e $dirindex && !-d _;
  654. }
  655. }
  656. my $file_is_cgi = appears_executable($file_actual);
  657. return $self->cgi( $env, $fullpath, $file_actual, $last_fetch ) if $file_is_cgi;
  658. $self->INFO( "Attempting to serve $fullpath [" . ( $file_possible // "" ) . "]" );
  659. my @ranges = parse_ranges($env);
  660. return $self->serve( $fullpath, $file_actual, $start, $streaming, \@ranges, $last_fetch, $deflate ) if $file_actual;
  661. return $self->notfound($cur_query);
  662. }
  663. =head2 mangle_filename($path) = STRING
  664. $path, lowercased on the systems whose filesystems ignore case.
  665. =cut
  666. sub mangle_filename {
  667. my ( $self, $path ) = @_;
  668. # XXX yes, you can have case sensitive OSX, but basically nobody enables that ever
  669. $path = lc($path) if List::Util::any { $^O eq $_ } qw{darwin MSWin32 dos};
  670. $self->DEBUG("Path mangled to $path");
  671. return $path;
  672. }
  673. =head2 parse_ranges($env) = LIST
  674. The byte ranges of the request's Range header, as ARRAY references of start
  675. and end. An end that the header leaves out is undef. If-Range without Range
  676. asks for the whole file.
  677. =cut
  678. sub parse_ranges {
  679. my $env = shift;
  680. # Handle HTTP range/streaming requests
  681. my $range;
  682. $range = $env->{HTTP_RANGE} || "bytes=0-" if $env->{HTTP_RANGE} || $env->{HTTP_IF_RANGE};
  683. my @ranges;
  684. if ($range) {
  685. $range =~ s{bytes=}{}g;
  686. push(
  687. @ranges,
  688. map { [ split( /-/, $_ ) ]; } split( /,/, $range )
  689. );
  690. }
  691. return @ranges;
  692. }
  693. my @executable_extensions = qw{cgi sh exe pl php py};
  694. =head2 appears_executable($path) = BOOL
  695. Whether $path is a CGI to run rather than a file to serve: it is executable,
  696. and it has no extension or one of C<cgi>, C<sh>, C<exe>, C<pl>, C<php> and C<py>.
  697. =cut
  698. sub appears_executable {
  699. my $subj = shift;
  700. # Definitely not executable if it does not exist!
  701. return 0 unless $subj;
  702. return 0 unless -x $subj; ## no critic (ProhibitFiletest_rwxRWX) -- whether to run it, not whether we may
  703. my ($extension) = $subj =~ m/[.](\S+)$/;
  704. return 1 if !$extension;
  705. return 1 if List::Util::any { $extension eq $_ } @executable_extensions;
  706. return 0;
  707. }
  708. =head2 route($route, $env, %request) = PSGI response
  709. Run the callback of $route, which TPSGI::Router::match() returned, with the
  710. query that extract_query() builds. In CGI mode, which C<x-tpsgi-env> asks
  711. for, the query is not built, and the callback gets the request as %ENV
  712. instead. respond() calls route(), with %request holding C<fullpath>, C<path>,
  713. C<start>, C<last_fetch>, C<deflate>, C<domain> and C<port>.
  714. =cut
  715. sub route {
  716. my ( $self, $route, $env, %request ) = @_;
  717. my ( $fullpath, $path, $start ) = @request{qw{fullpath path start}};
  718. $self->DEBUG("Executing route \U$route->{method}\E $route->{template}");
  719. my $operation = $route->{operation};
  720. my $cgi_mode = $operation->{'x-tpsgi-env'};
  721. my $streaming = $env->{'psgi.streaming'};
  722. # Build the query data for passing to a route.
  723. # GET, POST, path parameters, then explicit data overrides.
  724. my $query = {};
  725. # If we're running in CGI mode, leave everything as-is.
  726. $query = $self->extract_query( $path, $route, $env ) unless $cgi_mode;
  727. # We failed validation if $query isn't a hashref.
  728. return $query if ref $query eq 'ARRAY';
  729. # allow this stuff to survive down to the end of some routes
  730. $query->{last_fetched} = $request{last_fetch};
  731. $query->{deflate} = $request{deflate};
  732. $query->{streaming} = $streaming;
  733. my @ranges = parse_ranges($env);
  734. $query->{ranges} = \@ranges;
  735. $query->{start} = $start;
  736. # Put things to tv_interval in here for Server-Timing
  737. $query->{fullpath} = $fullpath;
  738. $query->{route} = $path;
  739. $query->{route_template} = $route->{template};
  740. $query->{cookies} = $env->{HTTP_COOKIE};
  741. $query->{dnt} = $env->{HTTP_DNT};
  742. $query->{nosellinfo} = $env->{HTTP_SEC_GPC};
  743. $query->{port} = $request{port};
  744. $query->{scheme} = $env->{'psgi.url_scheme'} // 'http';
  745. $query->{method} = $env->{REQUEST_METHOD};
  746. $query->{lang} = $env->{HTTP_ACCEPT_LANGUAGE};
  747. $query->{accept} = $env->{HTTP_ACCEPT};
  748. $query->{has_query} = !!$env->{QUERY_STRING};
  749. $query->{domain} = $request{domain};
  750. $query->{dispatcher} = $operation;
  751. $query->{ip} = $cur_query->{ip};
  752. $query->{ua} = $cur_query->{ua};
  753. $query->{referer} = $cur_query->{referer};
  754. # This allows for better error handlers if we die in the route.
  755. $cur_query = $query;
  756. # Setup the CGI vars they expect IF requested
  757. local %ENV = ( %ENV, CGI::Emulate::PSGI->emulate_environment($env) ) if $cgi_mode;
  758. # Emulate mod_unique_id
  759. local $ENV{UNIQUE_ID} = UUID::uuid() if $cgi_mode;
  760. # Run any watch callbacks, and make this a truly 'dynamic' application if requested.
  761. $self->handle_changes();
  762. my $output = $route->{callback}->( $self, $query );
  763. # If it's streaming, just hand it off.
  764. # It's up to the caller to handle things like running post-close callbacks in this event.
  765. return $output if ref $output eq 'CODE';
  766. die "$fullpath returned no or malformed data!" unless ref $output eq 'ARRAY' && @$output == 3;
  767. my %headers = @{ $output->[1] };
  768. my $bytes = $headers{'Content-Length'} // '?';
  769. $self->INFO("$env->{REQUEST_METHOD} $output->[0] $bytes $fullpath");
  770. # Append server-timing headers if they aren't present
  771. my $tot = tv_interval($start) * 1000;
  772. push( @{ $output->[1] }, 'Server-Timing' => "app;dur=$tot" ) unless List::Util::any { $_ eq 'Server-Timing' } @{ $output->[1] };
  773. # In the event that we have post-close callbacks, go ahead and run them.
  774. return $self->stream_raw_psgi( $output, $query ) if @{ $self->{callbacks} };
  775. return $output;
  776. }
  777. =head2 extract_query($path, $route, $env) = HASHREF
  778. The query a route's callback is given: the query string, then the POST body,
  779. then the path parameters of $route and the operation's C<x-tpsgi-data>, each
  780. over the last. First TPSGI::Router::validate() checks the request against the
  781. operation. A request that fails gets unsupported() for a 415 and badrequest()
  782. for a 400, and the reasons go to the log at the debug level.
  783. =cut
  784. sub extract_query {
  785. my ( $self, $path, $route, $env ) = @_;
  786. my $query = {};
  787. $query = URL::Encode::url_params_mixed( $env->{QUERY_STRING} ) if $env->{QUERY_STRING};
  788. #Actually parse the POSTDATA if this is a POST
  789. my ( $form, $uploads );
  790. if ( $env->{REQUEST_METHOD} eq 'POST' ) {
  791. my $body = HTTP::Body->new( $env->{CONTENT_TYPE}, $env->{CONTENT_LENGTH} );
  792. while ( $env->{'psgi.input'}->read( my $buf, $CHUNK_SIZE ) ) {
  793. $body->add($buf);
  794. }
  795. # A POST with no body has nothing for a requestBody schema to judge.
  796. $form = { %{ $body->param } } if $env->{CONTENT_TYPE} || $env->{CONTENT_LENGTH};
  797. $uploads = $body->upload;
  798. }
  799. my ( $status, @errors ) = $self->routes->validate(
  800. $route,
  801. query => $query,
  802. body => $form,
  803. content_type => $env->{CONTENT_TYPE},
  804. env => $env,
  805. );
  806. if ($status) {
  807. $self->DEBUG("Rejected $path: $_") foreach @errors;
  808. # What respond() knows of the request, which the log line and the
  809. # error page need. The query has neither the method nor the path.
  810. my $request = { method => $env->{REQUEST_METHOD}, fullpath => $path, %$cur_query };
  811. return $status == 415 ? $self->unsupported($request) : $self->badrequest($request);
  812. }
  813. @$query{ keys(%$form) } = values(%$form) if $form;
  814. @$query{ keys(%$uploads) } = values(%$uploads) if $uploads;
  815. @$query{ keys( %{ $route->{path} } ) } = values( %{ $route->{path} } );
  816. my $data = $route->{operation}{'x-tpsgi-data'};
  817. @$query{ keys(%$data) } = values(%$data) if ref $data eq 'HASH';
  818. return $query;
  819. }
  820. =head2 cgi($env, $fullpath, $file_actual, $last_fetch) = PSGI response
  821. Run $file_actual as a CGI. Read until we are done with headers, then pass off
  822. the filehandle to PSGI for streaming.
  823. =cut
  824. # TODO support compression
  825. sub cgi {
  826. my ( $self, $env, $fullpath, $file_actual, $last_fetch ) = @_;
  827. $self->INFO("Handoff $fullpath to $file_actual for cgi exec");
  828. # Setup the CGI vars they expect
  829. local %ENV = ( %ENV, CGI::Emulate::PSGI->emulate_environment($env) );
  830. # Running the CGI is the job. The handle is read to its end in the stream below.
  831. my $pid = open( my $out, '-|', $file_actual ) or die "Could not run $file_actual: $!"; ## no critic (ProhibitPipeOpen RequireBriefOpen)
  832. my ( $code, undef, %headers ) = extract_headers( $out, $last_fetch );
  833. $code //= 500;
  834. return sub {
  835. my $responder = shift;
  836. my $writer = $responder->( [ $code, [%headers] ] );
  837. while ( $out->read( my $buf, $CHUNK_SIZE ) ) {
  838. $writer->write($buf);
  839. }
  840. $writer->close;
  841. # Wait on the CGI script to do whatever it's doing after closing stdout
  842. close($out) or $self->WARN("$file_actual exited with status $?");
  843. waitpid( $pid, 0 );
  844. };
  845. }
  846. =head2 stream_raw_cgi($cgi)
  847. Run the perl file $cgi in this process, as a CGI: its STDIN is the request body,
  848. and what it prints goes to the client. The worker exits when it is done.
  849. Use when you just have a .cgi file you want to run without pipe-open.
  850. XXX this is extraordinarily unsafe, inefficient and kills workers.
  851. But sometimes you have to bite the bullet and do this to migrate stuff effectively.
  852. =cut
  853. sub stream_raw_cgi {
  854. my ( $self, $cgi ) = @_;
  855. # Discard STDIN in favor of the HTTP body. Not localized, because this
  856. # process exits at the end of this sub.
  857. close(STDIN) or $self->DEBUG("STDIN was not open: $!");
  858. *STDIN = $self->{input}; ## no critic (RequireLocalizedPunctuationVars)
  859. # Similarly make 'print' do what CGIs expect
  860. my $fh = $self->{filehandle};
  861. select($fh); ## no critic (ProhibitOneArgSelect)
  862. do($cgi);
  863. # We may or may not actually get here.
  864. close($fh) or $self->WARN("Could not close the response: $!") if $fh;
  865. exit 0;
  866. }
  867. =head2 stream_raw_psgi($response, $query)
  868. Write the PSGI $response straight to the client's socket, close it, then run
  869. the callbacks that add_post_close_callback() queued. route() does this when
  870. there are any. The worker exits when it is done.
  871. =cut
  872. sub stream_raw_psgi {
  873. my ( $self, $response, $data ) = @_;
  874. my $time_to_here = tv_interval( $data->{start} );
  875. $self->DEBUG("Routing took $time_to_here s");
  876. my $post_routing = [gettimeofday];
  877. my $fh = $self->{filehandle};
  878. my $head = "HTTP/1.1 $response->[0]\n";
  879. # Emit the rest of the headers
  880. foreach ( my $i = 0; $i < @{ $response->[1] }; $i += 2 ) {
  881. my $header = $response->[1][$i];
  882. my $value = $response->[1][ $i + 1 ];
  883. $head .= "$header: $value\n";
  884. }
  885. # Then the body
  886. print {$fh} "$head\n", $response->[2][0];
  887. close($fh) or $self->WARN("Could not close the response: $!");
  888. $self->DEBUG( "Response written in " . ( tv_interval($post_routing) ) . " s" );
  889. while ( my $callback = shift @{ $self->{callbacks} } ) {
  890. if ( ref $callback eq 'CODE' ) {
  891. my $post_start = [gettimeofday];
  892. local $@;
  893. eval { $callback->(); 1 } or $self->ERROR("Post-close callback encountered exception: $@");
  894. $self->DEBUG( "Post-close callback took " . ( tv_interval($post_start) ) . " s" );
  895. }
  896. }
  897. exit 0;
  898. }
  899. =head2 stream_raw_http($query, $last_fetch, $to_fork, $callback, $error_handler)
  900. Answer with a 200 whose headers and body a forked child prints: $to_fork runs
  901. in the child with the client's socket selected, and then $error_handler, when
  902. there is one. Once the child exits and the socket is closed, $callback runs.
  903. The worker exits when it is done.
  904. =cut
  905. sub stream_raw_http { ## no critic (ProhibitManyArgs) -- routers call it with these, in this order
  906. my ( $self, $data, $last_fetch, $to_fork, $callback, $error_handler ) = @_;
  907. my $time_to_here = tv_interval( $data->{start} );
  908. $self->DEBUG("Routing took $time_to_here s");
  909. my $post_routing = [gettimeofday];
  910. my $fh = $self->{filehandle};
  911. # The CGIs you are executing here SHOULD NOT emit a status line.
  912. # Most apache CGIs you find don't anyways, so this is usually not a big deal.
  913. # Then our server-timing header.
  914. print {$fh} "HTTP/1.1 200 OK\n", "Server-Timing: " . $self->build_server_timing($data) . "\n";
  915. # We *must* fork because we can't rely on the child to close stdout.
  916. $fh->autoflush(1);
  917. my $pid = fork() // die "Could not fork child";
  918. if ( $pid == 0 ) {
  919. # So that what the child prints goes to the client.
  920. select($fh); ## no critic (ProhibitOneArgSelect)
  921. $self->DEBUG( "Forking child took" . ( tv_interval($post_routing) ) . " s" );
  922. local $@;
  923. eval { $to_fork->(); 1 } or $self->ERROR("Raw HTTTP streaming encountered exception: $@");
  924. if ( ref $error_handler eq 'CODE' ) {
  925. local $@;
  926. eval { $error_handler->(); 1 } or $self->ERROR("HTTP streaming error handler encountered exception: $@");
  927. }
  928. exit 0;
  929. }
  930. waitpid( $pid, 0 );
  931. close($fh) or $self->WARN("Could not close the response: $!");
  932. $self->DEBUG( "Response written in " . ( tv_interval($post_routing) ) . " s" );
  933. if ( ref $callback eq 'CODE' ) {
  934. my $post_start = [gettimeofday];
  935. local $@;
  936. eval { $callback->(); 1 } or $self->ERROR("Post-close callback encountered exception: $@");
  937. $self->DEBUG( "Post-close callback took " . ( tv_interval($post_start) ) . " s" );
  938. }
  939. exit 0;
  940. }
  941. =head2 extract_headers($fh, $last_fetch)
  942. Extract the headers from a filehandle. Set the 'Last Modified' header appropriately based on the output of `stat` on the filehandle.
  943. 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.
  944. =cut
  945. sub extract_headers {
  946. my ( $fh, $last_fetch, $is_ref ) = @_;
  947. my $headers = '';
  948. # NOTE: this is relying on while advancing the file pointer
  949. while (<$fh>) {
  950. last if $_ eq "\n";
  951. $headers .= $_;
  952. }
  953. #XXX still a toctou
  954. my $offset = $fh->tell();
  955. my ( undef, undef, $status, undef, $headers_parsed ) = HTTP::Parser::XS::parse_http_response( "$headers\n", HEADERS_AS_HASHREF );
  956. my $code = $status // 200;
  957. $headers_parsed //= {};
  958. if ( !$is_ref ) {
  959. my $mt = ( stat($fh) )[9];
  960. my @gm = gmtime($mt);
  961. my $now_string = strftime( "%a, %d %b %Y %H:%M:%S GMT", @gm );
  962. $code = $mt > $last_fetch ? $status : 304;
  963. $headers_parsed->{"Last-Modified"} = $now_string;
  964. }
  965. return ( $code, $offset, %$headers_parsed );
  966. }
  967. =head2 static($fullpath, $path, $start, $streaming, $last_fetch) = PSGI response
  968. Answer with the static render of $path that save_render() made, headers and
  969. all, or forbidden() when there is none.
  970. =cut
  971. sub static { ## no critic (ProhibitManyArgs) -- routers call it with these, in this order
  972. my ( $self, $fullpath, $path, $start, $streaming, $last_fetch ) = @_;
  973. $self->DEBUG("Rendering static for $path");
  974. # XXX because of psgi I can't just vomit the file directly
  975. # The handle is the body of the response, so the server closes it.
  976. if ( open( my $fh, '<', "statics/$path" ) ) { ## no critic (RequireBriefOpen)
  977. my ( $code, $offset, $headers_parsed ) = ( 200, undef, {} );
  978. ( $code, $offset, %$headers_parsed ) = extract_headers( $fh, $last_fetch );
  979. # Append server-timing headers
  980. my $tot = tv_interval($start) * 1000;
  981. $headers_parsed->{'Server-Timing'} = "static;dur=$tot";
  982. #XXX uwsgi just opens the file *again* when we already have a filehandle if it has a path.
  983. # starman by comparison doesn't violate the principle of least astonishment here.
  984. # This is probably a performance optimization, but makes the kind of micromanagement I need to do inconvenient.
  985. # As such, we will just return a stream.
  986. $self->INFO("GET 200 $headers_parsed->{'Content-Length'} $fullpath");
  987. return sub {
  988. my $responder = shift;
  989. #push(@headers, 'Content-Length' => $sz);
  990. my $writer = $responder->( [ $code, [%$headers_parsed] ] );
  991. while ( $fh->read( my $buf, $CHUNK_SIZE ) ) {
  992. $writer->write($buf);
  993. }
  994. close($fh) or $self->WARN("Could not close statics/$path: $!");
  995. $writer->close;
  996. }
  997. if $streaming;
  998. return [ $code, [%$headers_parsed], $fh ];
  999. }
  1000. return $self->forbidden( $self->{current_query} );
  1001. }
  1002. =head2 get_config() = HASH
  1003. TPSGI::Startup::get_config(): F<~/.tpsgi.ini>, over the defaults.
  1004. =cut
  1005. sub get_config { return TPSGI::Startup::get_config() }
  1006. =head2 checkpoint($query, $name)
  1007. Mark the time as $name, for the Server-Timing header that
  1008. build_server_timing() makes.
  1009. =cut
  1010. sub checkpoint {
  1011. my ( $self, $data, $name ) = @_;
  1012. $data->{checkpoints} //= [ [ start => $data->{start} ] ];
  1013. push( @{ $data->{checkpoints} }, [ $name => [gettimeofday] ] );
  1014. return;
  1015. }
  1016. =head2 build_server_timing($query) = STRING
  1017. A Server-Timing header value: how long each checkpoint() took after the one
  1018. before it, and the total since the request started.
  1019. =cut
  1020. sub build_server_timing {
  1021. my ( $self, $data ) = @_;
  1022. my @st;
  1023. my $last_interval;
  1024. foreach my $checkpoint ( @{ $data->{checkpoints} } ) {
  1025. if ($last_interval) {
  1026. my $dur = tv_interval( $last_interval, $checkpoint->[1] ) * 1000;
  1027. push( @st, "$checkpoint->[0];dur=$dur" );
  1028. }
  1029. $last_interval = $checkpoint->[1];
  1030. }
  1031. my $tot = tv_interval( $data->{start} ) * 1000;
  1032. push( @st, "tot;dur=$tot" );
  1033. delete $data->{checkpoints};
  1034. return join( ', ', @st );
  1035. }
  1036. =head2 add_post_close_callback()
  1037. Add a thing to do after closing stdout; useful for housekeeping that doesn't need to hold up pageload.
  1038. =cut
  1039. sub add_post_close_callback {
  1040. my ( $self, $cb ) = @_;
  1041. push( @{ $self->{callbacks} }, $cb );
  1042. return;
  1043. }
  1044. =head2 signal_restart_parent()
  1045. Instruct tPSGI to reload after closing stdout.
  1046. =cut
  1047. sub signal_restart_parent {
  1048. my ($self) = @_;
  1049. $self->add_post_close_callback( sub { kill 'HUP', getppid } );
  1050. return;
  1051. }
  1052. =head2 save_render($path, $extension, $body)
  1053. Save a static render of the page.
  1054. Good to queue after closing stdout.
  1055. Passed file extension slapped on the end of the path.
  1056. You can then serve them as statics with an appropriately done try_files block in nginx.
  1057. =cut
  1058. # Remove one static render.
  1059. my sub invalidate_file {
  1060. my ( $self, $file ) = @_;
  1061. $self->INFO("Remove static render $file");
  1062. return unlink $file;
  1063. }
  1064. # fixup matrix
  1065. my %fixup = (
  1066. text => 'txt',
  1067. blob => 'bin',
  1068. );
  1069. sub save_render {
  1070. my ( $self, $path, $extension, $body ) = @_;
  1071. return unless $path && $extension;
  1072. # If this is an index, let's make it so
  1073. if ( $path =~ m{/$} ) {
  1074. $path = $path . 'index';
  1075. }
  1076. # Fixup the extension if needed.
  1077. $extension = $fixup{$extension} if exists $fixup{$extension};
  1078. my $path_fixed = $path;
  1079. $path_fixed =~ s/[.]\Q$extension\E$//;
  1080. $path_fixed = "$path_fixed.$extension";
  1081. my $path2file = "$self->{tpsgi_dir}/www/static/" . dirname($path_fixed);
  1082. if ( !-d $path2file ) {
  1083. File::Path::make_path( $path2file, { user => $<, group => $self->{gid}, chmod => 0o755 } ) or die "Could not create directory $path2file";
  1084. }
  1085. my $file = "$self->{tpsgi_dir}/www/static/$path_fixed";
  1086. my $verb = -e $file ? 'Overwrite' : 'Write';
  1087. $self->INFO("$verb $path as static/$path_fixed");
  1088. open( my $fh, '>', $file ) or die "Could not open $file for writing";
  1089. print {$fh} $body;
  1090. close($fh) or die "Could not write $file: $!";
  1091. chmod( 0755, $file );
  1092. chown( $<, $self->{gid}, $file );
  1093. return;
  1094. }
  1095. =head2 invalidate_render($path, $extension)
  1096. Remove an existing static render of a path.
  1097. =cut
  1098. sub invalidate_render {
  1099. my ( $self, $path, $extension ) = @_;
  1100. return unless $path && $extension;
  1101. # Fixup the extension if needed.
  1102. $extension = $fixup{$extension} if exists $fixup{$extension};
  1103. my $path_fixed = $path;
  1104. $path_fixed =~ s/[.]\Q$extension\E$//;
  1105. $path_fixed = "$path_fixed.$extension";
  1106. my $file = "$self->{tpsgi_dir}/www/static/$path_fixed";
  1107. return unless -e $file;
  1108. return invalidate_file( $self, $file );
  1109. }
  1110. =head2 invalidate_renders($extension)
  1111. Remove all existing static renders with the provided extension.
  1112. Useful when it's not easy to figure out what to re-render due to including templates in other templates, etc.
  1113. =cut
  1114. sub invalidate_renders {
  1115. my ( $self, $extension ) = @_;
  1116. File::Find::find(
  1117. {
  1118. wanted => sub {
  1119. my $object = $_;
  1120. invalidate_file( $self, $object ) if ( !-d $object && $object =~ m/[.]\Q$extension\E$/ );
  1121. },
  1122. no_chdir => 1,
  1123. bydepth => 1,
  1124. },
  1125. "$self->{tpsgi_dir}/www/static/"
  1126. );
  1127. return;
  1128. }
  1129. =head2 watch_for_changes(@dirs)
  1130. TPSGI::Startup::watch_for_changes(), which watches the .pm files under @dirs for
  1131. autoreload. A function, not a method.
  1132. =cut
  1133. sub watch_for_changes {
  1134. my (@dirs) = @_;
  1135. return TPSGI::Startup::watch_for_changes(@dirs);
  1136. }
  1137. =head2 add_watch($path, $callback, %options)
  1138. Add an arbitrary file or directory to the inotify watchlist, along with a
  1139. callback to run when it changes. Returns the inotify watch descriptor.
  1140. The callback is invoked as:
  1141. $callback->( $tpsgi, $change );
  1142. ...where $change is a HASH reference describing what happened:
  1143. path => full path of the thing which changed
  1144. watched => the path the watch was registered against
  1145. events => ARRAY reference of event names, e.g. [ 'MODIFY' ]
  1146. name => entry within a watched directory, if the watch was on a directory
  1147. wd => inotify watch descriptor
  1148. mask => raw inotify event mask
  1149. Which makes the common case - blowing away a static render because the thing it
  1150. was generated from changed - look like:
  1151. $tpsgi->add_watch( "data/posts.json", sub {
  1152. my ( $tpsgi, $change ) = @_;
  1153. $tpsgi->invalidate_renders('html');
  1154. });
  1155. %options are passed through to TPSGI::Startup::add_watch, so you may also pass
  1156. 'events' (defaults to CREATE, MODIFY, DELETE and MOVE) and 'key' (an explicit
  1157. dedupe key for the callback).
  1158. Some caveats worth knowing about:
  1159. =over
  1160. =item * Watches live in the inotify instance tarbaby sets up before forking, and
  1161. are therefore shared by every worker. Whichever worker notices a change first
  1162. consumes it for all of them, so register the same callbacks in every worker
  1163. (declare them in your router module, which every worker loads) and make sure
  1164. they're safe to run in any worker.
  1165. =item * Changes are only noticed when a request comes in, since nothing is
  1166. sitting in a select loop over the inotify FD. Renders are invalidated promptly
  1167. in practice, but not instantly.
  1168. =item * Watching a directory rather than a single file is more robust, as
  1169. watching a file which is later replaced wholesale (the usual write-tempfile-then-
  1170. rename dance) only survives because we re-establish the watch afterwards. If
  1171. the file is deleted and not immediately recreated, the watch is gone for good.
  1172. =back
  1173. =cut
  1174. sub add_watch {
  1175. my ( $self, $path, $callback, %options ) = @_;
  1176. my $existing = TPSGI::Startup::is_watched($path);
  1177. # A watch we can't establish (the usual cause being a path which isn't there yet)
  1178. # shouldn't be fatal to the request which happened to be the one registering it.
  1179. local $@;
  1180. my $wd = eval {
  1181. TPSGI::Startup::add_watch(
  1182. %options,
  1183. path => $path,
  1184. callback => $callback,
  1185. );
  1186. };
  1187. if ( !defined $wd ) {
  1188. my $why = $@;
  1189. $why =~ s/\n.*// if $why;
  1190. $self->WARN("Could not watch $path for changes: $why");
  1191. return undef;
  1192. }
  1193. $self->DEBUG("Watching $path for changes") unless $existing;
  1194. return $wd;
  1195. }
  1196. =head2 remove_watch($path)
  1197. Drop a path, and any callbacks registered against it, from the watchlist.
  1198. =cut
  1199. sub remove_watch {
  1200. my ( $self, $path ) = @_;
  1201. return TPSGI::Startup::remove_watch($path);
  1202. }
  1203. =head2 handle_changes
  1204. Drain the inotify queue, running the callbacks registered for whatever changed,
  1205. and restart tPSGI if any of the relevant libdir files changed (assuming
  1206. autoreload is on). Powers both the autorestart feature and app-registered
  1207. watches.
  1208. =cut
  1209. sub handle_changes {
  1210. my $self = shift;
  1211. my @changes = TPSGI::Startup::pending_changes();
  1212. return 0 unless @changes;
  1213. my $had_changes = 0;
  1214. my @rewatch;
  1215. foreach my $change (@changes) {
  1216. my $watch = $change->{watch};
  1217. # A watch descriptor we know nothing about belongs to a worker which registered
  1218. # watches we didn't. All we can sensibly do with those is the old behavior.
  1219. if ( !$watch ) {
  1220. $had_changes++ if $change->{path} =~ m/[.]pm$/;
  1221. next;
  1222. }
  1223. $had_changes++ if $watch->{restart} && $change->{path} =~ m/[.]pm$/;
  1224. foreach my $cb ( @{ $watch->{callbacks} } ) {
  1225. local $@;
  1226. my $ok = eval { $cb->{code}->( $self, $change ); 1 };
  1227. $self->WARN("Watch callback for $change->{path} died: $@") unless $ok;
  1228. }
  1229. # The kernel drops watches on files which get replaced or deleted, so put them back.
  1230. push( @rewatch, $change->{wd} ) if List::Util::any { $_ eq 'IGNORED' } @{ $change->{events} };
  1231. }
  1232. TPSGI::Startup::rewatch($_) foreach @rewatch;
  1233. return 0 unless $had_changes && $self->{autoreload};
  1234. # We don't have to clean up the wds, that is handled in the destructor for Inotify
  1235. $self->INFO("Relevant Change in libdirs detected, reloading\n");
  1236. $self->signal_restart_parent();
  1237. return 0;
  1238. }
  1239. =head2 restart_if_changes
  1240. Deprecated alias for handle_changes().
  1241. =cut
  1242. sub restart_if_changes { goto &handle_changes }
  1243. 1;
  1244. __END__
  1245. =head1 SYNOPSIS
  1246. A router module, which C<routers> in F<~/.tpsgi.ini> names:
  1247. package My::Routes;
  1248. use 5.040;
  1249. our %openapi = (
  1250. openapi => '3.0.3',
  1251. info => { title => 'My routes', version => '1' },
  1252. paths => {
  1253. '/hello' => {
  1254. get => {
  1255. 'x-tpsgi-callback' => \&hello,
  1256. responses => { 200 => { description => 'A greeting' } },
  1257. },
  1258. },
  1259. },
  1260. );
  1261. sub hello {
  1262. my ( $tpsgi, $query ) = @_;
  1263. return [ 200, [ 'Content-Type' => 'text/html' ], ['Hello world'] ];
  1264. }
  1265. =head1 DESCRIPTION
  1266. Start it with the Starman options you want, from the directory of the
  1267. application, with tarbaby on the PATH:
  1268. tpsgi ... [starman args]
  1269. A PSGI server built to migrate old mod_perl/apache stacks onto psgi, and that powers tCMS.
  1270. Will execute any executable file (chmod +x) in www/ as CGI, and otherwise host any static file therein.
  1271. The idea here is as a bridge between old and new which allows you to modernize at your own pace, gradually turning CGIs into routes.
  1272. The other idea is that we don't want to add any extra abstraction if we don't have to, while gently guiding into better practices.
  1273. Shops used to having full control over the HTTP response should continue to do so, rather than being locked into one framework or another.
  1274. This allows them to continue doing whatever funky thing they have been doing, like COMET, et cetera.
  1275. The best way to do that is to expect raw PSGI to be output.
  1276. =head1 CONFIGURATION
  1277. Obeys ~/.tpsgi.ini -- this is what will control all the custom aspects mentioned below:
  1278. verbose : Whether to print all log messages or not
  1279. custom_log : Custom location for default (rotated) log
  1280. routers : Router module(s), separate with comma. Relative to TPSGI install dir.
  1281. indices : Dirindex files. In addition to index.html, index.htm, index.cgi
  1282. loggers : Logger module(s), separate with comma. Each is a Log::Dispatch::Output class. tPSGI loads it by name, so it can live beside a router
  1283. auth : Authentication module
  1284. domain : Domain name of the application, used by things like service files
  1285. basedir : Directory you want to chdir into on startup. Relative to TPSGI install dir. Default '.'
  1286. user : Who to run as
  1287. http_user : Who's proxying this application (used for statics)
  1288. autoreload : Whether or not to automatically reload the application when library files change. Default 0.
  1289. You can run bin/tpsgi-config to get any config value needed by scripts.
  1290. We wrap starman with a custom script, C<bin/tarbaby> that implements Plack::Loader::Reload far less dangerously.
  1291. It only reloads the code relevant to tPSGI and your application itself.
  1292. All the CPAN deps should be updated out-of-band and the service manually restarted, as this is far more fraught.
  1293. =head1 CUSTOM ROUTING
  1294. tPSGI requires each router module, and routes requests by the OpenAPI 3
  1295. document that each one exports as C<%openapi>. A router can also export
  1296. C<%aliases>, C<%watches> and C<generic_route()>.
  1297. package My::Routes;
  1298. use 5.040;
  1299. our %openapi = (
  1300. openapi => '3.0.3',
  1301. info => { title => 'My routes', version => '1' },
  1302. paths => {
  1303. '/sitemap/{map}.xml' => {
  1304. get => {
  1305. 'x-tpsgi-callback' => \&sitemap,
  1306. 'x-myapp-nomap' => true,
  1307. parameters => [
  1308. {
  1309. name => 'map',
  1310. in => 'path',
  1311. required => true,
  1312. schema => { type => 'integer' },
  1313. },
  1314. ],
  1315. responses => { 200 => { description => 'One page of the sitemap' } },
  1316. },
  1317. },
  1318. '/lib/{module+}' => {
  1319. get => {
  1320. 'x-tpsgi-callback' => 'My::Routes::manual',
  1321. 'x-tpsgi-data' => { section => 'manual' },
  1322. parameters => [
  1323. {
  1324. name => 'module+',
  1325. in => 'path',
  1326. required => true,
  1327. schema => { type => 'string' },
  1328. },
  1329. ],
  1330. responses => { 200 => { description => 'The manual of a module' } },
  1331. },
  1332. },
  1333. },
  1334. );
  1335. The document is Perl data. A boolean in it is perl's own C<true> or C<false>,
  1336. which C<use 5.040> imports, or a JSON boolean such as C<Cpanel::JSON::XS::true>.
  1337. C<\1> is not a boolean, and fails the check against the OpenAPI schema.
  1338. new() dies when a document is not valid, so a broken router fails as it loads,
  1339. and not at the first request. A router that still defines C<@routes> and no
  1340. C<%openapi> dies the same way.
  1341. =head2 Templates
  1342. Each key of C<paths> is a template. C<{name}> matches any text up to the next
  1343. slash, and one segment can hold several variables and literal text, as in
  1344. C<{map}.xml>. C<{name+}> matches the rest of the path, slashes and all, and it
  1345. must be the whole last segment. Its parameter is named C<name+>, and the query
  1346. gives its value as C<name>. Any other character matches only itself. No
  1347. variable matches a newline.
  1348. When more than one template matches, the most concrete one wins. Literal
  1349. segments win over segments with variables, segments that mix text and
  1350. variables win over a lone C<{name}>, and C<{name+}> comes last. Then the
  1351. template with more literal characters wins, and then the one that sorts first.
  1352. L<TPSGI::Router> says this in full.
  1353. A path that a template matches, with a method that no matching template has,
  1354. gets a 405 with an C<Allow> header. HEAD uses the C<get> operation when there
  1355. is no C<head> one.
  1356. =head2 Operations
  1357. The C<x-tpsgi-> keys of an operation are tPSGI's:
  1358. x-tpsgi-callback The sub that answers the request: a CODE reference, or
  1359. the fully qualified name of a sub. Required.
  1360. x-tpsgi-data A HASH, which is merged over the query.
  1361. x-tpsgi-env True for CGI mode: the callback gets the request as a
  1362. CGI %ENV, and no query is built.
  1363. Any other C<x-> key is the application's. The callback finds the whole
  1364. operation in C<< $query->{dispatcher} >>, and the template that matched in
  1365. C<< $query->{route_template} >>.
  1366. =head2 Validation
  1367. Before the callback runs, tPSGI checks the request against the operation's
  1368. C<parameters> and C<requestBody>. Values are coerced to their schema, so an
  1369. integer arrives as a number. A request that fails gets a 400, and the reasons
  1370. go to the log at the debug level, not into the page. The 400 is also the
  1371. answer to a query key that the operation does not declare, when the operation
  1372. declares a query parameter or a request body. A form body is as strict as the
  1373. C<additionalProperties> of its schema. A body whose media type is not among
  1374. the C<content> of the C<requestBody> gets a 415.
  1375. =head2 Callbacks
  1376. A callback is passed the TPSGI object and the query, which means you can do:
  1377. sub route {
  1378. my ($self, $query) = @_;
  1379. $self->INFO("whatever");
  1380. $self->ERROR("eeee");
  1381. $self->DEBUG("blah");
  1382. # From the query string, the POST body, the path or x-tpsgi-data, in
  1383. # that order: a later one wins.
  1384. my $param = $query->{param};
  1385. my %headers = ...;
  1386. return [200, \%headers, ["Hello world"]]
  1387. }
  1388. =head1 WATCHING FILES FOR CHANGES
  1389. tPSGI already watches your library directories with inotify so that it can reload itself when your
  1390. code changes. Your application can put arbitrary files and directories onto that same watchlist,
  1391. along with a callback to run when they change. The usual reason to want this is throwing away a
  1392. static render because the thing it was generated from changed underneath it.
  1393. Routing modules declare these the same way they declare routes:
  1394. our %watches = (
  1395. # A bare coderef is the common case: run this when the path changes.
  1396. 'data/posts.json' => sub {
  1397. my ($tpsgi, $change) = @_;
  1398. $tpsgi->invalidate_renders('html');
  1399. },
  1400. # Or spell it out if you want to be picky about which events you care about.
  1401. 'www/assets' => {
  1402. callback => \&asset_changed,
  1403. events => [qw{CREATE MOVE}],
  1404. },
  1405. );
  1406. ...or register them at runtime with C<< $tpsgi->add_watch($path, $callback, %options) >>, and drop them
  1407. again with C<< $tpsgi->remove_watch($path) >>.
  1408. Callbacks are passed the TPSGI object and a hashref describing what happened:
  1409. sub asset_changed {
  1410. my ($tpsgi, $change) = @_;
  1411. $change->{path}; # full path of the thing which changed
  1412. $change->{watched}; # the path the watch was registered against
  1413. $change->{events}; # arrayref of event names, e.g. ['MODIFY']
  1414. $tpsgi->invalidate_render($change->{path}, 'html');
  1415. }
  1416. A few things worth knowing:
  1417. =over
  1418. =item *
  1419. Watches are shared by every worker, so whichever worker notices a change first consumes it for all
  1420. of them. Declare your watches in your routing module (which every worker loads) rather than from
  1421. inside a single route, and make the callbacks safe to run in any worker.
  1422. =item *
  1423. Nothing sits in a select loop over the inotify descriptor, so changes are noticed when the next
  1424. request comes in. Prompt, but not instant.
  1425. =item *
  1426. Watching the containing directory is more robust than watching a single file. Watches on files
  1427. which get replaced wholesale (write a tempfile, rename over the target) are re-established
  1428. afterwards, but a file which is deleted and not recreated takes its watch with it.
  1429. =back
  1430. =head1 CUSTOM AUTHENTICATION HANDLING
  1431. The (optional) Authentication handler can be passed, and look like so:
  1432. package Auth::Handler;
  1433. # Is user who they say they are
  1434. sub authenticate {
  1435. my (%auth_payload) = @_;
  1436. ...
  1437. return ($session_cookie);
  1438. }
  1439. # is the user authorized to do $thing
  1440. sub authorized {
  1441. my ($user, $priv) = @_;
  1442. ...
  1443. return 1 || 0;
  1444. }
  1445. # Available ACLs
  1446. sub acls {
  1447. ...
  1448. return %acls;
  1449. }
  1450. # Dump acls for user
  1451. sub acls_for_user {
  1452. my $user = shift;
  1453. ...
  1454. return %acls;
  1455. }
  1456. # What is the user for this session token
  1457. sub user_for_session {
  1458. my $session = shift;
  1459. ...
  1460. return $user;
  1461. }
  1462. In the event that auth or route handlers are found, these events will be noted in the startup log.
  1463. =head1 LOGGING
  1464. By default we have two log handlers...which you can augment with your own, based on Log::Dispatch.
  1465. The first default is to print ERROR and worse to the STDOUT of the PSGI server.
  1466. The other is to emit INFO or better to logs/tpsgi.log
  1467. Pass your own dispatch subclasses and watch it go whir.
  1468. =head1 SERVICE CONFIGURATION
  1469. Proper operation of this as a production service requires some things:
  1470. =over
  1471. =item 1.
  1472. You have a user which has this repository as their home directory.
  1473. =item 2.
  1474. You run the shell script, tpsgi.sh in service/ to run this as a service.
  1475. =item 3.
  1476. You set the username and the domain name of the service in .tpsgi.ini
  1477. =item 4.
  1478. You set your PATH appropriately to pick up on things like custom perls in a .bashrc
  1479. =item 5.
  1480. You set the group of your reverse proxy application in .tpsgi.ini (the http_user variable)
  1481. =back
  1482. run service/tpsgi.sh as root and you should be set supposing the above is true.
  1483. In general the operation is like so:
  1484. =over
  1485. =item 1.
  1486. nginx (or whatever) looks for a sock file owned $USER:$WEB_GROUP, and reverse proxies to this
  1487. =item 2.
  1488. tpsgi is actively listening on this sock file.
  1489. =back
  1490. Generally you'll just run bin/build_service and then systemctl start $domain.
  1491. It's configured to start as root, then drop privs thanks to Net::Server's capabilities inherited in starman.
  1492. =head2 Secrets the application needs
  1493. An application often needs a key or a password that has no business living in
  1494. its own checkout, where it would go into every backup alongside whatever it
  1495. protects. bin/build_service makes one and puts it in systemd's credential
  1496. store; the unit imports it and service/tpsgi.sh hands it on.
  1497. You do not have to do anything for this. build_service writes
  1498. C</etc/credstore.encrypted/tpsgi-vault> if it is not there already, encrypted
  1499. with C<systemd-creds --with-key=auto>: sealed to the TPM when the machine has a
  1500. usable one, and to the host key when it does not. It says which you got, since
  1501. that is the difference between a stolen disk being useless and a stolen disk
  1502. being a stolen key. An existing key is left alone.
  1503. systemd decrypts it into a ramdisk at C</run/credentials/$service> that only this
  1504. service can read. Nothing breaks if it is not there: C<ImportCredential=> is
  1505. quiet about a credential that does not exist, so an installation whose
  1506. application wants no secrets starts normally.
  1507. The workers are chrooted into the application directory and that ramdisk is
  1508. outside it, so service/tpsgi.sh reads the credential before the chroot happens
  1509. and exports it -- C<tpsgi-vault> becomes C<TPSGI_VAULT_KEY>. An application taking
  1510. one is expected to read it out of its environment as it starts and delete it
  1511. there and then, so that nothing it forks afterwards inherits it.
  1512. It is named for tPSGI rather than for whatever is being served, because this is
  1513. tPSGI's unit and one service is one application in one directory.
  1514. The key is not backed up and cannot be recovered. That is deliberate: it lives
  1515. and dies with the machine, so that it is never sitting in the same backup as the
  1516. data it protects. Rebuilding the host means whatever was sealed under it is
  1517. gone and gets stored again.
  1518. =cut