http://qs321.pair.com?node_id=11118092

Discipulus has asked for the wisdom of the Perl Monks concerning the following question:

hello nuns and monks,

I want to develop for a bunch of friends (no more than 30) a little webservice consumed by a client I provide them and I want to take the occasion to try mojolicious as it was several times suggested to me (thanks ;).

Security will be not a big concern as nothing critical will pass on, but I want to be sure my service will be used only if I can control the genuinity of the client.

Basically I will provide two pieces of text: TEXT_A and TEXT_B.

The client will be a simple perl script using LWP::UserAgent (see below for the code used as test) but I suppose to know how to deal with this (well I have to reuse the cookie set by the server. I only have to tell it to initialize a jar? LWP::UserAgent->new(cookie_jar => {}) no?).

The client-server interaction will be something like this:

  1. the client send a request using basic authentication (user and password sent in the requested URL)
  2. the server check if username and password sent by the client are valid
  3. the server check if the remote IP is allowed and, if so and authentication was done, set up a cookie with a session and send back as only response TEXT_A.
  4. the client receive TEXT_A and show it.
  5. the client, after other short activities, ask to the server for TEXT_B
  6. the server check if there is a valid session and, if so, send TEXT_B
  7. the server close the session.

After some reading and some unwise cut and paste, I imagined something like the following but I'd very like to ear from you where I'm wrong and how things can be done in a better way.

use strict; use warnings; use Mojolicious::Lite; plugin 'ClientIP'; plugin 'basic_auth_plus'; # sample data for authentication my %accepted_IPs = ( '8.8.8.8' => 1 ); my %users = ( usr1 => 'pwd1', usr2 => 'pwd2'); # sample data to send back to client my $text_a = 'Né più mai toccherò le sacre sponde'; my $text_b = 'ove il mio corpo fanciulletto giacque,'; # I expect all non specified routes to answer 404, right? # get text_a. Steps 2) and 3) of the above description get '/get_first' => sub { my $self = shift; my $remote_IP = $self->client_ip; # check username and password my ($href, $auth_ok) = $self->basic_auth( realm => sub { # @_ contains username and password if ( exists $users{$_[0]} and $users{$_[0]} eq $_[1]) +{ # log the correct login $self->log->info( "$_[0] from $remote_IP login OK" + ); return 1; } } ); # reject unwanted remote IP unless ( exists $accepted_IPs{ $remote_IP } ){ # log unwanted IP $self->log->warn( "$href->{username} logged from unwanted IP: +$remote_IP" ); # reject the request return $self->render( status => 401, text => 'unauthorized', ) } # process the request.. if ( $auth_ok ) { # set the session cookie valid for 2 minutes $self->session( expiration => 120, is_my_user_authenticated => + 1 ); # log the action $self->log->info( "sent TEXT_A to $remote_IP" ); # render TEXT_A return $self->render( status => 200, text => $text_a, ); } # .. or reject it else { # log a failed attempt $self->log->warn( "bad login attempt from valid IP $remote_IP + using user $href->{username}" ); # reject return $self->render( status => 401, text => 'unauthorized', ); } }; # get text_b. Steps 6) and 7) of the above description get '/get_second' => sub { my $self = shift; my $remote_IP = $self->client_ip; # check the previously set cookie session if ( $self->session( 'is_my_user_authenticated') == 1 ) { # expire the session cookie $self->session( expires => 1, is_my_user_authenticated => 0 ); # log the action $self->log->info( "sent TEXT_B to $remote_IP" ); # render TEXT_B return $self->render( status => 200, text => $text_b, ); } else { # log the action $self->log->warn( "unexpected request of TEXT_B from $remote_I +P: rejected ". "authenticated: $self->session( 'is_my_use +r_authenticated') ". "expiration: $self->session( 'expiration') +"); # reject return $self->render( status => 401, text => 'unauthorized', ); } }; app->start;

Here the client I used as test ( server: 10.0.0.1 client: 8.8.8.8 ):

use strict; use warnings; use LWP::UserAgent; use HTTP::Request::Common; my $ua = LWP::UserAgent->new(); $ua->cookie_jar( {} ); print "FIRST REQUEST\n\n"; my $request = GET 'http://10.0.0.1/get_first'; $request->authorization_basic('usr1', 'pwd1'); my $response = $ua->request($request); print $response->as_string(); sleep 3; print "SECOND REQUEST\n\n"; my $request2 = GET 'http://10.0.0.1/get_second'; my $response2 = $ua->request($request2); print $response2->as_string(); sleep 3; print "\nATTENTION: next should fail..\n\n"; my $request3 = GET 'http://10.0.0.1/get_second'; my $response3 = $ua->request($request3); print $response3->as_string();

