2

Problem Summary

Flow diagram with authenticated users viewing content from a localhost app.

Let's say you run a server and want to serve content from a localhost app only for users who have logged in with a username and password (authenticated users).

The app runs on your server, but it is not exposed to the public. It is only available via http://localhost:3838 on your server.

For simplicity, let's say this is how you run the localhost app:

cd my-private-folder
python3 -m http.server 3838

Authenticated users should be able to go to http://example.com/app to view the content served by the localhost app.

Unauthenticated users should be denied access and redirected to a login page.

Questions

  • Apache has a directive ProxyPass that modifies HTTP headers in the request before it is sent to the destination. It also has a directive ProxyPassReverse that modifies the headers in the response before it arrives back to the user.

  • Is there a way to mimic Apache's behavior in PHP?

  • Is there a simple way to do this with the Laravel PHP framework?

For reference, there is a package called oxy that achieves this goal in Go. I don't know of an equivalent package for PHP.


Failed attempt 1: Using routes

Here's what I tried with Laravel 5.5:

# /var/www/example.com/routes/web.php
Route::get('/app', function() {
    return Response::make(
        file_get_contents("http://localhost:3838/")
    );
});
# This does not work, because the HTTP header is not modified.
Route::get('/app/{any}', function($any) {
    return Response::make(
        file_get_contents("http://localhost:3838/$any")
    );
})->where('any', '.*');

This successfully passes the content from localhost:3838 to example.com/app, but it fails to pass any of the resources requested by the app, because the HTTP headers are not modified.

For example:

To understand why this fails, see the correctly working Apache solution below. Apache has a directive called ProxyPassReverse that modifies the HTTP headers, so the localhost app requests resources from the correct URL.


Using Apache

Apache works perfectly! However, adding new users requires running the htpasswd command on the server.

With Laravel, new users should be able to register by themselves on the website.

Without Laravel, you could use Apache to secure the app like this:

# /etc/apache2/sites-available/example.com.conf
<VirtualHost *:80>
  ServerName example.com
  Redirect /app /app/
  ProxyPass /app/ http://localhost:3838/
  ProxyPassReverse /app/ http://localhost:3838/
  ProxyPreserveHost On
  <Location /app/>
      AuthType Basic
      AuthName "Restricted Access - Please Authenticate"
      AuthUserFile /etc/httpd/htpasswd.users
      Require user myusername 
      # To add a new user: htpasswd /etc/httpd/htpasswd.users newuser 
  </Location>
</VirtualHost>

This related answer helped me to better understand how the ProxyPassReverse directive works:

