简体   繁体   中英

Reverse proxy pass websocket request with PHP in apache

I have a client (noVNC) that needs to connect through websocket to my server.

The request is received by a NGINX server that acts as reverse proxy. It redirects the request to the apache server. The apache server runs index.php and check some session variables of the requester. If everything it's ok, PHP should proxy to the root server (a VNC server with websocket support) in the same way as NGINX is doing it.

If I skip the apache/PHP step and connect directly the NGINX reverse proxy to the VNC/WS server everything works well. But I can't make it work with PHP in the middle.

NGINX site config:

server {
    listen 443 ssl;
    server_name vnc.domain.com;
    gzip off;

    server_tokens off;

    location / {
            proxy_set_header X-Forwarded-Host $host:$server_port;
            proxy_set_header X-Forwarded-Server $host;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_set_header Host $host;
            proxy_set_header X-Real-IP $remote_addr;


            proxy_set_header Upgrade $http_upgrade;
            proxy_set_header Connection "upgrade";

            proxy_pass https://webapi.lan/;
    }
ssl_certificate /etc/letsencrypt/live/vnc.domain.com/fullchain.pem; # managed by Certbot
ssl_certificate_key /etc/letsencrypt/live/vnc.domain.com/privkey.pem; # managed by Certbot


}

server {
    listen 80;
    server_name vnc.domain.com;
    return 301 https://$host$request_uri;
}

In the PHP side I'm a little bit lost. I have started with something like:

echo file_get_contents('proxypage');

But then all the headers info was missing. Reading other answer here in SO I have seen that people use curl, so I have installed curl. Then, I have been solving errors 1 by 1... Setting http response code manually to 101 to switch protocols from http to ws, then setting headers manually (upgrade & connection), then noVNC was complaining about no Sec-WebSocket-Accept header, so I forwarded the header from the curl response. Now it's saying the value of the header it's incorrect, probably because all the request headers aren't arriving to the final VNC server... And I don't know how to forward all the headers from PHP to the final VNC server. Right now it's like that:

function proxy_pass_vnc( )
{
$options = [
    CURLOPT_RETURNTRANSFER => true,     // return web page
    CURLOPT_HEADER         => true,     // return headers
    CURLOPT_FOLLOWLOCATION => true,     // follow redirects
    CURLOPT_ENCODING       => "",       // handle all encodings
    CURLOPT_AUTOREFERER    => true,     // set referer on redirect
    CURLOPT_CONNECTTIMEOUT => 120,      // timeout on connect
    CURLOPT_TIMEOUT        => 120,      // timeout on response
    CURLOPT_MAXREDIRS      => 10,       // stop after 10 redirects
];

$ch = curl_init('http://192.168.100.13:5900');
curl_setopt_array($ch, $options);
$remoteSite = curl_exec($ch);
$header = curl_getinfo($ch);
curl_close($ch);

header('Upgrade: websocket');
header('Connection: Upgrade');
header('Sec-WebSocket-Accept: ' . $header['Sec-WebSocket-Accept']);
http_response_code(101);
echo $remoteSite;
#return $header;
}

It feels like a very manual process and I think it has to exist an easier way. There isn't a way to tell apache from PHP to just proxy the incoming request and adjust the headers as needed? It seems like a job better suited for apache than for PHP. If not, how can I code it to automatically pass all the headers from the request to the final server and echo all the headers returned by curl?

Thanks for your time.

INFO: PHP 7.3,

EDIT: Improved my PHP to:

$vnc_request_headers = apache_request_headers();
$vnc_request_headers_simple_array = [];
foreach ($vnc_request_headers as $vnc_request_header => $value) {
    array_push($vnc_request_headers_simple_array, $vnc_request_header . ': ' . $value);
}