with this output (dont mind the encoding of the ouput.. ;):

FIRST REQUEST HTTP/1.0 200 OK Date: Mon, 15 Jun 2020 11:26:18 GMT Date: Mon, 15 Jun 2020 11:26:18 GMT Server: HTTP::Server::PSGI Content-Length: 38 Content-Type: text/html;charset=UTF-8 Client-Date: Mon, 15 Jun 2020 11:26:20 GMT Client-Peer: 10.0.0.1:80 Client-Response-Num: 1 Set-Cookie: mojolicious=eyJleHBpcmF0aW9uIjoxMjAsImV4cGlyZXMiOjE1OTIyMj +A0OTgsImlzX215X3VzZXJfYXV0aGVudGljYXRlZCI6MX0---d9029065f9456259d6cfd +b50c3adee1d4a2f3b65; expires=Mon, 15 Jun 2020 11:28:18 GMT; path=/; H +ttpOnly; SameSite=Lax N├® pi├╣ mai toccher├▓ le sacre sponde SECOND REQUEST HTTP/1.0 200 OK Date: Mon, 15 Jun 2020 11:26:21 GMT Date: Mon, 15 Jun 2020 11:26:21 GMT Server: HTTP::Server::PSGI Content-Length: 38 Content-Type: text/html;charset=UTF-8 Client-Date: Mon, 15 Jun 2020 11:26:24 GMT Client-Peer: 10.0.0.1:80 Client-Response-Num: 1 Set-Cookie: mojolicious=eyJleHBpcmF0aW9uIjoxMjAsImV4cGlyZXMiOjEsImlzX2 +15X3VzZXJfYXV0aGVudGljYXRlZCI6MH0---2f2a402a06de6b37f1eac53b7cbb79d5e +08f04e7; expires=Thu, 01 Jan 1970 00:00:01 GMT; path=/; HttpOnly; Sam +eSite=Lax ove il mio corpo fanciulletto giacque, ATTENTION: next should fail.. HTTP/1.0 401 Unauthorized Date: Mon, 15 Jun 2020 11:26:24 GMT Date: Mon, 15 Jun 2020 11:26:24 GMT Server: HTTP::Server::PSGI Content-Length: 12 Content-Type: text/html;charset=UTF-8 Client-Date: Mon, 15 Jun 2020 11:26:27 GMT Client-Peer: 10.0.0.1:80 Client-Response-Num: 1 Client-Warning: Missing Authenticate header unauthorized

and these log lines (newlines added for readability):

HTTP::Server::PSGI: Accepting connections at http://0:80/ [2020-06-15 13:26:18.41707] [29291] [debug] [MfmL-Zeg] GET "/get_first +" [2020-06-15 13:26:18.41765] [29291] [debug] [MfmL-Zeg] Routing to a ca +llback [2020-06-15 13:26:18.41810] [29291] [info] [MfmL-Zeg] usr1 from 8.8.8. +8 login OK [2020-06-15 13:26:18.41820] [29291] [debug] Your secret passphrase nee +ds to be changed [2020-06-15 13:26:18.41832] [29291] [info] [MfmL-Zeg] sent TEXT_A to 8 +.8.8.8 [2020-06-15 13:26:18.41865] [29291] [debug] [MfmL-Zeg] 200 OK (0.00157 +5s, 634.921/s) 8.8.8.8 - - [15/Jun/2020:13:26:18 +0200] "GET /get_first HTTP/1.1" 200 + - "-" "libwww-perl/6.26" [2020-06-15 13:26:21.60148] [29291] [debug] [cC2TWI3Q] GET "/get_secon +d" [2020-06-15 13:26:21.60208] [29291] [debug] [cC2TWI3Q] Routing to a ca +llback [2020-06-15 13:26:21.60249] [29291] [info] [cC2TWI3Q] sent TEXT_B to 8 +.8.8.8 [2020-06-15 13:26:21.60272] [29291] [debug] [cC2TWI3Q] 200 OK (0.00123 +4s, 810.373/s) 8.8.8.8 - - [15/Jun/2020:13:26:21 +0200] "GET /get_second HTTP/1.1" 20 +0 - "-" "libwww-perl/6.26" [2020-06-15 13:26:24.68206] [29291] [debug] [jscLU00Y] GET "/get_secon +d" [2020-06-15 13:26:24.68244] [29291] [debug] [jscLU00Y] Routing to a ca +llback Use of uninitialized value in numeric eq (==) at /root/srv01/srv01.pl +line 74, <DATA> line 755. [2020-06-15 13:26:24.68275] [29291] [warn] [jscLU00Y] unexpected reque +st of TEXT_B from 8.8.8.8: rejected authenticated: Mojolicious::Contr +oller=HASH(0x316b0a0)->session( 'is_my_user_authenticated') expiratio +n: Mojolicious::Controller=HASH(0x316b0a0)->session( 'expiration') [2020-06-15 13:26:24.68341] [29291] [debug] [jscLU00Y] 401 Unauthorize +d (0.001321s, 757.002/s) 8.8.8.8 - - [15/Jun/2020:13:26:24 +0200] "GET /get_second HTTP/1.1" 40 +1 - "-" "libwww-perl/6.26"

