Folks,

I am certain there is something really stupid going on in the chair to
keyboard interface here...but I am writing a new Lite app (have written
several), but for some reason, I keep getting an error that states that the
script did not return an application object...which I would expect to
happen if I forgot to put "app->start" at the end of the script...but it is
there.  I have tried commenting out my code block by block and am still
getting the same error...not sure how to track it down.  here is the source
code I have so far:

#!/usr/bin/env perl

use strict;
use warnings;
use 5.016;
use Carp qw(cluck carp croak);
use Data::Dumper;
use Net::LDAP;
use YAML::AppConfig;
use IO::Compress::Gzip 'gzip';
use POSIX;
use Digest::MD5 qw(md5_hex);
use Math::Random::Secure qw(irand);
use Mojolicious::Lite;
use Mojolicious::Sessions;
use Mojo::Log;
use Mojo::JSON qw(decode_json encode_json);
use Try::Tiny;
use Net::EmptyPort;
use Tie::Hash::Expire;

#=+ Setting the root directory as the absolute path where the executable
resides
use Cwd 'abs_path';
my $root_directory = abs_path().'/';

#=+ Set up logging
my $logLevel = 'debug';
my $ts = strftime("%Y-%m-%d", localtime(time));
my $log = Mojo::Log->new(path =>
$root_directory.'log/analytics_ui_log_'.$ts.'.log', level => $logLevel);

#=+ Check the root directory and any other needed directories
_checkDirectories($root_directory);

#=+ Need to initialize the application based on the configuration file
my $config = _config_init();

#=+ Set a much more secure key for our signed cookies using
Math::Random::Secure irand function, which IS
#   suitable for cryptographic functions.  We are using signed cookies that
are tamper-resistant as well
#   as forcing all traffic over SSL (https)
app->secrets([md5_hex(irand)]);

#=+ set up session(cookies) defaults
app->sessions(Mojolicious::Sessions->new);
app->sessions->cookie_name('Graph_Engine');
app->sessions->default_expiration($config->config->{'session_timeout'});
#=+ Force cookies to only be sent over SSL connection
app->sessions->secure(1);

#=+ Set environment variables
$ENV{'MOJO_MAX_MESSAGE_SIZE'}   =
$config->config->{'MOJO_MAX_MESSAGE_SIZE'};
$ENV{'MOJO_USERAGENT_DEBUG'}    = $config->config->{'MOJO_USERAGENT_DEBUG'};
$ENV{'MOJO_CONNECT_TIMEOUT'}    = $config->config->{'MOJO_CONNECT_TIMEOUT'};
$ENV{'MOJO_IOLOOP_DEBUG'}       = $config->config->{'MOJO_IOLOOP_DEBUG'};
$ENV{'MOJO_WEBSOCKET_DEBUG'}    = $config->config->{'MOJO_WEBSOCKET_DEBUG'};
$ENV{'MOJO_INACTIVITY_TIMEOUT'} =
$config->config->{'MOJO_INACTIVITY_TIMEOUT'};

#=+ Hypnotoad configuration.  This does no harm if we are just running morbo
app->config('hypnotoad' => {
              'listen'       => ['https://*:'.$config->config->{'port'}],
              'workers'      => $config->config->{'workers'},
              'multi_accept' => $config->config->{'multi_accept'}
            });

#=+ Set up and tie our blacklist hash, which automatically expires after 1
hour, perhaps this should be configurable?
my %blacklist;
tie %blacklist, 'Tie::Hash::Expire', { 'expire_seconds' => 3600 };

