File Coverage

blib/lib/Pepper/Templates.pm
Criterion Covered Total %
statement 17 31 54.8
branch 1 4 25.0
condition 1 2 50.0
subroutine 6 12 50.0
pod 0 9 0.0
total 25 58 43.1


line stmt bran cond sub pod time code
1             package Pepper::Templates;
2             # module that holds templates needed to setup this kit.
3             # used by Pepper::Commander (and our install tests)
4              
5             # For the record, I do hate doing it this way, and I should find a better solution.
6              
7 1     1   19 use 5.022001;
  1         4  
8 1     1   5 use strict;
  1         3  
  1         20  
9 1     1   5 use warnings;
  1         1  
  1         585  
10              
11             our $VERSION = "1.5";
12              
13             # just need to be an object
14             sub new {
15 1     1 0 1035 my ($class) = @_;
16              
17             # set up the object with all the options data
18             my $self = bless {
19 1         5 'pepper_directory' => $ENV{HOME}.'/pepper',
20             }, $class;
21              
22 1         3 return $self;
23             }
24              
25             # we will use like an object, but no real need for new()
26             sub get_template {
27 1     1 0 5 my ($self,$template_wanted) = @_;
28            
29             # presume they need the endpoint handler
30 1   50     4 $template_wanted ||= 'endpoint_handler';
31            
32             # only if we can provide it
33 1 50       11 return 'Not available' if !$self->can($template_wanted);
34            
35             # otherwise, send out
36 1         8 return $self->$template_wanted();
37            
38             # our method map
39            
40             }
41              
42             ### Start our templates
43              
44             # for the testing phase of cpanm
45             sub test_template {
46 1     1 0 3 my $self = shift;
47            
48 1         3 return q[[%test_date%] was a [%test_day%]];
49             }
50              
51             # return the template for endpoint handlers
52             sub endpoint_handler {
53 0     0 0   my $self = shift;
54 0           return q[package [%endpoint_handler%];
55             # provides handler for Endpoint URI: [%endpoint_uri%]
56              
57             # promote better coding
58             use strict;
59             use warnings;
60              
61             # handle the request
62             sub endpoint_handler {
63             my ($class,$pepper) = @_;
64             # the $class is there becauase this is a fake object; please see https://perldoc.perl.org/5.32.0/perlootut.html
65              
66             ### YOUR FANTASTIC CODE GOES HERE
67             # Parameters sent via GET/POST or JSON body are available
68             # within $pepper->{params}
69             #
70             # Simply return your prepared content and Pepper will deliver
71             # it to the client. To send JSON, return a reference to an Array or Hash.
72             # HTML or Text can also be returned. Please see the documentation for other options.
73             #
74             # Please see perldoc pepper for methods available in $pepper
75              
76             # for example: just a very basic start
77             my $starter_content = {
78             'current_timestamp' => $pepper->time_to_date( time(), 'to_date_human_full' ),
79             'hello' => 'world',
80             'psgi_params' => $pepper->{params},
81             };
82            
83             # return the content back to the main process, and Pepper will send it out to the client
84             # sending a data structure back will send that structure in JSON to the client
85             # you can alse return HTML or any text
86            
87             return $starter_content;
88              
89             }
90              
91             1;
92             ];
93             }
94              
95             # templates for the HTML example endpoint
96             sub html_example_endpoint {
97 0     0 0   my ($self,$send_handler) = @_;
98            
99             # we have two parts to this: a TT template and a endpoint handler module
100 0 0         if (!$send_handler) {
101 0           return q[[%# This template is used for the /pepper/html_example example endpoint.
102             Please see $ENV{HOME}/pepper/lib/PepperApps/HTMLExample.pm %]
103            
104             <!DOCTYPE html>
105             <html>
106             <head>
107             <meta charset="utf-8" />
108             <title>Pepper HTML Example</title>
109             </head>
110             <body>
111              
112             [% IF pepper_cookie %]
113             <h3>I Found a Cookie</h3>
114             Cookie value: [% pepper_cookie %]
115             [% END %]
116              
117             [% IF provided_phrase %]
118             <h3>Results of Form</h3>
119             <ul>
120             [% FOREACH fact IN phrase_facts.keys.sort %]
121             <li> [% fact %] == [% phrase_facts.$fact %] </li>
122             [% END %]
123             </ul>
124            
125             <h3>Resubmit our Very Basic Form</h3>
126            
127             [% SET submit_word = 'Re-Submit' %]
128            
129             [% ELSE %]
130              
131             <h3>Very Basic Form</h3>
132              
133             [% SET submit_word = 'Submit' %]
134            
135             [% END %]
136              
137             <form action="/pepper/html_example" method="post">
138              
139             <strong>Provide a phrase:</strong>
140             <br/><input type="text" size="40" name="provided_phrase" value="[%provided_phrase%]"/>
141             <br/><br/>
142              
143             <strong>Demo Setting a Cookie:</strong>
144             <br/><input type="checkbox" name="set_demo_cookie" value="yes"/>&nbsp;Click to have Pepper set a cookie with a random string.
145             <br/>Close the browser and return to see that the cookie persists.
146             <br/>Submitting with this check will overwrite the previous cookie.
147             <br/><br/>
148              
149              
150             <strong>Return JSON:</strong>
151             <br/><input type="checkbox" name="return_json" value="yes"/>&nbsp;Click to have the response be JSON instead of HTML.
152             <br/>Leave un-checked to compare the results with the content of the template.
153             <br/><br/>
154              
155             <button type="Submit">[%submit_word%] Form</button>
156              
157             </form>
158              
159             </body>
160             </html>
161             ];
162             # otherwise, send out the perl module
163             } else {
164            
165 0           return q[package PepperApps::HTMLExample;
166             # provides the example handler for Endpoint URI: /pepper_examples/html_example
167              
168             # promote better coding
169             use strict;
170             use warnings;
171              
172             # our request handler method
173             sub endpoint_handler {
174             my ($class,$pepper) = @_;
175             # the $class is there becauase this is a fake object; please see https://perldoc.perl.org/5.32.0/perlootut.html
176              
177             # if they submitted the form, prepare our silly example data structure
178             my $phrase_facts = {};
179             if ($pepper->{params}{provided_phrase}) {
180             # put together some useless facts
181             $phrase_facts = {
182             'Provided value'=> $pepper->{params}{provided_phrase},
183             'Value length' => length( $pepper->{params}{provided_phrase} ),
184             'Last three characters' => substr($pepper->{params}{provided_phrase}, -3, 3),
185             'Form submited' => $pepper->time_to_date(time(), 'to_date_human_time').' '.$pepper->{utils}->{time_zone_name}
186             };
187            
188             # if they checked the option to receive JSON back, return that data structure
189             # and Pepper.pm will convert it to JSON before sending out
190             return $phrase_facts if $pepper->{params}{return_json};
191             }
192            
193             # set the demo cookie?
194             my $pepper_cookie;
195             if ($pepper->{params}{set_demo_cookie}) {
196             $pepper_cookie = $pepper->random_string(20); # so we can display in the page
197             $pepper->set_cookie({
198             'name' => 'Pepper_Demo_Cookie', # could be any name
199             'value' => $pepper_cookie, # could be any string; nice to encrypt
200             });
201            
202             # even if we aren't setting it, let's show it to the nice people
203             } else {
204             $pepper_cookie = $pepper->{cookies}{Pepper_Demo_Cookie};
205             }
206            
207             # if they didn't check the return-JSON option, we will prepare some HTML via
208             # our basic Template Toolkit template. Please check out [%pepper_directory%]/templates/system/html_example.tt
209             return $pepper->template_process({
210             'template_file' => 'system/html_example.tt',
211             'template_vars' => {
212             'provided_phrase' => $pepper->{params}{provided_phrase},
213             'phrase_facts' => $phrase_facts,
214             'pepper_cookie' => $pepper_cookie,
215             },
216             });
217              
218             # all through, all done
219             }
220              
221             1;
222             ];
223              
224             }
225             }
226              
227             # return the template for our PSGI handler script
228             sub psgi_script {
229 0     0 0   my $self = shift;
230 0           return q[#!/usr/bin/env perl
231             # This is the PSGI script which runs Pepper as a Plack application
232             # Run via 'pepper start'
233             # Each worker/thread started by Plack/Gazelle (or other) will run a copy of this script
234              
235             # Please see the Perldoc notes below for more info about what this is.
236              
237             # load our plack modules
238             use Plack::Request; # access the incoming request info (like $q = new CGI;)
239             use Plack::Response; # handles the outgoing respose
240             use Plack::Builder; # enable use of middleware
241             use Plack::Middleware::DBIx::DisconnectAll; # protect DB connections
242             use Plack::Middleware::Timeout;
243             use File::RotateLogs; # log rotation
244             # probably more middleware to come
245              
246             # load up the modules we require
247             use Pepper;
248             use Pepper::Utilities;
249              
250             # be nice
251             use strict;
252             use warnings;
253              
254             # Here is the PSGI app itself; Plack needs a code reference, so work it like so:
255             my $app = sub {
256             # grab the incoming request
257             my $request = 'Plack::Request'->new(shift);
258             # set up the response object
259             my $response = 'Plack::Response'->new(200);
260            
261             # eval{} the real work, so we can maybe log the errors
262             eval {
263             my $pepper = Pepper->new(
264             'request' => $request,
265             'response' => $response,
266             );
267            
268             # put our logic for find and executing the needed handler into the $pepper object
269             $pepper->execute_handler();
270             # that will retrieve and ship out the content
271             };
272            
273             # catch the 'super' fatals, which is when the code breaks (usually syntax-error) before logging
274             if ($@ && $@ !~ /^(Execution stopped|Redirected)/) {
275             my $error_message = $@;
276              
277             # tie our UtilityBelt to the current request
278             my $utils = Pepper::Utilities->new(); # need this for logging
279             $utils->{response} = $response;
280             $utils->{request} = $request;
281            
282             # send the message to to client
283             if ($@ =~ /Plack::Middleware::Timeout/) {
284             # we want to log exactly what happened
285             $utils->logger({
286             'url' => 'https://'.$request->env->{HTTP_HOST}.$request->request_uri(),
287             'params' => $request->parameters,
288             },'timeouts');
289             # omnitool_routines.js will know how to handle this
290             $utils->send_response('Execution failed due to timeout.',3);
291              
292             # display via the utility belt
293             } else {
294             $utils->send_response('Fatal Error: '.$error_message,3);
295             }
296             }
297            
298             # vague server name
299             $response->header('Server' => 'Pepper');
300              
301             # consider setting these security headers
302             # if you have HTTPS/TLS configured:
303             # $response->header('Strict-Transport-Security' => 'max-age=63072000; includeSubdomains;');
304            
305             # to limit where JS/CSS and other content can originate. This limits to the same URL
306             # this is how you prevent inline JavaScript and Style tags, which is how XSS attacks happen.
307             # see: https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP
308             # $response->header('Content-Security-Policy' => qq{default-src 'self'; frame-ancestors 'self'; img-src 'self' data: 'self'; style-src 'self'});
309            
310             # finish up with our response
311             $response->finalize;
312             };
313              
314             # rotate the log every day
315             my $rotatelog = File::RotateLogs->new(
316             logfile => 'PEPPER_DIRECTORY/log/pepper_access_log.%Y%m%d%H%M',
317             linkname => 'PEPPER_DIRECTORY/log/pepper_access_log',
318             rotationtime => 86400,
319             maxage => 86400,
320             );
321              
322             # use Plack::Middleware::ReverseProxy to make sure the remote_addr is actually the client's IP address
323             builder {
324             # try not to have hung MySQL connections
325             enable "DBIx::DisconnectAll";
326             # set a reasonable timeout of 30 seconds
327             # response will be generated by error handling in main subroutine
328             # enable "Timeout", timeout => 30;
329             # use Plack::Middleware::ReverseProxy to make sure the remote_addr is actually the client's IP address
330             enable_if { $_[0]->{REMOTE_ADDR} eq '127.0.0.1' }
331             "Plack::Middleware::ReverseProxy";
332             # nice access log
333             enable 'Plack::Middleware::AccessLog',
334             format => '%P - %h - %t - %V - %r - %b - "%{User-agent}i"',
335             # the worker PID, the Remote Client IP, Local Time of Service, HTTP Type, URI, HTTP Ver,
336             # Response Length and client browser; separated by dashes
337             logger => sub {
338             $rotatelog->print(@_)
339             };
340             $app;
341             };
342              
343             # plack seems not to like 'exit' commands in these scripts
344             ];
345             }
346              
347             # sample SystemD config
348             sub systemd_config {
349 0     0 0   my $self = shift;
350 0           return qq{[Unit]
351             Description=Pepper/Plack Application Server
352             After=network.target
353             After=syslog.target
354              
355             [Service]
356              
357             # the number at the end of ExecStart determines the number of workers
358             ExecStart=/usr/local/bin/pepper start 30
359             ExecReload=pepper restart
360             ExecStop=pepper stop
361             Restart=on-failure
362             PIDFile=$ENV{HOME}/pepper/log/pepper.pid
363             KillSignal=SIGTERM
364             PrivateTmp=true
365             Type=forking
366              
367             # IMPORTANT: set this username to something other than root!
368             User=root
369              
370             [Install]
371             WantedBy=multi-user.target
372             };
373             }
374              
375             # sample Apache config
376             sub apache_config {
377 0     0 0   my $self = shift;
378            
379 0           return qq{# NOTE: You will need to enable several modules: a2enmod proxy ssl headers proxy_http rewrite
380            
381             # pepper application server virtual host
382             <VirtualHost *:443>
383             ServerName HOSTNAME.YOURDOMAIN.COM
384             # ServerAlias ANOTHER-HOSTNAME.YOURDOMAIN.COM
385             ServerAdmin you\@yourdomain.com
386              
387             # basic webroot options
388             DocumentRoot /var/www/html
389             Options All
390              
391             <Directory "/var/www/html">
392             Require all granted
393             </Directory>
394              
395             # enable HTTPS/TLS server -- this is my config, but adjust to taste
396             SSLEngine on
397             SSLProtocol -all +TLSv1.2 +TLSv1.3
398             SSLHonorCipherOrder on
399             SSLCipherSuite "ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES256-SHA384:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES128-SHA256"
400             SSLCompression off
401             Header always set Strict-Transport-Security "max-age=63072000; includeSubdomains;"
402              
403             # EFF/Certbot certificates are free and work very well.
404             # This is how I provision them:
405             # certbot --manual certonly --preferred-challenges=dns --email YOUR_EMAIL --server https://acme-v02.api.letsencrypt.org/directory --agree-tos -d YOUR_DOMAIN
406             # You will need to install 'certbot,' which involves adding a repo: https://certbot.eff.org/docs/install.html
407             SSLCertificateFile /etc/letsencrypt/live/YOUR_DOMAIN/fullchain.pem
408             SSLCertificateChainFile /etc/letsencrypt/live/YOUR_DOMAIN/fullchain.pem
409             SSLCertificateKeyFile /etc/letsencrypt/live/YOUR_DOMAIN/privkey.pem
410            
411             # try to speed things up
412             SetOutputFilter DEFLATE
413             SetEnvIfNoCase Request_URI "\.(?:gif|jpe?g|png)$" no-gzip
414              
415             # start our proxy config to handle requests via Plack
416             # You will need to enable the 'proxy' and 'proxy_http' modules
417             <Proxy *>
418             Order deny,allow
419             Allow from all
420             </Proxy>
421              
422             ProxyRequests Off
423             ProxyPreserveHost On
424              
425             # send *everything* to Plack -- this is how we can have any endpoint we want
426             ProxyPass / http://127.0.0.1:5000/
427             ProxyPassReverse / http://127.0.0.1:5000/
428              
429             # this is how you exempt files from being served via Plack
430             # ProxyPass /favicon.ico !
431            
432             RequestHeader set X-Forwarded-HTTPS "0"
433              
434             </VirtualHost>
435             };
436             }
437              
438             # sample Pepper script
439             sub sample_script {
440 0     0 0   my $self = shift;
441            
442 0           return q[#!env perl
443             # sample perl script working with Pepper
444              
445             # boilerplate
446             use strict;
447             use warnings;
448             use v5.26;
449              
450             # bring in pepper
451             use Pepper;
452             my $pepper = Pepper->new();
453              
454             # simple date trick
455             my $current_day_nice = $pepper->time_to_date( time(), 'to_date_human_dayname' );
456             say "Today is $current_day_nice";
457              
458             # if they have a database object, do some more tricks
459             if ($pepper->{db}) {
460            
461             # not a great example SQL, but here we go...
462             my ($databases, $dbkeys) = $pepper->sql_hash(qq{
463             select TABLE_NAME, TABLE_ROWS from information_schema.TABLES
464             where TABLE_TYPE='BASE TABLE'
465             });
466            
467             # output the results as JSON
468             say "Database table info in JSON:";
469             say $pepper->json_from_perl( $databases );
470            
471             }
472              
473             # if we were making changes to the database, we'd want
474             # to end with $pepper->commit(); to commit that DB transaction
475             # done
476             exit;
477             ];
478            
479             }
480              
481             1;
482              
483             __END__
484              
485             =head1 NAME
486              
487             Pepper::Templates
488              
489             =head1 DESCRIPTION
490              
491             This provides the templates needed by Pepper::Commander, which powers the 'pepper'
492             command line program. Please execute 'pepper help' in your shell for more details
493             on what is available.