Search This Blog

Wednesday 31 December 2014

PSGI and Plack Basics

Background

Web applications run within the context of a web server. Web client (browser, web crawler, a script making API calls etc) sends a request to a web server (Apache, Nginx, Starman etc) that the server either maps to a file system resource (for static pages) or, in case of dynamic pages, dispatches, according to the server configuration, to the relevant web application.

The purpose of the PSGI specification is to help create portable web applications, applications that are independent of the web server environment they are run in, and can be, therefore run with ease under different web servers.

A PSGI compatible web application is a code reference, that accepts input and provides output  in a prescribed format.

PSGI Middleware is a PSGI application, that can run another PSGI (web) application and can do preprocessing of an HTTP request and/or postprocessing after HTTP response is received. It is a wrapper around the web application/framework and it sits between the web server and the application/framework. From the server perspective, the middleware component is a web application and from the web application perspective, it is a web server.

PSCI Specification

PSGI application accepts input in the form of a hash reference with CGI like header information and psgi.xxx/psgix.yyy keys. The prescribed format of its output is:
  1. array reference containing three elements:
    1. HTTP status
    2. arrayref with HTTP headers
    3. response body:
      1. arrayref with the response content
      2. a handle (Perl built-in filehandle or an IO::Handle-like object) containing the response body as byte strings 
  2. code reference for a delayed/streaming response

    Example of a hashref input providing information about the environment:

    $env = {
              'SERVER_PROTOCOL' => 'HTTP/1.1',
              'HTTP_ACCEPT_LANGUAGE' => 'en-US,en;q=0.5',
              'psgi.input' => \*{'HTTP::Server::PSGI::$input'},
              'psgi.errors' => *::STDERR,
              'psgix.io' => bless( \*Symbol::GEN1, 'IO::Socket::INET' ),
              'HTTP_ACCEPT' => 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
              'psgi.version' => [
                                  1,
                                  1
                                ],
              'HTTP_USER_AGENT' => 'Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:34.0) Gecko/20100101 Firefox/34.0',
              'REMOTE_PORT' => 45541,
              'SCRIPT_NAME' => '',
              'REQUEST_URI' => '/',
              'psgix.input.buffered' => 1,
              'PATH_INFO' => '/',
              'psgi.run_once' => '',
              'REQUEST_METHOD' => 'GET',
              'HTTP_ACCEPT_ENCODING' => 'gzip, deflate',
              'psgi.url_scheme' => 'http',
              'psgi.multithread' => '',
              'psgi.streaming' => 1,
              'REMOTE_ADDR' => '127.0.0.1',
              'psgi.multiprocess' => '',
              'QUERY_STRING' => '',
              'SERVER_PORT' => 5000,
              'SERVER_NAME' => 'localhost',
              'HTTP_HOST' => '127.0.0.1:5000',
              'psgi.nonblocking' => '',
              'HTTP_CONNECTION' => 'keep-alive',
              'psgix.harakiri' => 1
            };


    Example of output with an arrayref body:

    $response = [
         200,
         ['Content-Type' => 'text/html'],
         ['<h2>Hello World</h2>', 'How are you today?' ]
    ]

    Example of output with a handle body:

    $body = new IO::File $file, "r"
    $body = MyClass->new                        # must implement read(), possibly seek()

    $response = [
         200,
         ['Content-Type' => 'text/plain'],
        $body,
    ]

    Plack

    Plack is a Perl module, a toolkit for using the PSGI stack: middleware, helpers and adapters to web servers. Standard adapters included in the module are for CGI, FCGI, Apache 1 and 2 and HTTP::Server::Simple. Others can be found on CPAN. Plack comes with its own standalone server HTTP::Server::PSGI.

    Utilities provided by Plack are aimed at web server, middleware and web framework authors. While it is possible to write PSGI web applications, the recommended way is to build web applications on top of web frameworks. Catalyst, Dancer, Mojolicious are some of the web frameworks supporting PSGI. PSGI enabled web frameworks have an adaptor/engine (middleware component) conforming to the PSGI specification.

    Examples

    An example of running a PSGI application in three different ways:

     

    1. running the application using the built-in Plack server
    2. running the application using the Apache server
    3. running the application using the built-in Plack server with Apache as a proxy server 

    Simple PSGI application


     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    #!/usr/bin/perl
    use strict;
    use warnings;
    
    use JSON qw(to_json);
    
    my $data = {
            data => {
                    message1 => 'Hello world',
                    message2 => 'Hello family',
                    message3 => 'Hello son',
            },
    };
    
    my $serialized = to_json($data);
    
    my $app = sub {
      my ($env) = @_;              # PSGI input, not used in this simple app
    
      return [                     # PSGI output
        '200',
        [ 'Content-Type' => 'application/json' ],
        [ $serialized ],
      ];
    };
    

     

    More complex example


     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    #!/usr/bin/perl
    
    =head2 plack_app1.pl
    
    run: plackup -o localhost -p 8000 plack_app1.pl
    
    =cut
    
    use strict;
    use warnings;
    use v5.018;
    use utf8;
    
    #use Data::Dumper qw(Dumper);
    #$Data::Dumper::Sortkeys = 1;
     
    use Plack::Request;
    use CHI;
    
    =head3 Cache messages for later display together with the latest message
    
    messages are cached on the file system, so will survive the server restart
    the storage is limited to 4kb
    
    =cut
    
    my $cache = CHI->new( driver => 'FastMmap',
        root_dir   => '/tmp/plack_app1_cache',
        cache_size => '4k');
    my $cache_key = "${0}_message";
    $cache->remove($cache_key);
     
    =head3 Plackup subroutine reference
    
    input:  hashref containing request information
    output: arrayref of the prescribed format
    
    =cut
    
    my $app = sub {
        my $env = shift;
    
        # Create the HTML form where a message can be input
        my $html    = _create_form();
    
        # Wraps the $env in a Plack request object 
        my $request = Plack::Request->new($env);
         
        # Recover old messages for display, store them all
        my $old_messages = $cache->get($cache_key) || '';
    
        # The request is a sent form with filled in message field
        if ($request->param('message')) {
    
            say "[$request->param('message')]";
    
            my $current_message = $request->param('message');
            my $message = $old_messages . "<br />$current_message";
            $message = (length $message > 2000) ? $current_message : $message;
            $cache->set($cache_key, $message);
    
            # Append the messages to the form html
            $message = $old_messages . "<br /><b>$current_message</b>";
            $html  .= "You told me:<br />$message";
        }
        elsif ($old_messages) {
            $html  .= "Old messages: <br />$old_messages";
        }
     
        return [
                '200',
                [ 'Content-Type' => 'text/html' ],
                [ $html ],
            ];
    };
    
    =head2 SUBROUTINES
    
    =head3 _create_form
    
    =cut
     
    sub _create_form {
        return q{
        <hr>
        <form>
            <input name="message">
            <input type="submit" value="Tell me">
        </form>
        <hr>
        }
    }
    
    Inspired by Dave Cross's Plack application example

    A) Built-in web server


    plackup  /path/to/my/my_app.psgi

    B) Apache 2 - using directly as a web server


    In /etc/apache2/sites-enabled/psgi_apps.conf:

    <VirtualHost *:4000>
            LogLevel debug
            ErrorLog ${APACHE_LOG_DIR}/error_psgi.log
            CustomLog ${APACHE_LOG_DIR}/access_psgi.log combined
    
            PerlOptions +Parent        
            
            <Location /test_my_app>          
              SetHandler perl-script
              PerlResponseHandler Plack::Handler::Apache2    # PSGI adaptor for apache
              PerlSetVar psgi_app  /var/www/apps/my_app.psgi
            </Location>
    
    </VirtualHost>
    

    C) Apache 2 - using as a proxy for the  built-in Plack web server


    The application runs locally on port 5000, on which the built-in Plack web server is listening (http://localhost:5000/). Locally or externally (if configured) the application can be accessed through:
    • http://some_hostname/test_my_app
    • http://localhost:4000/test_my_app

    a) cd /var/www; plackup  --host localhost /my_app.psgi 
    HTTP::Server::PSGI: Accepting connections at http://localhost:5000/

    b) Apache configuration - In /etc/apache2/sites-enabled/psgi_apps.conf:

    <VirtualHost *:4000>
            LogLevel debug
            ErrorLog ${APACHE_LOG_DIR}/error_psgi.log
            CustomLog ${APACHE_LOG_DIR}/access_psgi.log combined
    
            PerlOptions +Parent
               
            ## Proxying an application
            ##------------------------ 
            ProxyPass /test_my_app http://localhost:5000/
            ProxyPassReverse /test_my_app http://localhost:5000/
            
            <Location /test_my_app>
              Require all granted       # Apache 2.4
                      or
              Order allow,deny          # Apache 2.2
              Allow from all
    
            </Location>
    
    </VirtualHost>
    


     

    Note


    For the proxying to work, several prerequisites need to be satisfied:
    1. Apache proxy and proxy_http modules must be loaded 
      1. on Ubuntu:  a2enmod proxy proxy_http
    2. Uncomment the <proxy *> block in mods-enabled/proxy.conf
    3. Create a new site configuration sites-available/psgi_apps.conf and enable the configuration (on Ubuntu: a2ensite psgi_apps)
    4. In sites-available/psgi_apps.conf: allow access to the proxied location if the proxy configuration by default disallows access


    No comments:

    Post a Comment

    Note: only a member of this blog may post a comment.