#========================================#
# Helpers                                #
#========================================#
#=+ Automatically gzip responses if the user agent will accept gzip
hook after_render => sub {
  my ($self, $output, $format) = @_;

  #=+ Check if "gzip => 1" has been set in the stash
  return unless $self->stash->{gzip};

  #=+ Check if user agent accepts GZip compression
  return unless ($self->req->headers->accept_encoding // '') =~ /gzip/i;

  #=+ Compress content with GZip
  $self->res->headers->content_encoding('gzip');
  gzip $output, \my $compressed;
  $$output = $compressed;
};

#========================================#
# Routes                                 #
#========================================#

#=+ Main login page.  This and '/' are the only routes that don't require
authentication to access
get '/login' => sub {
  my $self = shift;
  $self->stash(title => 'CGE Log-in');
  $self->stash(gzip => 1);
} => 'login';

#=+ If someone goes to https://app.com:[port]/, then redirect them to the
login page automatically
get '/' => sub {
  my $self = shift;
  $self->flash(message => 'Redirecting to login page.');
  $self->redirect_to('login');
};

post '/bonafides' => sub {
  my $self = shift;

  #=+ Grab the form elements from the POST form
  my $creds = $self->req->body_params->to_hash;

  #=+ Weed out anyone on the blacklist
  if (exists $blacklist{$creds->{'user'}}) {
    $self->flash(message => 'User has been blocked due to excessive failed
login attempts, try again in an hour or so!');
    $self->session(expires => 1);
    $self->rendered(404);
  }
  #=+ Now check credentials
  elsif (_authenticate($creds->{'user'},$creds->{'pass'}) == 1) {
    $self->session('failcount' => 0, 'uid' => $creds->{'user'},
'last_login' => time);
    $self->redirect_to('main');
  }
  else {
    my $failCount = 1;
    if ($self->session('failcount')) {
      $failCount = $self->session('failcount');
    }
    #=+ So long as the user has not tried 5 times in a row, let them keep
trying
    if($failCount <= 5) {
      $failCount++;
      $self->session('failcount' => $failCount);
      $self->rendered(404);
    }
    #=+ Otherwise, blacklist the user and boot them
    else {
      $self->session(expires => 1);
      $blacklist{$creds->{'user'}} = time;
      $self->rendered(404);
    }
  }
};

#=+ All routes in this group require authentication
group {
  under sub {
    my $self = shift;
    return 1 if $self->session('user');
    $self->redirect_to('/login');
    return undef;
  };

  #=+ Get to the main page once logged in
  get '/main' => {
    template => 'main',
    title    => 'Cray Graph Engine (CGE) web-interface',
    gzip     => 1
  };

  #=+ A quick way to ensure we are grabbing the prefix file from a secure
location with a current copy and
  #   also add our own prefixes
  get '/yasqe_prefixes/all.file.json' => sub {
    my $self = shift;
    my $prefixUrl = 'https://prefix.cc/popular/all.file.json';
    my $prefixUA = Mojo::UserAgent->new;
    my $prefixTX = $prefixUA->get($prefixUrl);
    if($prefixTX->success) {
      my $jsonHash = $prefixTX->res->json;
      $jsonHash->{'afq'} = 'http://jena.hpl.hp.com/ARQ/function#';
      $jsonHash->{'yd'}  = 'http://yarcdata.com/';
      $jsonHash->{'cray-prop'} = '
http://www.cray.com/analysisUI/node-property/';
      $self->render(json => $jsonHash);
    }
    else {
      $log->error('Unable to retrieve prefixes from
https://prefix.cc/popular/all.file.json (response code:
'.$prefixTX->res->code.')');
      $self->rendered(500);
    }
  };
};

#=+ Finally, let's get started!
app->start;

#========================================#
# Data maps                              #
#========================================#
#=+ reference:
https://www.centos.org/docs/5/html/CDS/cli/8.0/Configuration_Command_File_Reference-Access_Log_and_Connection_Code_Reference-LDAP_Result_Codes.html
my %ldap_error_codes = (0  => 'success',
                        1  => 'operation_error',
                        2  => 'protocol_error',
                        3  => 'time_limit_exceeded',
                        4  => 'size_limit_exceeded',
                        5  => 'compare_false',
                        6  => 'compare_true',
                        7  => 'auth_method_not_supported',
                        8  => 'strong_auth_required',
                        9  => 'ldap_partial_results',
                        10 => 'referral_ldap_v3',
                        11 => 'admin_limit_exceeded_ldap_v3',
                        12 => 'unavailable_critical_extension_ldap_v3',
                        13 => 'confidentiality_required_ldap_v3',
                        14 => 'sasl_bind_in_progress',
                        16 => 'no_such_attribute',
                        17 => 'undefined_attribute_type',
                        18 => 'inappropriate_matching',
                        19 => 'constraint_violation',
                        20 => 'attribute_or_value_exists',
                        21 => 'invalid_attribute_syntax',
                        32 => 'no_such_object',
                        33 => 'alias_problem',
                        34 => 'invalid_dn_syntax',
                        35 => 'is_leaf',
                        36 => 'alias_dereferencing_problem',
                        48 => 'inappropriate_authentication',
                        49 => 'invalid_credentials',
                        50 => 'insufficient_access_rights',
                        51 => 'busy',
                        52 => 'unavailable',
                        53 => 'unwilling_to_perform',
                        54 => 'loop_defect',
                        64 => 'naming_violation',
                        65 => 'object_class_violation',
                        66 => 'not_allowed_on_nonleaf',
                        67 => 'not_allowed_on_rdn',
                        68 => 'entry_already_exists',
                        69 => 'object_class_mods_prohibited',
                        71 => 'affects_multiple_dsas_ldap_v3',
                        80 => 'other',
                        81 => 'server_down',
                        85 => 'ldap_timeout',
                        89 => 'param_error',
                        91 => 'connect_error',
                        92 => 'ldap_not_supported',
                        93 => 'control_not_found',
                        94 => 'no_results_returned',
                        95 => 'more_results_to_return',
                        96 => 'client_loop',
                        97 => 'referral_limit_exceeded');


#========================================#
# Subroutines                            #
#========================================#

#=+ Set up connection to the LDAP server...doing this separately do avoid
duplicate code
sub _ldap_connect {
  my $ldap_connection;
  try {
    $ldap_connection = Net::LDAP->new($config->config->{'ldap_host'});
  }
  catch {
    $log->fatal('Could not connect to LDAP server:
'.$config->config->{'ldap_host'});
    croak $_;
  };
  return $ldap_connection;
}

#=+ Authenticate users against LDAP
sub _authenticate {
  my ($user,$pass) = @_;

  my $ldap = _ldap_connect();
  my $mesg = $ldap->bind;
  $mesg = $ldap->search(base => $config->config->{'search_base'},filter =>
'(&(uid='.$user.'))', attrs => ['dn']);

  #=+ First, did we get a response?
  if (!$mesg) {
    $ldap->unbind;
    $log->fatal('Unknown LDAP error');
    croak $_;
  }
  #=+ Did we find the user?
  elsif ($mesg->code == 0) {
    my $dn = $mesg->entry->dn;
    $mesg = $ldap->bind($dn, password => $pass);

    #=+ Does the user's password match?
    if ($mesg->code == 0) {
      $ldap->unbind;
      return 1;
    }
    else {
      $ldap->unbind;
      $log->error('Authentication failed for user: '.$user.' ERROR:
'.$mesg->code.'('.$ldap_error_codes{$mesg->code}.')');
      return 0;
    }
  }
  else {
    $ldap->unbind;
    $log->error('Authentication failed for user: '.$user.' ERROR:
'.$mesg->code.'('.$ldap_error_codes{$mesg->code}.')');
    return 0;
  }
}

#=+ For the purposes of looking up system users in order to share database
access
sub _user_lookup {
  my($value) = @_;
  my @results;
  my $ldap = _ldap_connect();
  my $mesg = $ldap->bind;

  #=+ We are doing a wildcard search such that * represents zero or more
characters on either end, which is really just
  #   the equivalent of CONTAINS.  Later, we may decide that a
fuzzy/approximate match is desired...in which case, change
  #   (for example) uid=*searchterm* to uid~=*searchterm*
  $mesg = $ldap->search(base => $config->config->{'search_base'},filter =>
'(|(uid=*'.$value.'*)(cn=*'.$value.'*))');

  #=+ Each LDAP record entry is returned with the attributes stored in an
array of anonymous hashes, therefore,
  #   we have to iterate over each entry and the array of attributes to
grab the values we are interested in
  foreach my $entry($mesg->entries) {
    my %temp;
    foreach my $kv(@{$entry->{'asn'}->{'attributes'}}) {
      #=+ The regext here is looking for a value that is EXACTLY cn, uid,
or homeDirectory (case-sensitive).  Feel free to add more values if they
are useful
      if ($kv->{'type'} =~ m/^(:?cn|uid|homeDirectory)$/) {
        $temp{$kv->{'type'}} = $kv->{'vals'}->[0];
      }
    }
    push @results, \%temp;
  }
  return \@results;
}

#=+ Check to make sure any supplied directories exist, are readable, and
writeable
sub _checkDirectories {
  my @input = @_;
  foreach my $dir(@input) {
    unless(-d $dir) {
      $log->fatal('Directory does not exist: '.$dir);
      croak $dir.' : Directory does not exist.';
    }
    unless(-r $dir) {
      $log->fatal('Directory is not readable: '.$dir);
      croak $dir.' : Directory is not readable';
    }
    unless(-w $dir) {
      $log->fatal('Directory is not writable: '.$dir);
      croak $dir.' : Directory is not writable.';
    }
  }
  return 1;
}

#=+ Read in our configuration file
sub _config_init {
  #=+ no options to the init routine for now...may revisit later
  #=+ First, load up the master config file
  my $yaml_config;
  my $config_file_name = $root_directory.'analytics_ui_config.yaml';
  if(-e $config_file_name && -r _) {
    $yaml_config = YAML::AppConfig->new(file => $config_file_name);
  }
  else {
    $log->fatal('Could not initialize application: config file does not
exist or has permissions issues: '.$config_file_name);
    croak 'Could not initialize application: config file does not exist or
has permissions issues: '.$config_file_name;
  }

  #=+ Now, load the local/user's config file and merge it with the master
if it exists
  #   The user's settings will take precedence over the master config.
  #   For now, the location and file name are hard-coded.  If anyone thinks
this should be configurable, go for it
  if(-e (getpwuid $>)[7].'/analytics_ui_config.yaml' && -r _) {
    $yaml_config->merge(file => (getpwuid
$>)[7].'/analytics_ui_config.yaml');
  }

  #=+ Need to check if config file sets listen port explicitly and if it is
available
  if (!exists $yaml_config->config->{'port'}) {
    #=+ Force the search for a free port to start at 3000.  This is
completely arbitrary and was chosen because that
    #   is the default port that Mojolicious shows in the docs.  You could
set this to any valid port
    my $freePort = 3000;
    while(check_port($freePort)) {
      $freePort++;
    }
    $yaml_config->config->{'port'} = $freePort;
  }
  elsif(check_port($yaml_config->config->{'port'})) {
    my $oldPort = $yaml_config->config->{'port'};
    my $freePort = $oldPort + 1;

    #=+ Find the next available port
    while(check_port($freePort)) {
      $freePort++;
    }

    #=+ Use next available port
    say 'Port '.$oldPort.' is already in use, will use the next available
port: '.$freePort;
    $yaml_config->config->{'port'} = $freePort;
  }



  #=+ Need to alter the main javascript file to use the desired backend
server name and port
  #my $javascript_file_name = $root_directory.'public/js/analytics_ui.js';
  #if(-e $javascript_file_name && -r _ && -w _) {
  #  my $javascript_file = read_file($javascript_file_name);
  #  my $host_port =
$yaml_config->config->{'host'}.':'.$yaml_config->config->{'port'};
  #  $javascript_file =~ s/var backend \= \"[^"]+\"/var backend \=
\"$host_port\"/;
  #  open(my $JS_FILE,'>',$javascript_file_name);
  #  say {$JS_FILE} $javascript_file;
  #  close($JS_FILE);
  #}
  #else {
  #  $log->fatal('Could not initialize application: javascript file does
not exist or has permissions issues: '.$javascript_file_name);
  #  croak 'Could not initialize application: javascript file does not
exist or has permissions issues: '.$javascript_file_name;
  #}
  return $yaml_config;
}

-- 
You received this message because you are subscribed to the Google Groups 
"Mojolicious" group.
To unsubscribe from this group and stop receiving emails from it, send an email 
to [email protected].
To post to this group, send email to [email protected].
Visit this group at https://groups.google.com/group/mojolicious.
For more options, visit https://groups.google.com/d/optout.

Reply via email to