Kamil Slowikowski
  • 4,184
  • 3
  • 31
  • 39
  • Can you clarify what you mean by "Laravel does not rewrite the URL..."? If within the `/app/{any}` route you output `dd("http://localhost:3838/$any");` what do you see? – sam Jan 29 '18 at 16:41
  • In the `/app/{any}` route I added `dd("http://localhost:3838/$any");`. When I visit `http://example.com/app/shared` I see `"http://localhost:3838/shared"` – Kamil Slowikowski Jan 29 '18 at 16:46
  • Just clarifying, the `localhost:3838` is already developed and you are trying to make just a proxy in Laravel? – Diogo Sgrillo Jan 29 '18 at 16:48
  • @Diogo Yes, exactly. – Kamil Slowikowski Jan 29 '18 at 16:49
  • @sam In other words, the localhost app is trying to get its resources from the wrong URL. It doesn't know that it is behind Laravel, so the GET requests are going to the wrong place and failing. Since the Apache solution works correctly, I think I need the Laravel version of Apache's `ProxyPassReverse`. – Kamil Slowikowski Jan 29 '18 at 16:50
  • So for clarity, the problem is specifically when accessing assets (e.g: resource.jpg) and not when accessing endpoints like `/app/shared` — `/app/shared` is proxied correctly but `/app/resource.jpg` is not? – sam Jan 29 '18 at 16:50
  • @sam Yes, that's right. – Kamil Slowikowski Jan 29 '18 at 16:51
  • Okay, the problem is actually: "when I use Laravel to proxy requests, my application is not aware of the new request path". This is because you're using `file_get_contents` which is not passing in any of your request headers. You need to pass in your new request path when you request your local service so the local service knows to output URLs as `https://example.com/app/` not `https://example.com/`, I'll write up an answer for you on how to do that now. – sam Jan 29 '18 at 16:54
  • Actually, the problem is that the `assets` are not being proxied by Laravel. Instead, it is trying to serve as it was a local (of Laravel application) file – Diogo Sgrillo Jan 29 '18 at 16:59
  • @sam Yes. Apache uses `ProxyPassReverse` adjust the URL in the `Location`, `Content-Location` and `URI` headers on HTTP redirect responses. On the other hand, PHP `file_get_contents` does not adjust the HTTP headers. I don't know how to do this with PHP. – Kamil Slowikowski Jan 29 '18 at 17:03
  • 1
    @DiogoSgrillo That's just a symptom of the problem. The application (`localhost:3838`) thinks that the request path is `localhost:3838` so it's returning the assets under that (e.g: `localhost:3838/resource.jpg`) when it needs to return them as `localhost:3838/app/resource.jpg`, the application needs to know it's running under `/app`. – sam Jan 29 '18 at 17:04
  • It is exacly what @sam said. How are you generating the assets url under `localhost:3838`? Check if it has a `/` at the begin and remove it. – Diogo Sgrillo Jan 29 '18 at 18:22
  • @DiogoSgrillo Since Apache can solve this without modifying the localhost app, I would like to find a similar solution for PHP because it's too much work to modify the localhost app. In other words, what is the correct way to use PHP to adjust HTTP headers? – Kamil Slowikowski Jan 29 '18 at 18:34
  • Why the term "Iframe" comes to my mind? – common sense Jan 29 '18 at 20:21
  • @commonsense Sorry, I don't think `iframe` is relevant here. The `localhost` app is not running on the user's computer. It is running on the remote server. – Kamil Slowikowski Jan 29 '18 at 20:39

2 Answers2

0

If the problem is only with the assets, you can route them as well to the localhost:3838 application:

# /var/www/example.com/routes/web.php
Route::get('/app/{any}', function($any) {
    return Response::make(
        file_get_contents("http://localhost:3838/$any")
    );
})->where('any', '.*');

Route::get('/{assets}', function($assets) {
    return Response::make(
        file_get_contents("http://localhost:3838/$assets")
    );
})->where('assets', '.*(png|jpe?g|js)');
Diogo Sgrillo
  • 2,601
  • 1
  • 18
  • 28
  • Sorry, but this won't do. After more research, I realize that I am trying to mimic the behavior of Apache's directives `ProxyPass` and `ProxyPassReverse`. These directives modify the HTTP header before it arrives at the `localhost` app. When the `localhost` app responds, the header is modified again before it is delivered back to the user's browser. Therefore, PHP must also modify the HTTP headers to get the same behavior as Apache. The `file_get_contents` function is just a hack, but it does not modify the headers. – Kamil Slowikowski Jan 31 '18 at 20:50
0

I think Jens Segers has a package that can enable PHP to mimic the ProxyPass directive: https://github.com/jenssegers/php-proxy

Here's an example from the GitHub README:

use Proxy\Proxy;
use Proxy\Adapter\Guzzle\GuzzleAdapter;
use Proxy\Filter\RemoveEncodingFilter;
use Zend\Diactoros\ServerRequestFactory;

// Create a PSR7 request based on the current browser request.
$request = ServerRequestFactory::fromGlobals();

// Create a guzzle client
$guzzle = new GuzzleHttp\Client();

// Create the proxy instance
$proxy = new Proxy(new GuzzleAdapter($guzzle));

// Add a response filter that removes the encoding headers.
$proxy->filter(new RemoveEncodingFilter());

// Forward the request and get the response.
$response = $proxy->forward($request)->to('http://example.com');

// Output response to the browser.
(new Zend\Diactoros\Response\SapiEmitter)->emit($response);
Kamil Slowikowski
  • 4,184
  • 3
  • 31
  • 39