diff --git a/proxy.php b/proxy.php index 4d816fd..1b9a956 100644 --- a/proxy.php +++ b/proxy.php @@ -18,214 +18,217 @@ // test with: // http://localhost/richproxy/proxy.php?req=/browser/dist/cool.html?file_path=file:///opt/libreoffice/online/test/data/hello-world.odt -function debug_log($msg) -{ - // Disabled for production; enable for debugging - // error_log("richdocumentscode (proxy.php) debug, PID: " . getmypid() . ", Message: $msg"); +function debug_log($msg): void { + // Disabled for production; enable for debugging + // error_log("richdocumentscode (proxy.php) debug, PID: " . getmypid() . ", Message: $msg"); } -function errorExit($msg) -{ - print "
\n"; - print "Error: " . $msg . "
\n"; - print "\n"; - error_log("richdocumentscode (proxy.php) error exit, PID: " . getmypid() . ", Message: $msg"); - http_response_code(400); - exit(); +function errorExit($msg): void { + print "\n"; + print "Error: " . $msg . "
\n"; + print "\n"; + error_log("richdocumentscode (proxy.php) error exit, PID: " . getmypid() . ", Message: $msg"); + http_response_code(400); + exit(); } -debug_log('Proxy v1'); - -// Let the webserver time us out in its own good time. -set_time_limit(0); - -// Where the appimage is installed -$appImage = __DIR__ . '/collabora/Collabora_Online.AppImage'; - -$tmp_dir = ini_get('upload_tmp_dir') ? ini_get('upload_tmp_dir') : sys_get_temp_dir(); -$lockfile = "$tmp_dir/coolwsd.lock"; -$pidfile = "$tmp_dir/coolwsd.pid"; - -function getCoolwsdPid() -{ - global $pidfile; +function getCoolwsdPid(): int { + global $pidfile; - clearstatcache(); - if (file_exists($pidfile)) - { - $pid = rtrim(file_get_contents($pidfile)); - debug_log("Coolwsd server running with pid: " . $pid); - return $pid; - } + clearstatcache(); + if (file_exists($pidfile)) { + $pid = rtrim(file_get_contents($pidfile)); + debug_log("Coolwsd server running with pid: " . $pid); + return $pid; + } - debug_log("Coolwsd server is not running."); - return 0; + debug_log("Coolwsd server is not running."); + return 0; } -function isCoolwsdRunning() -{ - $pid = getCoolwsdPid(); - if ($pid === 0) - return 0; +function isCoolwsdRunning(): int { + $pid = getCoolwsdPid(); + if ($pid === 0) { + return 0; + } - return posix_kill($pid,0); + return posix_kill($pid,0); } -function startCoolwsd() -{ - global $appImage; - global $pidfile; - global $lockfile; - - // Remote font config URL (HTTPS only) - $remoteFontConfig = ""; - if (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') - { - $remoteFontConfigUrl = escapeshellarg("https://" . $_SERVER['HTTP_HOST'] . preg_replace("/richdocumentscode.*$/", "richdocuments/settings/fonts.json", $_SERVER['REQUEST_URI'])); - $remoteFontConfig = "--o:remote_font_config.url=" . $remoteFontConfigUrl; - } - - // Extract the AppImage if FUSE is not available - $launchCmd = "bash -c \"( $appImage $remoteFontConfig --pidfile=$pidfile || $appImage --appimage-extract-and-run $remoteFontConfig --pidfile=$pidfile) >/dev/null & disown\""; - - // Remove stale lock file (just in case) - if (file_exists("$lockfile")) - if (time() - filectime("$lockfile") > 60 * 5) - unlink("$lockfile"); - - // Prevent second start - $lock = @fopen("$lockfile", "x"); - if ($lock) - { - // We start a new server, we don't need stale pidfile around - if (file_exists("$pidfile")) - unlink("$pidfile"); - - debug_log("Launch the coolwsd server: $launchCmd"); - exec($launchCmd, $output, $return); - if ($return) - debug_log("Failed to launch server at $appImage."); - - fclose($lock); - } - - while (!isCoolwsdRunning()) - sleep(1); - - if (file_exists("$lockfile")) - unlink("$lockfile"); +function startCoolwsd(): void { + global $appImage; + global $pidfile; + global $lockfile; + + // Remote font config URL (HTTPS only) + $remoteFontConfig = ""; + if (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') { + $remoteFontConfigUrl = escapeshellarg("https://" . $_SERVER['HTTP_HOST'] . preg_replace("/richdocumentscode.*$/", "richdocuments/settings/fonts.json", $_SERVER['REQUEST_URI'])); + $remoteFontConfig = "--o:remote_font_config.url=" . $remoteFontConfigUrl; + } + + // Extract the AppImage if FUSE is not available + $launchCmd = "bash -c \"( $appImage $remoteFontConfig --pidfile=$pidfile || $appImage --appimage-extract-and-run $remoteFontConfig --pidfile=$pidfile) >/dev/null & disown\""; + + // Remove stale lock file (just in case) + if (file_exists("$lockfile")) { + if (time() - filectime("$lockfile") > 60 * 5) { + unlink("$lockfile"); + } + } + + // Prevent second start + $lock = @fopen("$lockfile", "x"); + if ($lock) { + // We start a new server, we don't need stale pidfile around + if (file_exists("$pidfile")) { + unlink("$pidfile"); + } + + debug_log("Launch the coolwsd server: $launchCmd"); + exec($launchCmd, $output, $return); + if ($return) { + debug_log("Failed to launch server at $appImage."); + } + fclose($lock); + } + + while (!isCoolwsdRunning()) { + sleep(1); + } + + if (file_exists("$lockfile")) { + unlink("$lockfile"); + } } -function stopCoolwsd() -{ - $pid = getCoolwsdPid(); - if (posix_kill($pid,0)) - { - debug_log("Stopping the coolwsd server with pid: $pid"); - posix_kill($pid, 15 /*SIGTERM*/); - } +function stopCoolwsd(): void { + $pid = getCoolwsdPid(); + if (posix_kill($pid,0)) { + debug_log("Stopping the coolwsd server with pid: $pid"); + posix_kill($pid, 15 /*SIGTERM*/); + } } // Check that the setup is suitable for running the coolwsd. // Returns the error ID if we find a problem. -function checkCoolwsdSetup() -{ - global $appImage; - - if (PHP_OS_FAMILY !== 'Linux') - return 'not_linux'; - - if (php_uname('m') !== 'x86_64') - return 'not_x86_64'; - - if (!file_exists($appImage)) - return 'appimage_missing'; - - @chmod($appImage, 0744); - clearstatcache(); // effect of chmod() won't be detected without this call - - if (!is_executable($appImage)) - return 'appimage_not_executable'; - - $disabledFunctions = explode(',', ini_get('disable_functions')); - if (in_array('exec', $disabledFunctions) || @exec('echo EXEC') !== "EXEC") - return 'exec_disabled'; - - exec("LD_TRACE_LOADED_OBJECTS=1 $appImage", $output, $return); - if ($return) - return 'no_glibc'; - - exec('( /sbin/ldconfig -p || scanelf -l ) | grep fontconfig > /dev/null 2>&1', $output, $return); - if ($return) - return 'no_fontconfig'; - - return ''; +function checkCoolwsdSetup(): string { + global $appImage; + + if (PHP_OS_FAMILY !== 'Linux') { + return 'not_linux'; + } + + if (php_uname('m') !== 'x86_64') { + return 'not_x86_64'; + } + + if (!file_exists($appImage)) { + return 'appimage_missing'; + } + + @chmod($appImage, 0744); + clearstatcache(); // effect of chmod() won't be detected without this call + + if (!is_executable($appImage)) { + return 'appimage_not_executable'; + } + + $disabledFunctions = explode(',', ini_get('disable_functions')); + if (in_array('exec', $disabledFunctions) || @exec('echo EXEC') !== "EXEC") { + return 'exec_disabled'; + } + + exec("LD_TRACE_LOADED_OBJECTS=1 $appImage", $output, $return); + if ($return) { + return 'no_glibc'; + } + + exec('( /sbin/ldconfig -p || scanelf -l ) | grep fontconfig > /dev/null 2>&1', $output, $return); + if ($return) { + return 'no_fontconfig'; + } + + return ''; } // parse and emit headers using 'header' ... -function parseLastHeader(&$chunk) -{ - $headers = explode("\r\n", $chunk); - debug_log("Headers: $chunk"); - $chop = 0; - $endOfHeaders = false; - foreach ($headers as $h) - { - debug_log("send: $h"); - $chop += strlen($h) + 2; - if ($h === '') - { - $endOfHeaders = true; - break; - } - header($h); - } - // keep looking for the next header. - $chunk = substr($chunk, $chop); - return $endOfHeaders; +function parseLastHeader(&$chunk): bool { + $headers = explode("\r\n", $chunk); + debug_log("Headers: $chunk"); + $chop = 0; + $endOfHeaders = false; + + foreach ($headers as $h) { + debug_log("send: $h"); + $chop += strlen($h) + 2; + if ($h === '') { + $endOfHeaders = true; + break; + } + header($h); + } + // keep looking for the next header. + $chunk = substr($chunk, $chop); + return $endOfHeaders; } -function startsWith($string, $with) { - return (substr($string, 0, strlen($with)) === $with); +function startsWith($string, $with): bool { + return (substr($string, 0, strlen($with)) === $with); } +debug_log('Proxy v1'); + +// Let the webserver time us out in its own good time. +set_time_limit(0); + +// Where the appimage is installed +$appImage = __DIR__ . '/collabora/Collabora_Online.AppImage'; + +$tmp_dir = ini_get('upload_tmp_dir') ? ini_get('upload_tmp_dir') : sys_get_temp_dir(); +$lockfile = "$tmp_dir/coolwsd.lock"; +$pidfile = "$tmp_dir/coolwsd.pid"; + // avoid unwanted escaping of req= parameter $request = $_SERVER['QUERY_STRING']; + // only asking for status? $statusOnly = false; // handle parameters if (startsWith($request, 'status')) { - $request = ''; - $statusOnly = true; + $request = ''; + $statusOnly = true; } else if (startsWith($request, 'req=')) { - $request = substr($request, strlen('req=')); - if (substr($request, 0, 1) !== '/') - errorExit("First ?req= param should be an absolute path: '" . $request . "'"); + $request = substr($request, strlen('req=')); + if (substr($request, 0, 1) !== '/') { + errorExit("First ?req= param should be an absolute path: '" . $request . "'"); + } } else { - errorExit("The param should be 'status' or 'req=...', but is: '" . $request . "'"); + errorExit("The param should be 'status' or 'req=...', but is: '" . $request . "'"); } debug_log("get URI " . $request); -if ($request === '' && !$statusOnly) - errorExit("Missing, required req= parameter"); +if ($request === '' && !$statusOnly) { + errorExit("Missing, required req= parameter"); +} if (startsWith($request, '/hosting/capabilities') && !isCoolwsdRunning()) { - header('Content-type: application/json'); - header('Cache-Control: no-store'); - - print '{ - "convert-to":{"available":true}, - "hasMobileSupport":true, - "hasProxyPrefix":false, - "hasTemplateSaveAs":false, - "hasTemplateSource":true - }'; - - http_response_code(200); - exit(); + header('Content-type: application/json'); + header('Cache-Control: no-store'); + + print '{ + "convert-to":{"available":true}, + "hasMobileSupport":true, + "hasProxyPrefix":false, + "hasTemplateSaveAs":false, + "hasTemplateSource":true + }'; + + http_response_code(200); + exit(); } // If we can't get a socket open in 3 seconds when that is backed by @@ -234,87 +237,95 @@ function startsWith($string, $with) { // Return the status and exit if it is a ?status request if ($statusOnly) { - header('Content-type: application/json'); - header('Cache-Control: no-store'); - if (!$local) { - $err = checkCoolwsdSetup(); - if (!empty($err)) - print '{"status":"error","error":"' . $err . '"}'; - else if (!isCoolwsdRunning()) { - startCoolwsd(); - print '{"status":"starting"}'; - } - } else if ($errno === 111) { - print '{"status":"starting"}'; - } else { - $response = file_get_contents("http://localhost:9983/hosting/capabilities", 0, stream_context_create(["http"=>["timeout"=>1]])); - if ($response) { - // Version check. - $obj = json_decode($response); - $actVer = substr($obj->{'productVersionHash'}, 0, 8); // expVer is at most 8 characters long - $expVer = '%COOLWSD_VERSION_HASH%'; - if ($actVer !== $expVer && $expVer !== '%' . 'COOLWSD_VERSION_HASH' . '%') { // deliberately split so that sed does not touch this during build-time - // Old/unexpected server version; restart. - error_log("Old server found, restarting. Expected hash $expVer but found $actVer."); - stopCoolwsd(); - // wait 10 seconds max - for ($i = 0; isCoolwsdRunning() && ($i < 10); $i++) - sleep(1); - - // somebody else might have restarted it in the meantime - if (!isCoolwsdRunning()) - startCoolwsd(); - - print '{"status":"restarting"}'; - } - else - print '{"status":"OK"}'; - } - else - print '{"status":"starting"}'; - fclose($local); - } - - http_response_code(200); - exit(); + header('Content-type: application/json'); + header('Cache-Control: no-store'); + + if (!$local) { + $err = checkCoolwsdSetup(); + + if (!empty($err)) { + print '{"status":"error","error":"' . $err . '"}'; + } else if (!isCoolwsdRunning()) { + startCoolwsd(); + print '{"status":"starting"}'; + } + } else if ($errno === 111) { + print '{"status":"starting"}'; + } else { + $response = file_get_contents("http://localhost:9983/hosting/capabilities", 0, stream_context_create(["http"=>["timeout"=>1]])); + + if ($response) { + // Version check. + $obj = json_decode($response); + $actVer = substr($obj->{'productVersionHash'}, 0, 8); // expVer is at most 8 characters long + $expVer = '5093121'; + + if ($actVer !== $expVer && $expVer !== '%' . 'COOLWSD_VERSION_HASH' . '%') { // deliberately split so that sed does not touch this during build-time + // Old/unexpected server version; restart. + error_log("Old server found, restarting. Expected hash $expVer but found $actVer."); + stopCoolwsd(); + // wait 10 seconds max + for ($i = 0; isCoolwsdRunning() && ($i < 10); $i++) { + sleep(1); + } + + // somebody else might have restarted it in the meantime + if (!isCoolwsdRunning()) { + startCoolwsd(); + } + + print '{"status":"restarting"}'; + } else { + print '{"status":"OK"}'; + } + } else { + print '{"status":"starting"}'; + } + fclose($local); + } + + http_response_code(200); + exit(); } + // URL into this server of the proxy script. if ((isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') - || (isset($_SERVER['HTTP_X_FORWARDED_PROTO']) && $_SERVER['HTTP_X_FORWARDED_PROTO'] === 'https' ) - || (isset($_SERVER['HTTP_X_FORWARDED_SSL']) && $_SERVER['HTTP_X_FORWARDED_SSL'] === 'on') -) { - $proxyURL = "https://"; + || (isset($_SERVER['HTTP_X_FORWARDED_PROTO']) && $_SERVER['HTTP_X_FORWARDED_PROTO'] === 'https') + || (isset($_SERVER['HTTP_X_FORWARDED_SSL']) && $_SERVER['HTTP_X_FORWARDED_SSL'] === 'on')) { + + $proxyURL = "https://"; } else { - $proxyURL = "http://"; + $proxyURL = "http://"; } // Start the appimage if necessary -if (!$local) -{ - $err = checkCoolwsdSetup(); - if (!empty($err)) - errorExit($err); - else if (!isCoolwsdRunning()) - startCoolwsd(); - - $logonce = true; - while (true) { - $local = @fsockopen("localhost", 9983, $errno, $errstr, 15); - if ($errno === 111) { - if($logonce) { - debug_log("Can't yet connect to socket so sleep"); - $logonce = false; - } - usleep(50 * 1000); // 50ms. - } else { - debug_log("connected?"); - break; - } - } +if (!$local) { + $err = checkCoolwsdSetup(); + if (!empty($err)) { + errorExit($err); + } else if (!isCoolwsdRunning()) { + startCoolwsd(); + } + + $logonce = true; + + while (true) { + $local = @fsockopen("localhost", 9983, $errno, $errstr, 15); + if ($errno === 111) { + if($logonce) { + debug_log("Can't yet connect to socket so sleep"); + $logonce = false; + } + usleep(50 * 1000); // 50ms. + } else { + debug_log("connected?"); + break; + } + } } if (!$local) { - errorExit("Timed out opening local socket: $errno - $errstr"); + errorExit("Timed out opening local socket: $errno - $errstr"); } // Fetch our headers for later @@ -332,44 +343,44 @@ function startsWith($string, $with) { // Oh dear - PHP's rfc1867 handling doesn't give any php://input to work with in this case. $multiBody = ''; if ($body === '' && count($_FILES) > 0) { - debug_log("Oh dear - PHP's rfc1867 handling doesn't give any php://input to work with"); - $type = isset($headers['Content-Type']) ? $headers['Content-Type'] : $headers['content-type']; - $boundary = trim(explode('boundary=', $type)[1]); - foreach ($_REQUEST as $key=>$value) { - if ($key === 'req') { - continue; - } - $multiBody .= "--" . $boundary . "\r\n"; - $multiBody .= "Content-Disposition: form-data; name=\"$key\"\r\n\r\n"; - $multiBody .= "$value\r\n"; - } - foreach ($_FILES as $file) { - $multiBody .= "--" . $boundary . "\r\n"; - $multiBody .= "Content-Disposition: form-data; name=\"file\"; filename=\"" . $file['name'] . "\"\r\n"; - $multiBody .= "Content-Type: " . $file['type'] . "\r\n\r\n"; - if ($file['tmp_name'] === '') { - errorExit("File " . $file['name'] . " is larger than maximum up-load file-size"); - } - $multiBody .= file_get_contents($file['tmp_name']) . "\r\n"; - } - $multiBody .= "--" . $boundary . "--\r\n"; - $body = $multiBody; - - debug_log("$body"); + debug_log("Oh dear - PHP's rfc1867 handling doesn't give any php://input to work with"); + $type = isset($headers['Content-Type']) ? $headers['Content-Type'] : $headers['content-type']; + $boundary = trim(explode('boundary=', $type)[1]); + foreach ($_REQUEST as $key=>$value) { + if ($key === 'req') { + continue; + } + $multiBody .= "--" . $boundary . "\r\n"; + $multiBody .= "Content-Disposition: form-data; name=\"$key\"\r\n\r\n"; + $multiBody .= "$value\r\n"; + } + foreach ($_FILES as $file) { + $multiBody .= "--" . $boundary . "\r\n"; + $multiBody .= "Content-Disposition: form-data; name=\"file\"; filename=\"" . $file['name'] . "\"\r\n"; + $multiBody .= "Content-Type: " . $file['type'] . "\r\n\r\n"; + if ($file['tmp_name'] === '') { + errorExit("File " . $file['name'] . " is larger than maximum up-load file-size"); + } + $multiBody .= file_get_contents($file['tmp_name']) . "\r\n"; + } + $multiBody .= "--" . $boundary . "--\r\n"; + $body = $multiBody; + + debug_log("$body"); } fwrite($local, $realRequest . "\r\n"); // Send the headers on ... foreach ($headers as $header => $value) { - debug_log("$header: $value\n"); - if ($multiBody !== '' && $header === 'Content-Length') - { - debug_log("Substitute Content-Length of " . $value . " with " . strlen($body)); - $value = strlen($body); - } - - fwrite($local, "$header: $value\r\n"); + debug_log("$header: $value\n"); + if ($multiBody !== '' && $header === 'Content-Length') { + debug_log("Substitute Content-Length of " . $value . " with " . strlen($body)); + $value = strlen($body); + } + + fwrite($local, "$header: $value\r\n"); } + fwrite($local, "ProxyPrefix: " . $proxyURL . "\r\n"); fwrite($local, "\r\n"); fwrite($local, $body); @@ -379,32 +390,33 @@ function startsWith($string, $with) { $rest = ''; $parsingHeaders = true; do { - $chunk = fread($local, 65536); - if($chunk === false) { - $error = implode(' ', error_get_last()); - echo "ERROR ! $error\n"; - debug_log("error on chunk: $error"); - break; - } elseif($chunk === '') { - debug_log("empty chunk last data"); - if ($parsingHeaders) - errorExit("No content in reply from coolwsd. Is SSL enabled in error ?"); - break; - } elseif ($parsingHeaders) { - $rest .= $chunk; - debug_log("build headers to: $rest\n"); - if (parseLastHeader($rest)) { - $parsingHeaders = false; - - $extOut = fopen("php://output", "w") or errorExit("fundamental error opening PHP output"); - fwrite($extOut, $rest); - $rest = ''; - debug_log("passed last headers"); - } - } else { - fwrite($extOut, $chunk); - debug_log("proxy : " . strlen($chunk) . " bytes \n"); - } + $chunk = fread($local, 65536); + if ($chunk === false) { + $error = implode(' ', error_get_last()); + echo "ERROR ! $error\n"; + debug_log("error on chunk: $error"); + break; + } elseif ($chunk === '') { + debug_log("empty chunk last data"); + if ($parsingHeaders) { + errorExit("No content in reply from coolwsd. Is SSL enabled in error ?"); + } + break; + } elseif ($parsingHeaders) { + $rest .= $chunk; + debug_log("build headers to: $rest\n"); + if (parseLastHeader($rest)) { + $parsingHeaders = false; + + $extOut = fopen("php://output", "w") or errorExit("fundamental error opening PHP output"); + fwrite($extOut, $rest); + $rest = ''; + debug_log("passed last headers"); + } + } else { + fwrite($extOut, $chunk); + debug_log("proxy : " . strlen($chunk) . " bytes \n"); + } } while(true); debug_log("closing local socket");