package Moxy; use 5.00800; use strict; use warnings; use base qw/Class::Accessor::Fast/; use Class::Component 0.16; our $VERSION = '0.56'; use Carp; use Encode; use File::Spec::Functions; use FindBin; use HTML::Entities; use HTML::Parser; use HTML::TreeBuilder::XPath; use HTML::TreeBuilder; use HTTP::Cookies; use HTTP::Session; use LWP::UserAgent; use MIME::Base64; use Moxy::Util; use Params::Validate ':all'; use Path::Class; use Scalar::Util qw/blessed/; use UNIVERSAL::require; use URI::Escape; use URI::Heuristic qw(uf_uristr); use URI; use YAML; use Time::HiRes (); use HTTP::MobileAttribute plugins => [ qw/CarrierLetter IS/, { module => 'Display', config => { DoCoMoMap => YAML::LoadFile( catfile( 'assets', 'common', 'docomo-display-map.yaml' ) ) } }, ]; __PACKAGE__->load_components(qw/Plaggerize Autocall::InjectMethod Context/); __PACKAGE__->load_plugins(qw/ DisplayWidth ControlPanel LocationBar Pictogram Status::401 Status::500 Status::404 UserID XMLisHTML UserAgentSwitcher RefererCutter CookieCutter FlashUseImgTag DisableTableTag GPS HTTPHeader QRCode ShowHTTPHeaders /); __PACKAGE__->mk_accessors(qw/response_time/); sub new { my ($class, $config) = @_; my $self = $class->NEXT( 'new' => { config => $config } ); $self->conf->{global}->{log}->{fh} ||= \*STDERR; return $self; } sub assets_path { my $self = shift; return $self->{__assets_path} ||= do { $self->conf->{global}->{assets_path} || dir( $FindBin::RealBin, 'assets' )->stringify; }; } # ------------------------------------------------------------------------- sub run_hook_and_get_response { my ($self, $hook, @args) = @_; $self->log(debug => "Run hook and get response: $hook"); for my $action (@{$self->class_component_hooks->{$hook}}) { my $code = $action->{plugin}->can($action->{method}); my $response = $code->($action->{plugin}, $self, @args); return $response if blessed $response && $response->isa('HTTP::Response'); } return; # not finished yet } sub rewrite_css { my ($base, $css, $url) = @_; my $base_url = URI->new($url); $css =~ s{url\(([^\)]+)\)}{ my $x = $1; sprintf "url(%s%s%s)", $base, ($base =~ m{/$} ? '' : '/'), uri_escape( URI->new($x)->abs($base_url) ) }ge; $css; } sub rewrite_html { my ($base, $html, $url) = @_; my $base_url = URI->new($url); # parse. my $tree = HTML::TreeBuilder::XPath->new; $tree->implicit_tags(0); $tree->no_space_compacting(1); $tree->ignore_ignorable_whitespace(0); $tree->store_comments(1); $tree->ignore_unknown(0); $tree->parse($html); $tree->eof; # define replacer. my $replace = sub { my ( $tag, $attr_name ) = @_; for my $node ( $tree->findnodes("//$tag") ) { if ( my $attr = $node->attr($attr_name) ) { next if $attr =~ /^mailto:/; if ($attr =~ /^tel:([0-9-]+)$/) { my $tel = $1; $node->attr( 'onclick' => qq{prompt('tel', '$1');return false;} ); } else { # maybe /https?/ $node->attr( $attr_name => sprintf( qq{%s%s%s}, $base, ($base =~ m{/$} ? '' : '/'), uri_escape( URI->new($attr)->abs($base_url) ) ) ); } } } }; # replace. $replace->( 'img' => 'src' ); $replace->( 'script' => 'src' ); $replace->( 'form' => 'action' ); $replace->( 'a' => 'href' ); $replace->( 'link' => 'href' ); # dump. my $result = $tree->as_HTML(q{<>"&'}, '', {}); $tree = $tree->delete; # cleanup :-) HTML::TreeBuilder needs this. # return result. return $result; } sub handle_request { my ($self, $req) = @_; $self->log(debug => "---------------------------"); my $conf = $self->conf->{global}->{session}; my $state_type = $conf->{state}->{module} || 'BasicAuth'; my $state = sub { if ($state_type eq 'Cookie') { require HTTP::Session::State::Cookie; HTTP::Session::State::Cookie->new( $conf->{state}->{config} ); } else { require Moxy::Session::State::BasicAuth; Moxy::Session::State::BasicAuth->new( $conf->{state}->{config} || {} ); } }->(); my $store = sub { my $postfix = $conf->{store}->{module} or die "missing session store module name"; my $klass = "HTTP::Session::Store::${postfix}"; $klass->require or die $@; $klass->new( $conf->{store}->{config} ); }->(); my $auth = join(',', $req->headers->authorization_basic); if ($state->isa('Moxy::Session::State::BasicAuth') && !$auth) { $self->log(debug => 'basicauth'); return HTTP::Engine::Response->new( status => 401, headers => { WWW_Authenticate => qq{Basic realm="Moxy needs basic auth.Only for identification.Password is dummy."}, }, body => 'authentication required', ); } else { $self->log(debug => "session: state: $state, store: $store"); my $session = HTTP::Session->new( state => $state, store => $store, request => $req, ); $self->log(debug => "session: $session"); my $res = $self->_make_response( req => $req, session => $session, ); $session->response_filter($res); $session->finalize; return $res; } } sub _make_response { my $self = shift; my %args = validate( @_ => +{ req => { isa => 'HTTP::Engine::Request', }, session => { type => OBJECT }, } ); my $req = $args{req}; my $base = $req->uri->clone; $base->path(''); $base->query_form({}); (my $url = $req->uri->path_query) =~ s!^/!!; $url = uf_uristr(uri_unescape $url); if ($url) { # do proxy my $res = $self->_do_request( url => $url, request => $req->as_http_request, session => $args{session}, ); $self->log(debug => '-- response status: ' . $res->code); if ($res->code == 302) { # rewrite redirect my $location = URI->new($res->header('Location')); $self->log(debug => "redirect to $location"); my $uri = URI->new($url); if (not defined $location->scheme) { # path only redirect is invalid! # e.g. Location: /foo/ $self->log(error => "----------------------------"); $self->log(error => "INVALID REDIRECT!! $location"); $self->log(error => "----------------------------"); $location = URI->new( $location->as_string, $uri->scheme ); $location->scheme($uri->scheme); $location->host($uri->host); $location->port($uri->port); $self->log(error => "FIXED TO: $location"); $self->log(error => "----------------------------"); } else { if ($uri->port != 80 && $location->port != $uri->port) { $location->port($uri->port); } } my $redirect = $base . '/' . uri_escape($location); $self->log(debug => "redirect to $redirect"); return HTTP::Engine::Response->new( status => 302, headers => { Location => $redirect, }, ); } else { my $content_type = $res->header('Content-Type'); $self->log(debug => "Content-Type: $content_type"); if ($content_type =~ /html/i) { $res->content( encode($res->charset, rewrite_html($base, decode($res->charset, $res->content), $url), Encode::FB_HTMLCREF) ); } elsif ($content_type =~ m{text/css}) { $res->content( encode($res->charset, rewrite_css($base, decode($res->charset, $res->content), $url), Encode::FB_HTMLCREF) ); } my $response = HTTP::Engine::Response->new(); $response->set_http_response($res); return $response; } } else { # please input url. my $response = HTTP::Response->new( 200 => 'ok', HTTP::Headers->new( 'content-type' => 'text/html;charset=utf-8', ), q{
please input url to location bar