$options = [
    CURLOPT_HTTPHEADER     => $vnc_request_headers_simple_array,
    CURLOPT_RETURNTRANSFER => true,     // return web page
    CURLOPT_HEADER         => true,     // return headers
    CURLOPT_FOLLOWLOCATION => true,     // follow redirects
    CURLOPT_ENCODING       => "",       // handle all encodings
    CURLOPT_AUTOREFERER    => true,     // set referer on redirect
    CURLOPT_CONNECTTIMEOUT => 120,      // timeout on connect
    CURLOPT_TIMEOUT        => 120,      // timeout on response
    CURLOPT_MAXREDIRS      => 10,       // stop after 10 redirects
];

$ch = curl_init('http://192.168.100.13:5900');
curl_setopt_array($ch, $options);

$vnc_response_headers = [];
// this function is called by curl for each header received
curl_setopt($ch, CURLOPT_HEADERFUNCTION,
function($curl, $header) use (&$vnc_response_headers)
    {
        $len = strlen($header);
        $header = explode(':', $header, 2);
        if (count($header) < 2) // ignore invalid headers
          return $len;

        $vnc_response_headers[strtolower(trim($header[0]))][] = trim($header[1]);

        return $len;
    }
);

$vnc_response = curl_exec($ch);
$vnc_header_size = curl_getinfo($ch, CURLINFO_HEADER_SIZE);
$vnc_httpCode = curl_getinfo($ch , CURLINFO_HTTP_CODE);
curl_close($ch);

$vnc_body = substr($vnc_response, $vnc_header_size);

http_response_code($vnc_httpCode);

foreach ($vnc_response_headers as $vnc_response_header => $array) {
    foreach($array as $vnc_response_header_value) {
        header($vnc_response_header . ": " . $vnc_response_header_value);
    }
}

echo $vnc_body;

But still nothing. It works if I point it to an standard web server, but it fails when pointed to the websocket server (the VNC server) with an 504 error (after being pending for while). What makes it fail when working with ws?

EDIT2:

Increasing the NGINX timeout to get rid of the 504 error, I have seen that the code gets stuck in the curl_exec($ch) command, until curl timeouts after 2 minuts, which could make sense because websocket connection never gets closed until the socket it's closed.

I have debugged the request & response headers and they are the correct ones and they arriving to the final websocket server (request headers) and to the browser (response headers). The problem is that when the request resolves it's because cURL has timed out and gives no possibility to websocket comunication.

So, how to proxy the websocket requests with PHP to the final websocket server without getting it stuck??

Seems like nowadays there isn't a way to manage websockets in PHP like that, except building yourself or find a whole PHP websocket server and manage the websocket request inside PHP instead of proxy it. But thanks to kicken I have found this really smart trick.

As you could read in my original post, I have a "general" nginx reverse proxy in front of the php-apache pair. So, what it can be done is to use Authentication Based on Subrequest Result nginx feature.

This allows make a subrequest to the apache-php server when a (websocket) request arrives to the nginx server. If the php response to the subrequest it's 2xx, nginx proxies to the final websocket server and everything works. If the php subrequest response it's 4xx, the main request is blocked.

This way you have blinded a websocket request behind a php session barely without any extra code if you already had a php login for standard requests.

That's for nginx, I don't know if there is somehting like that for apache.

That's how my final nginx config file looks like:

server {
    listen 443 ssl;
    server_name vnc.domain.com;

    server_tokens off;

    location / {
            auth_request /auth;

            proxy_set_header X-Forwarded-Host $host:$server_port;
            proxy_set_header X-Forwarded-Server $host;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_set_header Host $host;
            proxy_set_header X-Real-IP $remote_addr;

            proxy_pass http://vnc.lan:5900/;
    }

    location = /auth {
            internal;
            proxy_pass https://webapi.lan/index.php;

            proxy_pass_request_body off;
            proxy_set_header        Content-Length "";
            proxy_set_header        X-Original-URI $request_uri;
    }

ssl_certificate /etc/letsencrypt/live/vnc.domain.com/fullchain.pem; # managed by Certbot
ssl_certificate_key /etc/letsencrypt/live/vnc.domain.com/privkey.pem; # managed by Certbot


}

server {
        listen 80;
        server_name vnc.domain.com;
        return 301 https://$host$request_uri;
}

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM