From 2ee3f158eb93b0aa4bad6709e9f294b39ad0d615 Mon Sep 17 00:00:00 2001 From: Erik Huelsmann Date: Sat, 16 Nov 2024 19:11:28 +0100 Subject: [PATCH] Support multiple sessions in a single browser (#8496) * Support multiple sessions in a single browser, one per company This adds support for multiple logins in the same browser on a single server. The logins can be different companies with the same user, the same company with different users or different users with different companies. Closes #6352 --- UI/src/store/configTemplate.js | 10 +- UI/src/store/sessionUser.js | 2 +- cpanfile | 2 +- ...26-resource-locking-over-stateless-http.md | 52 ++++ doc/conf/webserver/apache-vhost.conf | 31 +-- doc/conf/webserver/nginx-github.conf | 20 +- doc/conf/webserver/nginx-vhost.conf | 20 +- lib/LedgerSMB/Middleware/SessionStorage.pm | 51 +++- lib/LedgerSMB/PSGI.pm | 257 +++++++++--------- lib/LedgerSMB/PSGI/Util.pm | 16 +- lib/LedgerSMB/Scripts/erp.pm | 4 +- lib/LedgerSMB/Scripts/login.pm | 8 +- old/lib/LedgerSMB/oldHandler.pm | 1 - 13 files changed, 282 insertions(+), 192 deletions(-) create mode 100644 doc/adr/0026-resource-locking-over-stateless-http.md diff --git a/UI/src/store/configTemplate.js b/UI/src/store/configTemplate.js index d3b4c13841..514306adf3 100644 --- a/UI/src/store/configTemplate.js +++ b/UI/src/store/configTemplate.js @@ -15,7 +15,7 @@ export const configStoreTemplate = { }, actions: { async initialize() { - const response = await fetch(`/erp/api/v0/${this.url}`, { + const response = await fetch(`./erp/api/v0/${this.url}`, { method: "GET" }); @@ -28,7 +28,7 @@ export const configStoreTemplate = { } }, async add(adding) { - const response = await fetch(`/erp/api/v0/${this.url}`, { + const response = await fetch(`./erp/api/v0/${this.url}`, { method: "POST", headers: { "Content-Type": "application/json" @@ -48,7 +48,7 @@ export const configStoreTemplate = { }, async del(id) { const warehouse = this.getById(id); - const response = await fetch(`/erp/api/v0/${this.url}/${id}`, { + const response = await fetch(`./erp/api/v0/${this.url}/${id}`, { method: "DELETE", headers: { "If-Match": warehouse._meta.ETag @@ -71,7 +71,7 @@ export const configStoreTemplate = { } const warehouse = this.items[index]; if (!warehouse || !warehouse._meta || warehouse._meta.invalidated) { - const response = await fetch(`/erp/api/v0/${this.url}/${id}`, { + const response = await fetch(`./erp/api/v0/${this.url}/${id}`, { method: "GET" }); @@ -101,7 +101,7 @@ export const configStoreTemplate = { }, async save(id, data) { const warehouse = this.getById(id); - const response = await fetch(`/erp/api/v0/${this.url}/${id}`, { + const response = await fetch(`./erp/api/v0/${this.url}/${id}`, { method: "PUT", headers: { "Content-Type": "application/json", diff --git a/UI/src/store/sessionUser.js b/UI/src/store/sessionUser.js index 74f4145faf..633cf0240f 100644 --- a/UI/src/store/sessionUser.js +++ b/UI/src/store/sessionUser.js @@ -11,7 +11,7 @@ export const useSessionUserStore = defineStore("sessionUser", { }, actions: { async initialize() { - const response = await fetch("/erp/api/v0/session", { + const response = await fetch("./erp/api/v0/session", { method: "GET" }); diff --git a/cpanfile b/cpanfile index 1fd4018386..93caf4e80f 100644 --- a/cpanfile +++ b/cpanfile @@ -96,11 +96,11 @@ requires 'PGObject::Type::ByteString', '1.2.3'; requires 'PGObject::Util::DBMethod', '1.1.0'; requires 'PGObject::Util::DBAdmin', '1.6.2'; requires 'Plack', '1.0031'; -requires 'Plack::App::File'; requires 'Plack::Builder'; requires 'Plack::Builder::Conditionals'; requires 'Plack::Middleware::ConditionalGET'; requires 'Plack::Middleware::ReverseProxy'; +requires 'Plack::Middleware::Static'; requires 'Plack::Request'; requires 'Plack::Request::WithEncoding'; requires 'Plack::Util'; diff --git a/doc/adr/0026-resource-locking-over-stateless-http.md b/doc/adr/0026-resource-locking-over-stateless-http.md new file mode 100644 index 0000000000..a4b29eb2f3 --- /dev/null +++ b/doc/adr/0026-resource-locking-over-stateless-http.md @@ -0,0 +1,52 @@ +# 0026 Resource locking over stateless HTTP + +Date: During 1.3 cycle (before 2013) + +## Status + +Accepted + +## Context + +LedgerSMB - being a multi-user (web) application - needs to protect some +of its resources (eg., transactions and batches) against concurrent +modification. Applications with direct database access or client/server +applications with persistent connections can use long-running database +connections with record locking to protect against concurrent modification. + +As a web-application, LedgerSMB is a type of client/server application, +with only short-lived connections to the server (and in a load-balancing +scenario even "with short-lived connections to *a* server"). In LedgerSMB's +design, database connections are tied to the HTTP request/response cycle. As +a consequence, the connections don't last long enough to protect resources +from concurrent modification. + +Since the traditional method of record locking does not help LedgerSMB to +protect against concurrent modification of resources, it needs a different +mechanism, which - like more traditional applications - is tied to the +logical duration of access. The logical duration of access would be the +period over which the user is working on the resource. The resource should +be freed after the user completes or given a period of inactivity. + +## Decision + +LedgerSMB will have a `session` concept for each user logged into the +application. This session persists in the database as long as the user +is logged in. Extended periods of inactivity will lead to the session +expiring and cleanup. The session will also be cleaned up when the user +logs out. + +The session will be an anchor for other - session scoped - concepts, +including but not limited to locked transactions and batches. + +## Consequences + +1. There will be a `session` table with records for each logged in user +2. The table will have a clean up mechanism to remove expired sessions +3. Resources which need to be locked, will have a link to the session in + which they are locked, signalling a "logical" lock +4. There needs to be a mechanism to clean up the links to expired sessions + (eg. `ON DELETE SET NULL`) + +## Annotations + diff --git a/doc/conf/webserver/apache-vhost.conf b/doc/conf/webserver/apache-vhost.conf index 3c745a25f9..28f2b50214 100644 --- a/doc/conf/webserver/apache-vhost.conf +++ b/doc/conf/webserver/apache-vhost.conf @@ -44,6 +44,13 @@ NameVirtualHost *:443 # configuration files (those ending in '.conf'), don't exist RewriteRule "\.conf$" - [R=404,L] + RewriteCond "%{REQUEST_FILENAME}" -f + RewriteRule .* "-" [L] + + RewriteCond "%{REQUEST_URI}" "/[a-z0-9A-Z]+(/.*)" + RewriteCond "%{DOCUMENT_ROOT}%1" -f + RewriteRule .* "-" [L] + # Rewrite non-static content to the application backend RequestHeader set X-Forwarded-Proto "https" RequestHeader set X-Forwarded-Port "443" @@ -51,28 +58,14 @@ NameVirtualHost *:443 RewriteCond "%{REQUEST_FILENAME}" !-d RewriteRule "^/(.*)" "http://STARMAN_HOST:5762/$1" [P] ProxyPassReverse "/" "http://STARMAN_HOST:5762/" + # If you host LedgerSMB on a path other than "/" in exposed + # URL space, you need + #ProxyPassReverseCookiePath / /THE-EXPOSED-PATH - # Timeout settings, if you receive reverse proxy - # timeout or 50x errors, especially during company + # Timeout settings, if you receive reverse proxy + # timeout or 50x errors, especially during company # database creation, try to raise these setting. Timeout 300 ProxyTimeout 300 - - # Serve gzip CSS and JS, if they exist and if the client accepts gzip - RewriteCond "%{HTTP:Accept-encoding}" "gzip" - RewriteCond "%{REQUEST_FILENAME}\.gz" -s - RewriteRule "^(.*)\.(css|js)" "$1\.$2\.gz" [QSA] - - # Fix types and prevent double gzip - RewriteRule "\.css\.gz$" "-" [T=text/css,E=no-gzip:1] - RewriteRule "\.js\.gz$" "-" [T=text/javascript,E=no-gzip:1] - - - Header append Content-Encoding gzip - - # Force proxies to store gzip and css/js files separately - Header append Vary Accept-Encoding - - diff --git a/doc/conf/webserver/nginx-github.conf b/doc/conf/webserver/nginx-github.conf index 2ea004bdb1..5f359f8703 100644 --- a/doc/conf/webserver/nginx-github.conf +++ b/doc/conf/webserver/nginx-github.conf @@ -65,21 +65,23 @@ http { return 301 /login.pl; } - # JS & CSS - location ~* \.(js|css)$ { - add_header Pragma "public"; - add_header Cache-Control "public, must-revalidate, proxy-revalidate"; # Production - expires 7d; # Indicate that the resource can be cached for 1 week # Production - try_files $uri =404; + location / { + try_files $uri @strippedprefix @starman; } - location / { + location @strippedprefix { + rewrite ^/([a-z0-9A-Z]+)/(.*) /$2 break; + } + + location @starman { + proxy_pass http://lsmb:5762; + proxy_read_timeout 300; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Host $host; + proxy_set_header X-Forwarded-Server $host; proxy_set_header X-Forwarded-Proto $scheme; - proxy_read_timeout 300; - proxy_pass http://lsmb:5762; } } } diff --git a/doc/conf/webserver/nginx-vhost.conf b/doc/conf/webserver/nginx-vhost.conf index 6aadf231b3..d42ba92ffb 100644 --- a/doc/conf/webserver/nginx-vhost.conf +++ b/doc/conf/webserver/nginx-vhost.conf @@ -41,17 +41,23 @@ server { return 404; } location / { - try_files $uri $uri/ @starman; + try_files $uri @strippedprefix @starman; + } + + location @strippedprefix { + rewrite ^/([a-z0-9A-Z]+)/(.*) /$2 break; } location @starman { # If you changed the port in the Starman service file, change it here too - proxy_pass http://localhost:5762; - proxy_set_header Host $host; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Host $host; - proxy_set_header X-Forwarded-Server $host; - proxy_set_header X-Forwarded-Proto $scheme; + proxy_pass http://localhost:5762; + proxy_read_timeout 300; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Host $host; + proxy_set_header X-Forwarded-Server $host; + proxy_set_header X-Forwarded-Proto $scheme; } } diff --git a/lib/LedgerSMB/Middleware/SessionStorage.pm b/lib/LedgerSMB/Middleware/SessionStorage.pm index b58722c1e9..edc6cf95fb 100644 --- a/lib/LedgerSMB/Middleware/SessionStorage.pm +++ b/lib/LedgerSMB/Middleware/SessionStorage.pm @@ -28,12 +28,14 @@ use warnings; use parent qw ( Plack::Middleware ); use Cookie::Baker; +use HTTP::Status qw( HTTP_BAD_REQUEST ); use Plack::Request; use Plack::Util; use Plack::Util::Accessor qw( cookie cookie_path domain duration inner_serialize secret store force_create ); use Session::Storage::Secure; use String::Random; +use URI; use LedgerSMB::PSGI::Util; @@ -43,10 +45,6 @@ use LedgerSMB::PSGI::Util; Implements Cprepare_app()>. -=head2 $self->call($env) - -Implements Ccall()>. - =cut sub prepare_app { @@ -58,25 +56,60 @@ sub prepare_app { $self->store( $store ); } +=head2 $self->call($env) + +Implements Ccall()>. + +=cut + +sub _prefix_path { + my ($self, $env) = @_; + my $token = $env->{'lsmb.session'}->{company_path} ? + $env->{'lsmb.session'}->{company_path} . '/' : ''; + + if ($self->cookie_path) { + return $self->cookie_path . $token; + } + else { + my $path = ($env->{SCRIPT_NAME} =~ s|[^/]*$||r); + $path .= $token + if $path !~ m/$token/; + + return $path; + } +} + sub call { my $self = shift; my ($env) = @_; my $req = Plack::Request->new($env); + my $referer = $req->headers->header( 'referer' ); + my $referer_uri = $referer ? URI->new( $referer ) : undef; + my $referer_user = $referer_uri ? $referer_uri->query_param( 'user' ) : ''; my $cookie = $req->cookies->{$self->cookie}; my $session = (not $self->force_create) ? $self->store->decode($cookie) : undef; - $session->{csrf_token} //= String::Random->new->randpattern('.' x 23); + my $session_user = $session ? $session->{login} : ''; + + if ($referer_user + and $session_user + and $session_user ne $referer_user) { + return [ HTTP_BAD_REQUEST, + [ 'Content-Type' => 'text/plain' ], + [ "Browser expects session for user '$referer_user', ", + "but session for user '$session_user' found" ] + ]; + } - my $secure = defined($env->{HTTPS}) && $env->{HTTPS} eq 'ON'; - my $path = - $self->cookie_path // - LedgerSMB::PSGI::Util::cookie_path($env->{SCRIPT_NAME}); + $session->{csrf_token} //= String::Random->new->randpattern('.' x 23); $env->{'lsmb.session'} = $session; + my $secure = defined($env->{HTTPS}) && $env->{HTTPS} eq 'ON'; return Plack::Util::response_cb( $self->app->($env), sub { my $res = shift; if (! $self->inner_serialize) { + my $path = $self->_prefix_path( $env ); my $_cookie_attributes = { value => $self->store->encode( $env->{'lsmb.session'}, diff --git a/lib/LedgerSMB/PSGI.pm b/lib/LedgerSMB/PSGI.pm index 3768e73f31..3b2581e52a 100644 --- a/lib/LedgerSMB/PSGI.pm +++ b/lib/LedgerSMB/PSGI.pm @@ -43,7 +43,7 @@ use LedgerSMB::Routes::ERP::API::Templates; use LedgerSMB::Setting; use CGI::Emulate::PSGI; -use HTTP::Status qw( HTTP_FOUND ); +use HTTP::Status qw( HTTP_FOUND HTTP_NOT_FOUND ); use List::Util qw{ none }; use Log::Any; use Log::Log4perl; @@ -55,9 +55,9 @@ use Feature::Compat::Try; use Plack; use Plack::Builder; use Plack::Request::WithEncoding; -use Plack::App::File; use Plack::Middleware::ConditionalGET; use Plack::Middleware::ReverseProxy; +use Plack::Middleware::Static; use Plack::Builder::Conditionals; use Plack::Util; @@ -154,7 +154,7 @@ sub psgi_app { return sub { my $env = shift; my $psgi_req = Plack::Request::WithEncoding->new($env); - my $request = LedgerSMB->new($psgi_req, $wire); + my $request = LedgerSMB->new($psgi_req, $wire); $request->{__action} = $env->{'lsmb.action_name'}; my $res; @@ -224,14 +224,127 @@ sub _hook_psgi_logger { return; } +sub _oldscript_mount { + my ($script, $cookie, $secret, $wire) = @_; + builder { + enable '+LedgerSMB::Middleware::RequestID'; + enable 'AccessLog', format => 'Req:%{Request-Id}i %h %l %u %t "%r" %>s %b "%{Referer}i" "%{User-agent}i"'; + enable '+LedgerSMB::Middleware::SessionStorage', + domain => 'main', + cookie => $cookie, + duration => 60*60*24*90, + secret => $secret, + # can marshall state in, but not back out (due to forking) + # so have the inner scope handle serialization itself + inner_serialize => 1; + enable '+LedgerSMB::Middleware::Log4perl', + script => $script; + enable '+LedgerSMB::Middleware::Authenticate::Company', + provide_connection => 'closed', + factory => $wire->get( 'db' ); + enable '+LedgerSMB::Middleware::MainAppConnect', + provide_connection => 'closed', + require_version => $LedgerSMB::VERSION; + old_app($script, $wire) + } +} + +sub _psgiscript_mount { + my ($script, $cookie, $secret, $wire, $app, $open_connection) = @_; + + builder { + enable '+LedgerSMB::Middleware::RequestID'; + enable 'AccessLog', + format => 'Req:%{Request-Id}i %h %l %u %t "%r" %>s %b "%{Referer}i" "%{User-agent}i"'; + enable '+LedgerSMB::Middleware::SessionStorage', + domain => 'main', + cookie => $cookie, + secret => $secret, + duration => 60*60*24*90; + enable '+LedgerSMB::Middleware::DynamicLoadWorkflow', + max_post_size => $wire->get( 'miscellaneous/max_upload_size' ), + script => $script; + enable '+LedgerSMB::Middleware::Log4perl', + script => $script; + enable '+LedgerSMB::Middleware::Authenticate::Company', + provide_connection => $open_connection ? 'open' : 'none', + factory => $wire->get( 'db' ); + enable '+LedgerSMB::Middleware::MainAppConnect', + provide_connection => $open_connection ? 'open' : 'none', + require_version => $LedgerSMB::VERSION; + enable '+LedgerSMB::Middleware::DisableBackButton'; + $app; + } +} + +sub _api_mount { + my ($cookie, $secret, $wire) = @_; + builder { + enable '+LedgerSMB::Middleware::RequestID'; + enable 'AccessLog', + format => 'Req:%{Request-Id}i %h %l %u %t "%r" %>s %b "%{Referer}i" "%{User-agent}i"'; + enable '+LedgerSMB::Middleware::SessionStorage', + domain => 'main', + cookie => $cookie, + cookie_path => '/', + secret => $secret, + duration => 60*60*24*90; + enable '+LedgerSMB::Middleware::Authenticate::Company', + provide_connection => 'open', + factory => $wire->get( 'db' ); + enable '+LedgerSMB::Middleware::MainAppConnect', + provide_connection => 'open', + require_version => $LedgerSMB::VERSION; + + my $router = router 'erp/api'; + $router->hooks('before' => \&_hook_psgi_logger); + $router->hooks( + 'before' => sub { + my ($env) = @_; + + $env->{wire} = $wire; + return; + }); + sub { return $router->dispatch(@_); }; + } +} + +sub _session_url_space { + my ($wire, $cookie, $secret) = @_; + my $psgi_app = psgi_app($wire); + + builder { + enable 'Plack::Middleware::Static', + root => $wire->get( 'paths/UI' ), + path => sub { 1 }, + pass_through => 'yes'; + + mount "/$_.pl" => _oldscript_mount($_, $cookie, $secret, $wire) + for ('aa', 'am', 'ap', 'ar', 'gl', 'ic', 'ir', 'is', 'oe', 'pe'); + + mount "/$_" => _psgiscript_mount($_, $cookie, $secret, + $wire, $psgi_app, 1) + for (grep { $_ !~ m/^(log(in|out)|setup)[.]pl$/ } + (SCRIPT_NEWSCRIPTS)->@*); + + mount '/logout.pl' => _psgiscript_mount('logout.pl', $cookie, $secret, + $wire, $psgi_app, 0); + + # not using LedgerSMB::Magic::SCRIPT_OLDSCRIPTS: + # it has more than only entry-points + mount '/erp/api/v0' => _api_mount($cookie, $secret, $wire); + } +} + sub setup_url_space { my %args = @_; my $wire = $args{wire}; - my $psgi_app = psgi_app($wire); my $cookie = $wire->get( 'cookie' )->{name} // 'LedgerSMB'; my $secret = $wire->get( 'cookie' )->{secret} // String::Random->new->randpattern('.' x 50); + my $psgi_app = psgi_app($wire); + my $sess_app = _session_url_space($wire, $cookie, $secret); return builder { if (my $proxy_ip = eval { $wire->get( 'miscellaneous/proxy_ip' ); }) { @@ -239,59 +352,12 @@ sub setup_url_space { } enable match_if path(qr!.+\.(css|js|png|ico|jp(e)?g|gif)$!), 'ConditionalGET'; + enable 'Plack::Middleware::Static', + root => $wire->get( 'paths/UI' ), + path => sub { 1 }, + pass_through => 'yes'; - # not using LedgerSMB::Magic::SCRIPT_OLDSCRIPTS: - # it has more than only entry-points - mount "/$_.pl" => builder { - my $script = $_; - enable '+LedgerSMB::Middleware::RequestID'; - enable 'AccessLog', format => 'Req:%{Request-Id}i %h %l %u %t "%r" %>s %b "%{Referer}i" "%{User-agent}i"'; - enable '+LedgerSMB::Middleware::SessionStorage', - domain => 'main', - cookie => $cookie, - duration => 60*60*24*90, - secret => $secret, - # can marshall state in, but not back out (due to forking) - # so have the inner scope handle serialization itself - inner_serialize => 1; - enable '+LedgerSMB::Middleware::Log4perl', - script => $script; - enable '+LedgerSMB::Middleware::Authenticate::Company', - provide_connection => 'closed', - factory => $wire->get( 'db' ); - enable '+LedgerSMB::Middleware::MainAppConnect', - provide_connection => 'closed', - require_version => $LedgerSMB::VERSION; - old_app($script, $wire) - } - for ('aa', 'am', 'ap', 'ar', 'gl', 'ic', 'ir', 'is', 'oe', 'pe'); - - mount "/$_" => builder { - my $script = $_; - enable '+LedgerSMB::Middleware::RequestID'; - enable 'AccessLog', - format => 'Req:%{Request-Id}i %h %l %u %t "%r" %>s %b "%{Referer}i" "%{User-agent}i"'; - enable '+LedgerSMB::Middleware::SessionStorage', - domain => 'main', - cookie => $cookie, - secret => $secret, - duration => 60*60*24*90; - enable '+LedgerSMB::Middleware::DynamicLoadWorkflow', - max_post_size => $wire->get( 'miscellaneous/max_upload_size' ), - script => $script; - enable '+LedgerSMB::Middleware::Log4perl', - script => $script; - enable '+LedgerSMB::Middleware::Authenticate::Company', - provide_connection => 'open', - factory => $wire->get( 'db' ); - enable '+LedgerSMB::Middleware::MainAppConnect', - provide_connection => 'open', - require_version => $LedgerSMB::VERSION; - enable '+LedgerSMB::Middleware::DisableBackButton'; - $psgi_app; - } - for (grep { $_ !~ m/^(log(in|out)|setup)[.]pl$/ } - (SCRIPT_NEWSCRIPTS)->@*); + mount '/erp/api/v0' => _api_mount($cookie, $secret, $wire); mount '/login.pl' => builder { enable '+LedgerSMB::Middleware::RequestID'; @@ -318,59 +384,6 @@ sub setup_url_space { $psgi_app; }; - mount '/logout.pl' => builder { - enable '+LedgerSMB::Middleware::RequestID'; - enable 'AccessLog', - format => 'Req:%{Request-Id}i %h %l %u %t "%r" %>s %b "%{Referer}i" "%{User-agent}i"'; - enable '+LedgerSMB::Middleware::SessionStorage', - domain => 'main', - cookie => $cookie, - secret => $secret, - duration => 60*60*24*90; - enable '+LedgerSMB::Middleware::DynamicLoadWorkflow', - max_post_size => $wire->get( 'miscellaneous/max_upload_size' ), - script => 'logout.pl'; - enable '+LedgerSMB::Middleware::Log4perl', - script => 'login.pl'; - enable '+LedgerSMB::Middleware::Authenticate::Company', - provide_connection => 'none', - factory => $wire->get( 'db' ); - enable '+LedgerSMB::Middleware::MainAppConnect', - provide_connection => 'none', - require_version => $LedgerSMB::VERSION; - enable '+LedgerSMB::Middleware::DisableBackButton'; - $psgi_app; - }; - - mount '/erp/api/v0' => builder { - enable '+LedgerSMB::Middleware::RequestID'; - enable 'AccessLog', - format => 'Req:%{Request-Id}i %h %l %u %t "%r" %>s %b "%{Referer}i" "%{User-agent}i"'; - enable '+LedgerSMB::Middleware::SessionStorage', - domain => 'main', - cookie => $cookie, - cookie_path => '/', - secret => $secret, - duration => 60*60*24*90; - enable '+LedgerSMB::Middleware::Authenticate::Company', - provide_connection => 'open', - factory => $wire->get( 'db' ); - enable '+LedgerSMB::Middleware::MainAppConnect', - provide_connection => 'open', - require_version => $LedgerSMB::VERSION; - - my $router = router 'erp/api'; - $router->hooks('before' => \&_hook_psgi_logger); - $router->hooks( - 'before' => sub { - my ($env) = @_; - - $env->{wire} = $wire; - return; - }); - sub { return $router->dispatch(@_); }; - }; - mount '/setup.pl' => builder { enable '+LedgerSMB::Middleware::RequestID'; enable 'AccessLog', @@ -391,24 +404,24 @@ sub setup_url_space { $psgi_app; }; - enable sub { - my $app = shift; + mount '/' => sub { + my $env = shift; - return sub { - my $env = shift; + return [ HTTP_FOUND, + [ Location => 'login.pl' ], + [ '' ] ] + if $env->{PATH_INFO} eq '/'; - return [ HTTP_FOUND, - [ Location => 'login.pl' ], - [ '' ] ] - if $env->{PATH_INFO} eq '/'; + return [ HTTP_NOT_FOUND, + [ 'Content-Type' => 'text/plain' ], + [ 'not found' ] ] + unless $env->{PATH_INFO} =~ m|^(/[0-9a-zA-Z]+)/|; - return $app->($env); - } + $env->{SCRIPT_NAME} .= $1; + $env->{PATH_INFO} = substr($env->{PATH_INFO}, length($1)); + return $sess_app->($env); }; - - mount '/' => Plack::App::File->new( root => $wire->get('paths/UI') )->to_app; }; - } diff --git a/lib/LedgerSMB/PSGI/Util.pm b/lib/LedgerSMB/PSGI/Util.pm index b85d58c60f..eafb6e46cb 100644 --- a/lib/LedgerSMB/PSGI/Util.pm +++ b/lib/LedgerSMB/PSGI/Util.pm @@ -31,7 +31,7 @@ use HTTP::Status qw( HTTP_OK HTTP_UNAUTHORIZED HTTP_INTERNAL_SERVER_ERROR use parent 'Exporter'; our @EXPORT_OK = qw( internal_server_error unauthorized session_timed_out - incompatible_database cookie_path template_response + incompatible_database template_response ); =head1 METHODS @@ -115,20 +115,6 @@ sub incompatible_database { } -=head2 cookie_path($script) - -Returns the C parameter to be used with the C -(authorization) header. - -=cut - -sub cookie_path { - my $script = shift; - - return ($script =~ s|[^/]*$||r); -} - - =head2 template_response( $template, %args ) Transform a template into a PSGI response, taking additional args diff --git a/lib/LedgerSMB/Scripts/erp.pm b/lib/LedgerSMB/Scripts/erp.pm index cc62ceb4a7..329ea7ddf7 100644 --- a/lib/LedgerSMB/Scripts/erp.pm +++ b/lib/LedgerSMB/Scripts/erp.pm @@ -19,13 +19,13 @@ use strict; use warnings; -=item root +=item __default Displays the root document. =cut -sub root { +sub __default { my ($request) = @_; $request->{title} = "LedgerSMB $request->{version} -- ". diff --git a/lib/LedgerSMB/Scripts/login.pm b/lib/LedgerSMB/Scripts/login.pm index d87c92f67b..8b9ac080cc 100644 --- a/lib/LedgerSMB/Scripts/login.pm +++ b/lib/LedgerSMB/Scripts/login.pm @@ -18,8 +18,10 @@ This script contains the request handlers for logging in of LedgerSMB. use strict; use warnings; +use Digest::MD5 qw( md5_hex ); use HTTP::Status qw( HTTP_OK ); use JSON::MaybeXS; +use URI::Escape; use LedgerSMB::PSGI::Util; @@ -86,9 +88,13 @@ sub authenticate { return $r; } + $request->{_req}->env->{'lsmb.session'}->{company_path} = + md5_hex( $r->{company} ); + my $token = $request->{_req}->env->{'lsmb.session'}->{company_path}; + my $user = uri_escape( $r->{login} ); return [ HTTP_OK, [ 'Content-Type' => 'application/json' ], - [ '{ "target": "erp.pl?__action=root" }' ]]; + [ qq|{ "target": "$token/erp.pl?user=$user" }| ]]; } diff --git a/old/lib/LedgerSMB/oldHandler.pm b/old/lib/LedgerSMB/oldHandler.pm index bccb456ef5..fba6157555 100644 --- a/old/lib/LedgerSMB/oldHandler.pm +++ b/old/lib/LedgerSMB/oldHandler.pm @@ -128,7 +128,6 @@ sub handle { $form->{session_id} = $psgi_env->{'lsmb.session'}->{session_id}; $form->db_init( $psgi_env->{'lsmb.app'}, \%myconfig ); - my $path = LedgerSMB::PSGI::Util::cookie_path($ENV{SCRIPT_NAME}); # we get rid of myconfig and use User as a real object %myconfig = %{ LedgerSMB::User->fetch_config( $form ) };