How to add and remove routes dynamically in Mojolicious? - perl

I am trying to put together a maintenance page in my Mojolicious app which all of my users will be shown whenever a file or DB entry is present on the server.
I know I can check for this file or entry on startup and if its there add in my 'catch all' route. However I'm not sure how to do this dynamically? I don't want to have to restart the backend whenever I want to go into maintenance.
Is there a way to add and remove routes from a hook? for example use the before dispatch hook to monitor for the file/db entry and if it exists modify the routes?
I tried this but I didn't seem to be able to access the routes from the hooked function, only in the startup function.
Thank you.

The router is dynamic until the first request has been served, after that, the router cannot change routes (source). That said, can you not declare the route generally and just prohibit any access until that condition exists?
#!/usr/bin/env perl
use Mojolicious::Lite;
any '/' => sub { shift->render( text => 'Hello World' ) };
under sub {
unless (-e 'myfile') {
shift->render_not_found;
return 0;
}
return 1;
};
any '/protected' => sub { shift->render( text => 'I am safe' ) };
app->start;

Related

How to change Plack Session's name?

I have two applications on the same domain, but they are both creating a plack_session every time the user logs in. It happens because application A overwrites application B's plack session.
It's a complex process to remove one of them and make them use one that is created by a central application, but for now, how can I change one of those 'plack_session' names to something like 'plack_session2' so they don't see each other?
I don't even know if it is possible.
Here is the document for Plack Session, but I can't see anything that can help me here.
As shown in the documentation you link to, the Plack session middleware is enabled with code like this:
builder {
enable 'Session',
state => Plack::Session::State->new;
$app;
};
Later in the same document, you'll find the documentation for the new() method:
new ( %params )
The %params can include session_key, sid_generator and sid_checker however in both cases a default will be provided for you.
session_key
This is the name of the session key, it defaults to 'plack_session'.
...
Putting all this together, I'd guess (and I haven't ever done this) that you can do what you want with code like this:
builder {
enable 'Session',
state => Plack::Session::State->new(
session_key => 'my_session_key',
);
$app;
};

Perl Mojolicious routes call sub only once

