在反向代理(ProxyPass)后面配置Mojolicious中带有前缀的URL

时间:2014-05-09 18:27:06

标签: apache perl url-rewriting reverse-proxy mojolicious

我正在寻找一种可靠的方法来配置在/ app下的Apache反向代理后面运行Mojolicious,以便url_for('/foo')实际返回/app/foo而不仅仅是/foo(否则全部链接将被破坏)。

documentation显示了/下所有内容的反向代理示例。但这不是我需要的,因为应用程序应该在/ app。下 将ProxyPass / http://localhost:8080/转换为ProxyPass /app http://localhost:8080/会导致问题,因为应用程序生成的所有网址中都会丢失/app前缀。

该文档还有一个section on rewriting,其中有一个before_dispatch挂钩示例,它将获取请求URL的第一部分并将其用作基础。这需要在Apache配置中将前缀附加到ProxyPass url(ProxyPass /app http://localhost:8080/app/带尾部斜杠),这在该页面上似乎没有提及,但可能不需要(“移动第一部分和从路径到基础路径的斜线“)因为它很明显。这样就可以调用http://localhost/app/page,这会变成http://localhost:8080/app/page('app'被钩子删除),其中url_for('/foo')会返回'/app/foo'http://localhost/app/foo) ,因此链接将是正确的(在ProxyPass规则中没有尾随斜杠,这将使/apppage/foo)。

但是,在此示例中,网址修改始终在生产模式下生成(if app->mode eq 'production')。所以直接调用后端服务器(http://localhost:8080/testpage)将不再起作用,因为所有的URL都会被破坏。

所以我想,我会检查是否设置X-Forwarded-For标头(通过mod_proxy_http),这将始终设置为反向代理请求。由于Apache mod_proxy documentation提到此标头可能已经在客户端请求中设置(并且最终包含多个值),我首先将其从请求中删除 - 因为发送此标头的客户端不应该导致网址修改。

Apache VirtualHost配置:

# ProxyPreserveHost: Mojo::URL::to_abs() not 127.0.0.1
ProxyPreserveHost On
<Location "/app/">
    # ProxyPass: prefix pass-thru
    ProxyPass http://localhost:3000/app/
    # RequestHeader: must not be set externally
    RequestHeader unset X-Forwarded-For
</Location>

Hook in Mojolicious startup():

$self->hook('before_dispatch' => sub {
    my $c = shift;
    my $behind_proxy = !!$c->req->headers->header('X-Forwarded-Host');
    if ($behind_proxy) {
        push @{$c->req->url->base->path->trailing_slash(1)},
            shift @{$c->req->url->path->leading_slash(0)};
        $c->req->url->path->trailing_slash(0) # root 404
            unless @{$c->req->url->path->parts};
    }
});

这似乎有用......

问题:我的方法在“现实世界”中是否可靠运行还是有缺陷?

编辑:
通过反向代理请求根地址(http://localhost:3000/app/)总是导致错误404.所以在这种情况下我添加了两行来关闭尾部斜杠。由于我在文档中找不到,可能有更好的方法。

3 个答案:

答案 0 :(得分:3)

您需要在 before_dispatch hook

中为每个请求网址设置基本路径
$app->hook(before_dispatch => sub {
  my $c = shift;
  $c->req->url->base->path('/app/');
});

示例:

use Mojolicious::Lite;

app->hook(before_dispatch => sub {
  shift->req->url->base->path('/app/');
});


get '/' => sub {
  my $c = shift;
  $c->render(text => $c->url_for('test'));
};

get '/test/url' => sub { ... } => 'test';

app->start;

结果:

$ curl 127.0.0.1:3000
/app/test/url

答案 1 :(得分:1)

我现在正在回答自己的问题,因为我收到了一些在他们的应用程序代码中添加硬编码前缀的人的建议(不仅仅是在这里)。显然,手动为所有生成的URL加上前缀不是解决方案。想象一下部署在同一服务器上的同一应用程序的两个实例,一个在/app1下,另一个在/app2下。在我的问题中,建议的代码的全部要点是,如果通过反向代理访问应用程序且不会中断直接发送到应用程序服务器的请求,则该应用程序可以正常工作并生成正确的url。开发人员可以运行Morbo,但是硬编码前缀可以解决该问题。另外,我在问题中至少犯了一个错误,但似乎没人注意到。

代码中的缺陷

在我的问题示例中,我的斜线太多了。 定义Location块的方式,对/app的请求(不带斜杠)将失败。最好这样写:

<Location "/app">
...

接下来,我写了我检查X-Forwarded-For标头,但实际上检查了X-Forwarded-Host。如果我也要清除该标头,但我却清除了X-Forwarded-For,那将不是问题。遇到该尴尬的错误,安全机制将无法正常工作,因此,如果您在localhost:3000上与应用程序服务器一起使用时设置了此标头,则该应用程序将尝试修复操纵的url,即使它不应该这样做。

应该是:

RequestHeader unset X-Forwarded-Host

示例:

ProxyPreserveHost On
<Location /app>
    ProxyPass http://localhost:3000/app
    RequestHeader unset X-Forwarded-Host
</Location>

只要应用程序在任何地方都使用相对URL,就不需要ProxyPreserveHost指令。如果应用程序要生成绝对URL,例如url_for('/page')->to_abs,则应启用ProxyPreserveHost,否则外部客户端将获得http://localhost:3000/app/page

反向代理检测

当我写这个问题时,我看到了Mojolicious documentation中的before_dispatch钩子,并且如问题中所指出的,我想将其用于在/app下运行的应用程序。但是,我不想破坏Morbo。该示例假定应用程序在反向代理后面运行时处于生产模式($app->mode),但在通过Morbo直接访问时不处于生产模式,但是我不想为其他所有请求更改模式。

这就是为什么我添加了一个条件来检查请求是否通过反向代理的原因。由于此标头仅由Apache(由mod_proxy_http module设置,而不由Mojo::Server::Morbo设置,因此可以用作反向代理检测。我相信,加上清除X-Forwarded-Host的正确指令,我相信我的问题的答案是,应该可以可靠地起作用。

(尽管最后一部分不是严格必需的,只要直接访问应用程序服务器仅限于开发人员即可。)

操纵的网址

为说明为什么我在Apache配置的/app行中添加了ProxyPass前缀,我想指出这种方法操纵url以允许应用程序在给定的前缀。有another question个人忘记在Apache配置中添加前缀,而我写了answer explaining what the hook does

Morbo: localhost:3000
Apache reverse proxy: host.com/app or localhost/app

# Browser > Apache:
http://host.com/app
# Apache (ProxyPass http://localhost:3000/) > Mojolicious sees:
GET /
url_for '/test' = /test 
(or even //test if the hook pushes undef, see the other answer linked above)
# Apache (configured as described above) > Mojolicious sees:
GET /app
# Hook:
base = /app
request = /
url_for '/test' = /app/test 

通常,ProxyPass伪指令中的本地目标参数不带前缀,而只是像ProxyPass http://...:3000/之类的东西。在这种情况下,应用程序不知道前缀,这就是为什么所有生成的url和链接都不完整的原因。

此方法要求您让Apache将前缀传递给应用程序服务器。该应用程序不知道前缀,因此不知道如何处理对/app/page的请求。这是钩子插入的地方。它假定路径的第一级始终是前缀,因此它将/app/page转换为/page,并方便地将/app前缀附加到url base(在生成网址时使用),请确保指向/test的链接实际上指向/app/test

显然,对于直接发送给Morbo的任何请求,都不应进行此修改。

替代

或者,可以由反向代理设置自定义请求标头,然后由挂钩使用该标头来生成有效的url。 Mojolicious::Plugin::RequestBase module以这种方式工作。它希望您在X-Request-Base标头而不是url中定义前缀:

RequestHeader set X-Request-Base "/app"

在这种情况下,应用程序应该只接收相对于该前缀的url请求:

ProxyPass http://localhost:3000/

该模块真正要做的就是拾取标头并将其用作url库:

$c->req->url->base($url); # url = X-Request-Base = /app

示例:

<Location /app>
    ProxyPass http://localhost:3000
    RequestHeader set X-Request-Base "/app"
</Location>

这是一个很好且简单的解决方案。请注意,在这种情况下,/app前缀会出现两次。当然,hook implemented by that module仅在设置了X-Request-Base头的情况下才起作用,就像上面显示的钩子在未设置X-Forwarded-Host头的情况下什么也不起作用。

答案 2 :(得分:0)

您应该将应用程序安装在所需的路径下。

startup你应该这样做:

$r =  $app->routes;
$r =  $r->any( '/api' )->partial( 1 );

$r->get( '/test' );

您不应该专门配置您的apache。当GET /api/test到来时,您的应用将获得/api/test路由。此路线部分与/api匹配,其余路线/test将分配到->stash->{ path }

因此,将针对/testsource

检查休息路线