I'd really like to know: is the above sketch correct, in its logic and syntax? The basic authentication and the session management is appropriate (expiration, is_my_user_authenticated and the expires parameters in the cookie)? The logging is done in the right way? I missed something important? How to deploy? I used plackup for testing purpose. I see debug infos in logs: why they are there (happy to see them atm..)? What strings like [MfmL-Zeg] means in loglines?

Thanks for reading

L*

There are no rules, there are no thumbs..
Reinvent the wheel, then learn The Wheel; may be one day you reinvent one of THE WHEELS.

Replies are listed 'Best First'.
Re: first steps with Mojolicious::Lite
by davido (Cardinal) on Jun 17, 2020 at 16:47 UTC

    I intended to comment sooner and forgot to do so.... so late to the party but here are some things that I think might be helpful:

    One of the overarching goals of a MVC framework is to keep the controller as thin as possible. You want to push environmental decisions to configuration that can be decided at build time, business logic into the model, and leave in your application just startup decisions, and routes with thin controllers that just wire end points to the model, while passing the results through to the appropriate views.

    In your application there's an opportunity to simplify your controller subs (the hybrid route / controller subs) by abstracting the business logic into its own module that can be accessed through a helper. I prefer to have helpers for physical resources such as storage, database, etc, that get instantiated at startup, and other helpers for business logic that, when instantiated, have the physical resource objects injected through their constructors. Dependency injection. That allows testing to happen in isolation of various components. But the biggest win is that by keeping the controllers thin, you can to a large degree look upon the framework as just one way of presenting your model. If next week you decided that you wanted to go with CGI or Catalyst or Dancer, the model shouldn't have to change at all.

    A small reference app that demonstrates this might be something like:

    #!/usr/bin/env perl use Mojolicious::Lite; use Time::HiRes 'time'; use FindBin qw($Bin); use lib "$Bin/lib"; my $startup = time; plugin Config => {file => "$Bin/my-hello-app.beta.conf"}; helper storage => sub { # instantiate a storage class; filesystem, database, or in-memory +based on config... # As long as the present the same interface, it doesn't matter. }; helper useragent => sub {return state $ua = Mojo::UserAgent->new;}; helper myhello => sub { my $c = shift; return state $hello_obj = do { require Model::Hello; Model::Hello->new( storage => $c->storage, ua => $c->useragent, ); }; }; get '/hello' => sub { my $c = shift; $c->render(json => $c->myhello->hello); }; get '/_live' => sub { shift->render( json => { alive_since => (time - +$startup) } ) }; app->start;

    This does not demonstrate an actual Model class.

    Now look how small your controller subs have become. Of course this is just a hello, but the controller can pass to the model calls whatever stash / param values it needs to. The application doesn't deal directly with storage, either, making the model and the application easier to test.

    The other things I wanted to mention: Mojolicious comes with a user agent. No need to bring in LWP::UserAgent. And it includes its own testing framework:

    #!/usr/bin/env perl use Test::More; use Test::Mojo; use FindBin qw($Bin); use lib "$Bin/../lib"; my $t = Test::Mojo->new(Mojo::File->new("$Bin/../hello")); $t->get_ok('/_live') ->status_is(200) ->json_has('/alive_since'); $t->get_ok('/hello') ->status_is(200) ->json_is({success => 1, code => 200, message => 'OK'}); done_testing();

    Dave