Iam using Mojolicious::Lite to declare routes for a webservice.
Maybe i missunderstood the routes behaviour but if iam calling a subroutine in the routes definition it gets only called once.
I thougt the sub should get triggered every time the webservice route is called... which isn't the case.
For example i wrote a test route:
use Mojolicious::Lite;
get '/test' => {
text => get_test()
};
sub get_test {
say 'Hello iam only showing up once';
return 'test';
};
This is the console output on starting the server and visiting the localhost:3000/test route:
Hello iam only showing up once
[2020-04-04 22:07:21.09011] [69050] [info] Listening at "http://*:3000"
Server available at http://127.0.0.1:3000
[2020-04-04 22:07:28.26033] [69050] [debug] [78278b87] GET "/test"
[2020-04-04 22:07:28.26097] [69050] [debug] [78278b87] 200 OK
(0.000626s, 1597.444/s)
The "Hello iam only showing up once" is outputted once as the server starts. Visiting the route is not triggering the sub again.
If this is the wanted behaviour how can i get my routes to triggering the sub everytime the route is visited?
I need this because i use this webservice in my application to scan the network and return the result. And i want to rescan the network everytime i call the webservice GET route. Otherwise the data would be useless if its not up to date.
Thanks for your help and explanations.
Your code isn't actually rendering. Your get sub needs to call the render method from Mojolicious::Controller. Inside the get sub, $_[0] is an instance of the controller. In a Mojolicious::Lite app, the route and controller are combined, so you do need to render. If you change your code to this, it will do what you might expect.
use Mojolicious::Lite;
get '/test' => sub {
shift()->render(text => get_test());
};
sub get_test {
warn "Hello I show up on every hit.\n";
return 'test';
};
The key difference is the shift()->render(...) call, which could also be written like this:
get '/test' => sub {
my $c = shift;
$c->render(text => get_text());
}
The above answer is good, but one of the key problems in your code is that you seem to have incorrectly defined your route code ref:
get '/test' => {
should be:
get '/test' => sub {
This is why your get_test only gets called once - you are defining a hash and populating it with the result of get_test, instead of defining a subroutine. But see #DavidO's answer for the full detail on how to approach this.

How to login with user but still stay admin?

I want to implement feature when operator/admin may login as user. Do something under user's credentials and then return back and continue as operator/admin
I try to mount whole application under /as_user/:user_id route. So when request come I adjust session to :user_id.
I try detour
$app->routes->route( '/as_user/:app_user' )->detour( app => $app );
But in this case when GET /as_user/17/packages request come the application fall into infinite loop
Also I think to append ?U=17 query parameter. But I do not know how and where rewrite code in such way: All link should be rendered with ?U=17 appended.
Please advice how to login with another user but still stay admin.
Seems I found the answer:
$r->under( '/as_user/:user_id', sub{
# FIX THE SESSION HERE. Just like:
# $_[0]->session->{ user_id } = _[0]->match->stack->[-1]->{ user_id };
return 1; # Required to not break the dispatch chain
})->route('/')->detour( 'App' );
Instead of application instance you should pass application class and Mojolicious will instantiate it itself.
PS. Infinite loop maybe because of cyclic refs. (But Mojolicious check refs here)
UPD
Infinite loop because of bug

How do I localize an object that is inside a property of a Moo object in Perl?

I've got an object that stores an LWP::UserAgent. I want to use different cookie jars for different calls with that UA, so I decided to make the cookie_jar local when doing a call.
The following code shows what I did without debug stuff (for reading, not running). Below is another version with lots of debugging output.
package Foo;
use strictures;
use Moo;
use LWP::UserAgent;
has ua => (
is => 'ro',
default => sub { my $ua = LWP::UserAgent->new; $ua->cookie_jar( {} ); return $ua; },
);
sub request {
my ($self, $cookie_jar) = #_;
local $self->{ua}->{cookie_jar} = $cookie_jar;
$self->ua->get('http://www.stackoverflow.com');
}
package main;
my $foo = Foo->new;
my $new_jar = HTTP::Cookies->new;
$foo->request( $new_jar );
So basically I decided to locally overwrite the cookie jar. Unfortunately, when we call get it will still use the cookie jar that is originally inside the UA object.
package Foo;
use strictures;
use Moo;
use LWP::UserAgent;
use HTTP::Cookies;
use Data::Printer;
use feature 'say';
has ua => (
is => 'ro',
default => sub { my $ua = LWP::UserAgent->new; $ua->cookie_jar( {} ); return $ua; },
);
sub request {
my ($self, $cookie_jar) = #_;
say "before local " . $self->{ua}->{cookie_jar};
local $self->{ua}->{cookie_jar} = $cookie_jar;
$self->ua->get('http://www.stackoverflow.com');
print "local jar " . p $self->{ua}->{cookie_jar};
say "after local " . $self->{ua}->{cookie_jar};
}
package main;
use Data::Printer;
use HTTP::Cookies;
my $foo = Foo->new;
say "before outside of local " . $foo->{ua}->{cookie_jar};
my $new_jar = HTTP::Cookies->new;
say "before outside of local " . $new_jar;
$foo->request( $new_jar );
say "after outside of local " . $foo->{ua}->{cookie_jar};
print "global jar " . p $foo->ua->cookie_jar;
__END__
before outside of local HTTP::Cookies=HASH(0x30e1848)
before outside of local HTTP::Cookies=HASH(0x30e3b20)
before local HTTP::Cookies=HASH(0x30e1848)
local jar HTTP::Cookies {
public methods (13) : add_cookie_header, as_string, clear, clear_temporary_cookies, DESTROY, extract_cookies, load, new, revert, save, scan, set_cookie, set_cookie_ok
private methods (3) : _host, _normalize_path, _url_path
internals: {
COOKIES {}
}
}after local HTTP::Cookies=HASH(0x30e3b20)
after outside of local HTTP::Cookies=HASH(0x30e1848)
global jar HTTP::Cookies {
public methods (13) : add_cookie_header, as_string, clear, clear_temporary_cookies, DESTROY, extract_cookies, load, new, revert, save, scan, set_cookie, set_cookie_ok
private methods (3) : _host, _normalize_path, _url_path
internals: {
COOKIES {
stackoverflow.com {
/ {
prov [
[0] 0,
[1] "185e95c6-a7f4-419a-8802-42394776ef63",
[2] undef,
[3] 1,
[4] undef,
[5] 2682374400,
[6] undef,
[7] {
HttpOnly undef
}
]
}
}
}
}
}
As you can see, the HTTP::Cookies object gets localized and replaced correctly. The addresses look totally correct.
But the output of p tells a different story. LWP::UA has not used the local cookie jar at all. That remains a fresh, empty one.
How can I make it use the local one instead?
I have tried using Moo, Moose and classic bless objects. All show this behaviour.
Edit: Since this came up in the comments, let me give a little more background why I need to do this. This is going to be a bit of a rant.
TLDR: Why I do not want alternative solution but understand and fix the problem
I'm building a Dancer2-based webapp that will run with Plack and multiple workers (Twiggy::Prefork - multiple threads in multiple forks). It will allow users to use a service of a third company. That company offers a SOAP webservice. Think of my application as a custom frontend to this service. There is a call to 'log the user in' on the webservice. It returns a cookie (sessionid) for that specific user and we need to pass that cookie with each consecutive call.
To do the SOAP-stuff I am using XML::Compile::WSDL11. Compiling the thing is pretty costly, so I do not want to do that each time a route is handled. That would be way inefficient. Thus the SOAP client will be compiled from the WSDL file when the application starts. It will then be shared by all workers.
If the client object is shared, the user agent inside is shared as well. And so is the cookie jar. That means that if there are two requests at the same time, the sessionids might get mixed up. The app could end up sending wrong stuff to the users.
That's why I decided to localize the cookie jar. If it's a local unique one for a request, it will never be able to interfere with another worker's request that is happening in parallel. Just making a new cookie jar for each request will not cut it. They would still be shared, and might even get lost because they would overwrite each other in the worst case.
Another approach would be to implement a locking mechanism, but that would totally beat the purpose of having multiple workers.
The only other solution I see is using another SOAP-client alltogether. There is SOAP::WSDL, which does not run on newer Perls. according to CPAN testers it breaks on 5.18 andI have verified that. It would be more efficient as it works like a code generator and precreates classes that are cheaper to use than just compiling the WSDL file every time. But since it's broken, it is out of the question.
SOAP::Lite will compile the WSDL, and badly. It is not something anyone should use in production if it can be avoided in my opinion. The only alternative left that I see is to implement the calls without using the WSDL file and parsing the results directly with an XML parser, ignoring the schema. But those are BIG results. It would be very inconvenient.
My conclusion to this rant is that I would really like to understand why Perl does not want to localize the cookie jar in this case and fix that.
Perhaps instead of using local you use the clone and cookie_jar methods of LWP::UserAgent.
...
sub request {
my ($self, $new_cookie_jar) = #_;
my $ua = $self->ua; # cache user agent
if( defined $new_cookie_jar ){
# create a new user agent with the new cookie jar
$ua = $ua->clone;
$ua->cookie_jar( $new_cookie_jar );
}
my $result = $ua->get('http://www.stackoverflow.com');
# allow returning the newly cloned user agent
return ( $result, $ua ) if wantarray;
return $result;
}
If you don't want to do that, you should at least use the methods instead of manipulating the internals of the objects.
...
sub request {
my ($self, $new_cookie_jar) = #_;
my $ua = $self->ua; # cache user agent
my $old_cookie_jar = $ua->cookie_jar( $new_cookie_jar );
my $result = $ua->get('http://www.stackoverflow.com');
# put the old cookie jar back in place
$ua->cookie_jar( $old_cookie_jar );
return $result;
}

Catalyst: global init action

In Catalyst framework there is a global init sub(executed before any controller). i want the set some config variable from DB(like supper admin id or admin email for GPG configuration ).
i tried to use auto and 'begin' actions but those actions are just executed for its corresponding controller.
in Root.pm file:
sub auto :Private {
my ($self, $c) = #_;
my $config = $c->model('DB')->resultset('Config')->single();
$c->config->{var1} = $config->var1;
};
When i request another controller like Controller2 and begin , the $c->config->{var1} was empty..
That sort of configuration belongs in the main program, i.e. MyApp.pm, so it is set before you even think about accepting a request. You don't normally want to be configuring database connectivity during every request cycle.
Your question isn't entirely clear, but if you do need to configure database settings on every request, then the auto sub in the root controller Root.pm would be the place to ensure it happened at the top of every request.
But I can't help feeling there's an X-Y problem in here...