Multiple app directories with Dancer perl - perl

Is there a way to have one app in dancer but with multiple appdirs.
Or could I do something like this:
My project is in dir 'foo'. And let's say that I have a dir 'bar' (not inside 'foo') which has a directory called 'public'. I what my app 'foo' to use this public as its own public and if it searches for let's say '/css/style.css' and it is not in '/bar/public/' it should search the '/foo/public/'. How can I do that?

OK, here is the good way to do it. It can of course be a plugin.
You should never do this kind of things by hacking inside Dancer's core, you should rather always consider implementing a route handler to do the job:
#!/usr/bin/env perl
use Dancer;
use File::Spec;
use Dancer::FileUtils 'read_file_content';
use Dancer::MIME;
use HTTP::Date;
# your routes here
# then the catchall route for
# serving static files
# better in config
my #public_dirs = qw(/tmp/test/foo /tmp/test/bar /tmp/test/baz);
get '/**' => sub {
my $path = request->path;
my $mime = Dancer::MIME->instance;
# security checks
return send_error("unauthrorized request", 403) if $path =~ /\0/;
return send_error("unauthrorized request", 403) if $path =~ /\.\./;
# decompose the path_info into a file path
my #path = split '/', $path;
for my $location (#public_dirs) {
my $file_path = File::Spec->catfile($location, #path);
next if ! -f $file_path;
my $content = read_file_content($file_path);
my $content_type = $mime->for_file($file_path);
my #stat = stat $file_path;
header 'Content-Type', $content_type;
header 'Content-Length', $stat[7];
header 'Last-Modified', HTTP::Date::time2str($stat[9]);
return $content;
}
pass;
};
start;
An example of this app running:
$ mkdir -p /tmp/test/foo /tmp/test/bar /tmp/test/baz
$ echo 1 > /tmp/test/foo/foo.txt
$ echo 2 > /tmp/test/bar/bar.txt
$ echo 3 > /tmp/test/baz/baz.txt
$ ./bin/app.pl
$ curl -I http://0:3000/baz.txt
HTTP/1.0 200 OK
Content-Length: 2
Content-Type: text/plain
Last-Modified: Fri, 14 Oct 2011 11:28:03 GMT
X-Powered-By: Perl Dancer 1.3051

One of the ways if to write a plugin that renders static (and replaces some functionality). You can use Dancer::Plugin::Thumbnail as an example.
Other way I see is to monkey-patch get_file_response() at Dancer::Renderer which is not really such a good idea.
Following code looks for static files in each dir from #dirs array. It's dirty, ugly and unsafe.
This can be broken in future version and may cause problems with other parts of Dancer framework I'm not familiar with. You're warned.
#!/usr/bin/env perl
use Dancer;
use Dancer::Renderer;
use MyWeb::App;
my $get_file_response_original = \&Dancer::Renderer::get_file_response;
my #dirs = ('foo');
*Dancer::Renderer::get_file_response = sub {
my $app = Dancer::App->current;
my $result;
# Try to find static in default dir
if ($result = $get_file_response_original->(#_)) {
return $result;
}
# Save current settings
my $path_backup = $app->setting('public');
# Go through additional dirs
foreach my $dir (#dirs) {
$app->setting(public => $dir);
if ($result = $get_file_response_original->(#_)) {
last;
}
}
# Restore public
$app->setting('public' => $path_backup);
return $result
};
dance;
Third ways is to let nginx just do this work for you by writing proper nginx config for your application.

May be this module will help to you?
https://github.com/Perlover/Dancer-Plugin-Hosts
You can setup virtual sites in Dancer with own appdir & other directory settings
I uploaded this module today to github
Soon will be in CPAN

Related

"sh: 1: file: not found" thrown in Perl

So this is an issue I see thrown around on several coding help-sites that always have a slight variation. I'm not entirely familiar with what it means, and what's even more curious is that this error is thrown midway through a larger Upload.pm script, and does not cause any sort of fatal error. It gets tossed into my error log somewhere during this unless conditional snippet
# If this is the first slice, validate the file extension and mime-type. Mime-type of following slices should be "application/octet-stream".
unless ( defined $response{'error'} ) {
if ( $slice->{'index'} == 1 ) {
my ($filename, $directory, $extension) = fileparse($path.$parent_file, qr/\.[^.]*/);
unless ( is_valid_filetype($slice->{'tmp_file'}, $extension) ) {
$response{'error'} = "Invalid file type.";
$response{'retry'} = 0;
}
}
}
Now, let me be perfectly honest. I don't really understand the error message, and I could really use some help understanding it, as well as solving it.
Our Perl based web app has refused to let us upload files correctly since upgrading to Debian Bullseye, and I've been stuck debugging this code I didn't write for a few days now. I'm wondering if the upgrade depreciated some Perl modules, or if the directories to said modules are no longer working?
I'm testing this in a Ubuntu based Docker environment running Debian Bullseye on an Apache 2 server.
If you need any more context, clarification, etc, please let me know.
is_valid_filetype() looks like this:
sub is_valid_filetype
{
my ($tmp_file, $extension) = #_;
if ( $tmp_file && $extension ) {
# Get temp file's actual mime-type.
my $mime = qx/file --mime-type -b '${tmp_file}'/;
$mime =~ s/^\s+|\s+$//g;
# Get valid mime-types matching this extension.
my $dbh = JobTracker::Common::dbh or die("DBH not available.");
my $mime_types = $dbh->selectrow_array('SELECT `mime_types` FROM `valid_files` WHERE `extension` = ?', undef, substr($extension, 1));
if ( $mime && $mime_types ) {
if ( $mime_types !~ /,/ ) {
# Single valid mime-type for this extension.
if ( $mime eq $mime_types ) {
return 1;
}
} else {
# Multiple valid mime-types for this extension.
my %valid_mimes = map { $_ => 1 } split(/,/, $mime_types);
if ( defined $valid_mimes{$mime} ) {
return 1;
}
}
}
}
return 0;
}
It's a message from sh (not Perl). It concerns an error on line 1 of the script, which was apparently an attempt to run the file utility. But sh couldn't find it.
The code in question executes this command using
qx/file --mime-type -b '${tmp_file}'/
Install file or adjust the PATH so it can be found.
Note that this code suffers from a code injection bug. It will fail if the string in $tmp_path contains a single quote ('), possibly resulting in the unintentional execution of code.
Fixed:
use String::ShellQuote qw( shell_quote );
my $cmd = shell_quote( "file", "--mime-type", "-b", $tmp_file" );
qx/$cmd/
Debian Bullseye was reading our CSV files as the wrong mime-type. It was interpreting the file command as application/csv, despite obviously not being an application.
This may be an actual bug in Bullseye, because both my boss and I have scoured the internet with no lucky finding anyone else with this issue. I may even report to Bullseye's devs for further awareness.
The fix was manually adding in our own mime-types that interpreted this file correctly.
It took us dumping the tmp directory to confirm the files existed, and triple checking I had my modules installed.
This was such a weird and crazy upstream issue that either of us could not have imaged it would be the file type interpretation at an OS level in Bullseye.
I really hope this helps someone, saves them the time it took us to find this.

perl mismatched tag, problem = XML::Simple module?

when i try to run this script to send my ip to cpanel...
#!/usr/bin/perl
# -------------------------------------------------------------------------------
# dns_update_script.pl
#
# Version 1.0 - 16.01.2012
#
# PERL script to dynamically update the IP of a host via the cPanel-API. This
# script was written to work with the Finnish hoster Neobitti but it might work
# with other hosters which use cPanel too.
#
# Copyright (C) 2012 Stefan Gofferje - http://stefan.gofferje.net/
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License as
# published by the Free Software Foundation; either version 2 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
# -------------------------------------------------------------------------------
use strict;
use LWP::UserAgent;
use MIME::Base64;
use XML::Simple;
use Data::Dumper;
# --- Command line parameters ------------------------------------------------
my $param_domain=$ARGV[0];
my $param_host=$ARGV[1];
my $param_ip=$ARGV[2];
# --- cPanel information -----------------------------------------------------
# Storing passwords in clear text is ugly!
my $cpanel_domain = "yourdomain.com";
my $user = "yourcpaneluser";
my $pass = "yourcpanelpassword";
my $auth = "Basic " . MIME::Base64::encode( $user . ":" . $pass );
# --- Deactivate SSL certificate validation ----------------------------------
# This is ugly but neccessary because Neobitti uses self-signed SSL
# certificates which will fail validation
my $ua = LWP::UserAgent->new(ssl_opts => { verify_hostname => 0 });
# --- Find out the linenumber for the A-record we want to change -------------
sub getlinenumber_a {
my $domain=$_[0];
my $hostname=$_[1].".";
my $xml = new XML::Simple;
my $request = HTTP::Request->new( GET => "https://$cpanel_domain:2083/xml-api/cpanel?cpanel_xmlapi_module=ZoneEdit&cpanel_xmlapi_func=fetchzon
e&domain=$domain" );
$request->header( Authorization => $auth );
my $response = $ua->request($request);
my $zone = $xml->XMLin($response->content);
my $linenumber="";
if ($zone->{'data'}->{'status'} eq "1") {
my $count = #{$zone->{'data'}->{'record'}};
my $oldip="";
for (my $item=0;$item<=$count;$item++) {
my $name=$zone->{'data'}->{'record'}[$item]->{'name'};
my $type=$zone->{'data'}->{'record'}[$item]->{'type'};
if ( ($name eq $hostname) && ($type eq "A") ) {
$linenumber=$zone->{'data'}->{'record'}[$item]->{'Line'};
$oldip=$zone->{'data'}->{'record'}[$item]->{'record'};
print "Found $hostname in line $linenumber with IP $oldip.\n"; # DEBUG
}
}
} else {
$linenumber="0";
print $zone->{'event'}->{'data'}->{'statusmsg;'}
}
return($linenumber);
}
# --- Change the IP address record for a certain linenumber ------------------
sub setip {
my $domain=$_[0];
my $linenumber=$_[1];
my $newip=$_[2];
my $result="";
my $xml = new XML::Simple;
my $request = HTTP::Request->new( GET => "https://$cpanel_domain:2083/xml-api/cpanel?cpanel_xmlapi_module=ZoneEdit&cpanel_xmlapi_func=edit_zone_record&domain=$domain&line=$linenumber&address=$newip" );
$request->header( Authorization => $auth );
my $response = $ua->request($request);
my $reply = $xml->XMLin($response->content);
if ($reply->{'data'}->{'status'} eq "1") {
$result="1";
} else {
$result=$reply->{'data'}->{'statusmsg'};
}
return($result);
}
# --- Main procedure ---------------------------------------------------------
print "Trying to find the linenumber for $param_host in $param_domain...\n";
my $line=getlinenumber_a($param_domain,$param_host);
if ( ($line ne "0") && ($line ne "") ) {
print "Trying to update IP...\n";
my $result=setip ($param_domain,$line,$param_ip);
if ($result eq "1") {
print "Update successful!\n";
} else {
print "$result\n";
}
} else {
print "Error - check domain and hostname!\n";
}
... I get this error:
mismatched tag at line 37, column 2, byte 4634 at /usr/lib/arm-linux-gnueabihf/perl5/5.28/XML/Parser.pm line 187.
XML::Simple called at dns_update_script.pl line 55.
in the cPanel information section I have filled in my cpanel username, password and domain in my file (not this one). I have filled this in in open text like a noob and ignored this thing:
my $auth = "Basic " . MIME::Base64::encode( $user . ":" . $pass );
but i dont think this is the problem.
I think the problem is in the XML::Simple module because the script stops when that module is called it seems like to me but idk. pls help.
You are getting that error because the string you pass to XMLin isn't valid XML. You will need to fix the input to be valid XML.
While I strongly recommend against using XML::Simple, it's not because of problems parsing XML. The underlying parser used in this situation (expat via XML::Parser) isn't known for wrongly claiming valid XML is invalid.
Your program should be checking if the request is successful ($response->is_success). I suspect you are getting some kind of error (e.g. 403 Forbidden). $response->status_line is useful to add to your error message.
(If the request is successful, perhaps you should print out $response->as_string to examine the response to get a better idea of what is happening.)
I just used this shell script instead and hooked it up to my crontab and now it runs the script every hour and it works fine.
https://github.com/CpanelInc/cpanel-dynamicdns-tools
for my raspberry pi web server

Route to static file in Mojo

I have small app based on mojolicious. And I have index.html in public dir. I want to have route to this file when user asks for '/'.
I wrote two solution, but I don't like them.
First solution - add simple controller.
sub stratup {
//...
$r->get('/')->to('general#index_html');
//...
}
package MyPackage::General;
use Mojo::Base 'Mojolicious::Controller';
use strict;
use warnings;
sub index_html {
my $self = shift;
$self->render_static('index.html');
return;
}
1;
Second solution - add hook
sub startup {
my $self = shift;
$self->hook(before_dispatch => sub {
my $self = shift;
if ($self->req->url eq '/') {
$self->req->url( Mojo::URL->new('/index.html') );
}
});
What I want:
$r->get('/')->to('/index.html');
or something like that.
P.S. I know, than usualy nginx/apache do it, but I use morbo to run code.
You want:
$r->get('...')->to(cb => sub {
my $c = shift;
$c->reply->static('index.html')
});
(As long as you're after Mojolicous 5.45 2014-09-26)
By far the simplest way is
get "/" => "index";
I'll dig this up from the graveyard, why not.
I found myself similarly trying to serve a static html file in a docker container that I had using to serve both a Mojolicious REST API and a Vue.js front end. After searching around and piecing sporadic information together, this is what seems to work for me.
** disclaimer: I have not fully tested this with Vue routing and other aspects as yet.
My directory structure:
/app
/app/script
/app/modules/ui
/app/modules/ui/dist
From the command line the app directory, using morbo to test:
morbo script/ui.pl
ui.pl script
#!/usr/bin/env perl
use Mojolicious::Lite -signatures;
use Mojo::File qw(curfile);
use v5.25;
my $app = app;
my $static = $app->static;
push #{$static->paths}, curfile->dirname->sibling('modules/ui/dist')->to_string;
any '/' => sub {
my $c = shift;
my $content = $static->file("/index.html")->slurp;
$c->render(text => $content);
};
$app->start;
Using a combo of information from https://metacpan.org/pod/Mojolicious::Static and basic routing information at https://docs.mojolicious.org/Mojolicious/Lite, I could get the vue.js index page to render as expected.
** UPDATED A DAY LATER **
As it turns out, there is an easier way, though not clearly documented. If you place the static files inside your public folder, you can use the default helpers included with Mojolicious to render the files. The documentation refers to it here, https://docs.mojolicious.org/Mojolicious/Guides/Rendering#Serving-static-files, but it's not very clear on how to make it happen.
I tooled around some, but it took browsing the code of Controller.pm of for Mojolicious to sort it out. This section of the POD led me to determine how to get the reply object:
=head2 helpers
my $helpers = $c->helpers;
Return a proxy object containing the current controller object and on which helpers provided by /app can be called. This includes all helpers from Mojolicious::Plugin::DefaultHelpers and Mojolicious::Plugin::TagHelpers.
# Make sure to use the "title" helper and not the controller method
$c->helpers->title('Welcome!');
# Use a nested helper instead of the "reply" controller method
$c->helpers->reply->not_found;
Based on this, I can drop my files into the public folder:
/app/public/index.html
Then modify my controller to match:
# https://docs.mojolicious.org/Mojolicious/Guides/Rendering#Serving-static-files
any '/' => sub {
my $c = shift;
$c->helpers->reply->static('index.html');
};

Removing top-directory-only URLs from a list of URLs?

I have a question that I'm having trouble researching, as I don't know how to ask it correctly on a search engine.
I have a list of URLs. I would like to have some automated way (Perl for preference) to go through the list and remove all URLs that are top directory only.
So for example I might have this list:
http://www.example.com/hello.html
http://www.foo.com/this/thingrighthere.html
In this case I would want to remove example.com from my list, as it is either top-directory only or they reference files in a top directory.
I'm trying to figure out how to do that. My first thought was, count forward slashes and if there's more than two, eliminate the URL from the list. But then you have trailing forward slashes, so that wouldn't work.
Any ideas or thoughts would be much appreciated.
Something like this:
use URI::Split qw( uri_split );
my $url = "http://www.foo.com/this/thingrighthere.html";
my ($scheme, $auth, $path, $query, $frag) = uri_split( $url );
if (($path =~ tr/\///) > 1 ) {
print "I care about this $url";
}
http://metacpan.org/pod/URI::Split
You could do this with regexes, but its much less work to let the URI library do it for you. You won't get caught out by funny schemes, escapes, and extra stuff before and after the path (query, anchor, authorization...). There's some trickiness around how paths are represented by path_segments(). See the comments below and the URI docs for details.
I have assumed that http://www.example.com/foo/ is considered a top directory. Adjust as necessary, but its something you have to think about.
#!/usr/bin/env perl
use URI;
use File::Spec;
use strict;
use warnings;
use Test::More 'no_plan';
sub is_top_level_uri {
my $uri = shift;
# turn it into a URI object if it isn't already
$uri = URI->new($uri) unless eval { $uri->isa("URI") };
# normalize it
$uri = $uri->canonical;
# split the path part into pieces
my #path_segments = $uri->path_segments;
# for an absolute path, which most are, the absoluteness will be
# represented by an empty string. Also /foo/ will come out as two elements.
# Strip that all out, it gets in our way for this purpose.
#path_segments = grep { $_ ne '' } #path_segments;
return #path_segments <= 1;
}
my #filtered_uris = (
"http://www.example.com/hello.html",
"http://www.example.com/",
"http://www.example.com",
"https://www.example.com/",
"https://www.example.com/foo/#extra",
"ftp://www.example.com/foo",
"ftp://www.example.com/foo/",
"https://www.example.com/foo/#extra",
"https://www.example.com/foo/?extra",
"http://www.example.com/hello.html#extra",
"http://www.example.com/hello.html?extra",
"file:///foo",
"file:///foo/",
"file:///foo.txt",
);
my #unfiltered_uris = (
"http://www.foo.com/this/thingrighthere.html",
"https://www.example.com/foo/bar",
"ftp://www.example.com/foo/bar/",
"file:///foo/bar",
"file:///foo/bar.txt",
);
for my $uri (#filtered_uris) {
ok is_top_level_uri($uri), $uri;
}
for my $uri (#unfiltered_uris) {
ok !is_top_level_uri($uri), $uri;
}
Use the URI module from CPAN. http://search.cpan.org/dist/URI
This is a solved problem. People have already written, tested and debugged code that handles this already. Whenever you have a programming problem that others have probably had to deal with, then look for existing code that does it for you.

First 8 bytes are always wrong when downloading a file from my script

I have a Mojolicious Lite script that "gives out" an executable file (user can download the file from the script's URL). I keep encoded data in an inline template in DATA section, then encode it and render_data.
get '/download' => sub {
my $self = shift;
my $hex_data = $self->render_partial( 'TestEXE' );
my $bin_data;
while( $hex_data =~ /([^\n]+)\n?/g ) {
$bin_data .= pack "H".(length $1), $1;
}
my $headers = Mojo::Headers->new;
$headers->add( 'Content-Type', 'application/x-download;name=Test.exe' );
$headers->add( 'Content-Disposition', 'attachment;filename=Test.exe' );
$headers->add( 'Content-Description', 'File Transfer');
$self->res->content->headers($headers);
$self->render_data( $bin_data );
};
__DATA__
## TestEXE.html.ep
4d5a90000300000004000000ffff0000b8000000000000004000000000000000
00000000000000000000000000000000000000000000000000000000b0000000
0e1fba0e00b409cd21b8014ccd21546836362070726f6772616d2063616e6e6f
....
When I run this locally (via built in webserver on http://127.0.0.1:3000/, Win7) I get the correct file (size and contents). But when I run it in CGI mode on shared hosting (Linux), it comes back with correct size, but first 8 bytes of the file are always incorrect (and always different). The rest of the file is correct.
If in my sub i specify $hex_data instead of $bin_data I get what suppose to be there.
I'm at lost.
render_partial isn't what you want.
First, re-encode the executable in base64 format, and specify that the template is base64 encoded (This is assuming hex is not a requirement for your app):
## template-name (base64)
Also, you don't actually need a controller method at all. Mojolicious will handle the process for you - all you have to do is appropriately name the template.
use Mojolicious::Lite;
app->start;
__DATA__
## Test.exe (base64)
...
http://127.0.0.1:3000/Test.exe will then download the file.
-
If you still want to use a controller method for app-specific concerns, get the data template specifically:
use Mojolicious::Lite;
get '/download' => sub {
my $self = shift;
# http://mojolicio.us/perldoc/Mojolicious/Renderer.pm#get_data_template
my $data = $self->app->renderer->get_data_template({}, 'Test.exe');
# Replace content-disposition instead of adding it,
# to prevent duplication from elsewhere in the app
$self->res->headers->header(
'Content-Disposition', 'attachment;filename=name.exe');
$self->render_data($data);
};
app->start;
__DATA__
## Test.exe (base64)
...
http://127.0.0.1:3000/download will get the template, set the header, and then download it as name.exe.