Re: first steps with Mojolicious::Lite
by alexander_lunev (Pilgrim) on Jun 16, 2020 at 09:03 UTC

    Greetings! And paraphrasing a famous quote: For those about to web (with Mojolicious) - we salute you!

    First of all, as of common good practices of programming, you should avoid duplicate code. You have three chunks in your code that do the same thing - shows "401 unauthorised" and do some logging about it. So, make it in one place. Like this:

    helper deny_with_message => sub { my $self = shift; my $message = shift; $self->app->log->warn($message); $self->render( status => 401, text => 'unauthorized', ); };

    And then you could invoke it like this:

    return $self->deny_with_message("$href->{username} logged from unwanted IP: $remote_IP");

    And then about under... People already told you to use under, I could only support this in some usual cases, though, in my opinion, in your particular case using under will not make your code readable or more logical - it will be sugar for sugar. Using under is great when you have usual web application with login page and many many pages that is under one and the same check (like, "do we have a cookie?"). Your case is different - there's one page that checks IP, login and password, and another page that checks session cookie. So you'll have to use two under subs which, again, will not make your code ligher or more readable.

    I'm using under myself, both in session-cookie-based apps and in JWT-based apps, and it's great in apps with whole lot of pages that could be viewed after one same check, but I'm doubt that your code will be better if you rewrite your app with under. So just refactor your code to not repeat itself and you'll be fine.

    P.S.: If you're didn't saw this already, I recommend you to see Mojocasts.

Re: first steps with Mojolicious::Lite
by haukex (Archbishop) on Jun 15, 2020 at 22:01 UTC

    I unfortunately don't have the time to dig into this in detail right now, so for now just a few short comments: I posted an example Mojo app with login support here (which I expanded on a bit here), it shows the use of under as suggested by the AM - I also strongly recommend you refactor your authentication code out of the handlers, that'll make for a better design. Also, don't use Basic Authentication, at least use Digest Authentication; there's Mojolicious::Plugin::DigestAuth but I haven't tried this myself yet. More of my Mojo examples are on my scratchpad.

Re: first steps with Mojolicious::Lite
by Anonymous Monk on Jun 15, 2020 at 15:29 UTC
    An idea: use Mojo's under sub { ... } to run the code you need to run in both cases (i.e. check authentication), then provide two short routes get '/get_X' => sub { shift->render(...); }. If the code in under sub {...} returns a truthy value, the get routes get a chance to fire. If it returns undef, Mojo behaves as if they don't exist (so, 404 for unauthenticated users). The code in under can also call shift->render(...) instead of returning undef to send its own response. See Mojolicious::Guides::Routing for a better explanation.
      Thanks Anonymous Monk for the hint,

      I looked at Under and it seems powerful, maybe a bit too much framew-orcish at the moment. Or, if you prefere, too much sugar for my taste. Surely for my ignorance but when I see my $foo = $r->under('/foo')->to('foo#baz'); I cannot see immediately what is passing on.

      But anyway thanks for the hint; something to study.

      But, if I dont misunderstand myself, I have not duplicated code in the actual code: the IP check and the authentication only happens in get_first while in get_second only a cookie is checked, no?

      L*

      There are no rules, there are no thumbs..
      Reinvent the wheel, then learn The Wheel; may be one day you reinvent one of THE WHEELS.
        But, if I dont misunderstand myself, I have not duplicated code in the actual code: the IP check and the authentication only happens in get_first while in <c>get_second only a cookie is checked, no?

        Well yes, but since http is stateless, what happens if get_second gets called first?

        I'm developing some Mojolicious::Lite apps at the time, and my layout is as follows

        #!/usr/bin/perl use Mojolicious::Lite; use Foo::Blargh; # yadda yadda, other usefull stuff get "/login" => sub { my $c = shift; # $c is the controller object $c->render('login'); # renders templates/login.html.ep }; # this sub checks username and password post "/login" => sub { validate_user(shift); # the sub gets the controller }; get "/logout" => sub { my $c = shift; $c->cookie(MySession => 'none'); # unset cookie $c->redirect_to("/login"); }; # with the following, all following routes require # a logged-in user and a proper cookie under sub { validate_cookie(shift); }; # more routes get "/foo" => sub { my $c = shift; ... $c->render($whatever); }; get "/bar" => sub { my $c = shift; ... $c->render($something_else); }; # all set up, start the application; app->start; ### ## # subs sub validate_cookie { my $c = shift; my $cookie = $c->cookie('MySession'); if (! $cookie) { $c->redirect_to('/login'); return; # important! } else { # more checks to see if cookie is valid ... } 1; } sub validate_user { my $c = shift; my $u = $c->param('user'); my $p = $c->param('pass'); if ( login($u,$p) ) { # set cookie. Multiple cookies can be set by calling # $c->cookie() with other key/value pairs more than 1 time $c->cookie(MySession => generate_cookie() ); $c->redirect_to('/foo'); # whatever is appropriate return; } $c->stash(error => 'wrong user or password'); # gets rendered in / +login $c->render('login'); return; } sub login { # validate user and password: database, ldap, ... # returns 1 on success, nothing on failure } sub generate_cookie { my $n = shift || 42; my @chars = ('A'..'Z','a'..'z',0..9,'+','/' ); # we use the BASE64 + chars join '', map{ $chars[rand @chars] }1..$n; # session ID }

        Works for me ;-)

        perl -le'print map{pack c,($-++?1:13)+ord}split//,ESEL'

        Hi Discipulus,

        I have really looked forward to assisting you in some trivial matter because of the MAJOR enlightenment and joy you add to Perlmonks!!

        You really need to look at under because it is exactly what you want, I'm pretty sure!

        I'll try to provide some code specific to your example, but this is from a small web site (like 200 occasional users)

        my $onlyLoggedIn = $r->under('/admin' => \&loggedIn); $onlyLoggedIn->post('uploadFile')->to('files#insert'); $onlyLoggedIn->delete('files')->to('files#delete'); sub loggedIn { my $c = shift; if ($c->session('login')) { return 1; } $c->render( template => 'login', title => 'website title', status => 401, ); return 0; }; # elsewhere sub login { my $self = shift; my $name = $self->param('user'); my $password = $self->param('password'); my $responseCode = 401; # Pretty sure this hashes the param and checks it against the +hashed database entry if (Something::Model::Users::login($name,$password)) { # $self->signed_cookie(loggedIn => 1); $self->session(expiration => 60*60*10); $self->session(login => $name); $responseCode = 200; $self->app->log->warn("$name logged in."); } else { $self->app->log->warn("Invalid login - '$name'"); } $self->render(data => '',status => $responseCode); }

        So we have all urls /admin/whatever for only authenticated users.

        Something I recently learned

        $self->helper( onlyauth => sub { my ($c,$block) = @_; if ($c->session('login')) { return $block->() if $block; } });

        So I can use this in my templates!! I have an admin bar at the top of every regular page that only logged in users see

        %= onlyauth begin %= include 'adminbar' % end
Re: first steps with Mojolicious::Lite
by trippledubs (Deacon) on Jun 17, 2020 at 03:49 UTC
    #!/usr/bin/env perl #server.pl use strict; use warnings; use Mojolicious::Lite; plugin 'ClientIP'; plugin 'basic_auth_plus'; # sample data for authentication my %accepted_IPs = ( '10.0.0.3' => 1 ); my %users = ( usr1 => 'pwd1', usr2 => 'pwd2' ); # sample data to send back to client my $text_a = 'Né più mai toccherò le sacre sponde'; my $text_b = 'ove il mio corpo fanciulletto giacque,'; # I expect all non specified routes to answer 404, right? sub checkUserPW { my $self = shift; my ($href, $auth_ok) = $self->basic_auth( realm => sub { if ( exists $users{$_[0]} and $users{$_[0]} eq $_[1]){ return 1; } return 0; }); } sub checkIP { my $c = shift; my $remote_IP = $c->client_ip; if (exists $accepted_IPs{$remote_IP}) { return 1; } return 0; } sub checkCreds { my $c = shift; return 1 if ( checkIP($c) && checkUserPW($c) ); return 0; } under sub { my $c = shift; return 1 if checkCreds($c); $c->render(status => 401, text => 'not ok'); return undef; }; get 'get_first' => sub { my $c = shift; return $c->render(text => $text_a); }; get 'get_second' => sub { my $c = shift; return $c->render(text => $text_b); }; app->start;
    ./client.pl FIRST REQUEST [2020-06-16 22:30:43.75375] [7657] [debug] [siQnjFJG] GET "/get_first" [2020-06-16 22:30:43.75403] [7657] [debug] [siQnjFJG] Routing to a cal +lback [2020-06-16 22:30:43.75436] [7657] [debug] [siQnjFJG] Routing to a cal +lback [2020-06-16 22:30:43.75466] [7657] [debug] [siQnjFJG] 200 OK (0.000877 +s, 1140.251/s) HTTP/1.1 200 OK Date: Wed, 17 Jun 2020 03:30:43 GMT Server: Mojolicious (Perl) Content-Length: 38 Content-Type: text/html;charset=UTF-8 Client-Date: Wed, 17 Jun 2020 03:30:43 GMT Client-Peer: 10.0.0.3:3000 Client-Response-Num: 1 Né più mai toccherò le sacre sponde SECOND REQUEST [2020-06-16 22:30:46.76167] [7657] [debug] [rq4czVSf] GET "/get_second +" [2020-06-16 22:30:46.76195] [7657] [debug] [rq4czVSf] Routing to a cal +lback [2020-06-16 22:30:46.76231] [7657] [debug] [rq4czVSf] 401 Unauthorized + (0.000614s, 1628.664/s) HTTP/1.1 401 Unauthorized Date: Wed, 17 Jun 2020 03:30:46 GMT Server: Mojolicious (Perl) WWW-Authenticate: Basic realm="realm" Content-Length: 6 Content-Type: text/html;charset=UTF-8 Client-Date: Wed, 17 Jun 2020 03:30:46 GMT Client-Peer: 10.0.0.3:3000 Client-Response-Num: 1 not ok ATTENTION: next should fail.. [2020-06-16 22:30:49.77717] [7657] [debug] [DknpCqhV] GET "/get_second +" [2020-06-16 22:30:49.77744] [7657] [debug] [DknpCqhV] Routing to a cal +lback [2020-06-16 22:30:49.77780] [7657] [debug] [DknpCqhV] 401 Unauthorized + (0.000613s, 1631.321/s) HTTP/1.1 401 Unauthorized Date: Wed, 17 Jun 2020 03:30:49 GMT Server: Mojolicious (Perl) WWW-Authenticate: Basic realm="realm" Content-Length: 6 Content-Type: text/html;charset=UTF-8 Client-Date: Wed, 17 Jun 2020 03:30:49 GMT Client-Peer: 10.0.0.3:3000 Client-Response-Num: 1 not ok

    I tested with  morbo server.pl. A small prod deployment server.pl daemon -m production -l http://ip:port. The string MfmL-Zeg is per HTTP request I believe. So you can track through a complicated HTTP session that spawns a bunch of requests.

      Hello trippledubs and many thanks for your full, tested example,

      it is not exactly the cleaner replication of mine: infact i suspect that you used the client I proposed in the original post, and if so, the client does not send username and password in the second request because it received back a valid cookie. You never get ove il mio corpo fanciulletto giacque, printed. My get_second only checks this cookie, not credentials. The third request made by the client was there only to check the cookie corectly expired, so it has to fail. But these are details and your code is clean and a good example to show.

      About under : it shows to be a powerful tecnique but, if I'm permitted, even too much. I mean: the code will result shorter and cleaner and very DRY, but imagine what happens if you have two or three screenful of routes: then after one month you get back at the code to review route_105 which is affected by under one but you dont see it.. Is the typical situation I will hate, like a no warnings put in the middle of a long code, with global effect..

      So I'd probably go for something like (DRY code at the cost of WET comments.. ;)

      # see UNDER above get 'get_first' => sub { my $c = shift; return $c->render(text => $text_a); }; # see UNDER above get 'get_second' => sub { my $c = shift; return $c->render(text => $text_b); };

      Or put them in a group like shown in the tutorial:

      # Admin section group { # Local logic shared only by routes in this group under '/admin' => sub { my $c = shift; return 1 if $c->req->headers->header('X-Awesome'); $c->render(text => "You're not awesome enough."); return undef; }; # GET /admin/dashboard get '/dashboard' => {text => 'Nothing to see here yet.'}; };

      But the last example raise another question in my mind: why the comment # GET /admin/dashboard ?? Is not /dashboard the route defined? Or... under means: all routes logically under the specified one ( like in under '/admin' ) and, if not specified logically under the root like under '/'?

      If the above assumption is correct I'd really like to know where it is explained.

      L*

      There are no rules, there are no thumbs..
      Reinvent the wheel, then learn The Wheel; may be one day you reinvent one of THE WHEELS.
        why the comment # GET /admin/dashboard ??
        Under is a superset of nested routing. Nested routes create prefixes (with / prefix implicit), but under also runs code after matching the prefix but before dispatching the request further down the chain.
Re: first steps with Mojolicious::Lite (-Mojo g)
by Anonymous Monk on Jun 15, 2020 at 19:56 UTC

    ?? using LWP::UserAgent ??

    :D

    perl -Mojo -e " print g(q{http://user:pass@10.0.0.1/get_first})->to_st +ring "