Not long after I bought my DS212+ I became interested in building my own embedded apps: packages that run within a window in DSM. I didn’t had much experience with Perl or Python, but I did with PHP. But I found out it wasn’t easy to get PHP to work as an embedded app without changing configuration files on the NAS, installing some other package first or forwarding a port. I want my apps to be usable for everybody in a user friendly way, so my first goal was to build an embedded Hello World app in PHP without any dependencies.

apache-web and apache-sys

According to the Developers guide a copy or symlink to a PHP package must be made in the /var/services/web directory on the NAS, so the application runs on the “apache-web” service on the NAS. But this way a package is dependent on whether Web Station is running. Also, “apache-web” runs on port 80, while DSM runs on port 5000 (“apache-sys” service). A user needs to forward port 80 to access my package remotely from DSM. Two dependencies I don’t accept.

So I need to find a way to run my PHP script on the “apache-sys” service. Looking into a PERL package that does not have the limitations from above, I found out I need to create a symlink to the package in the /usr/syno/synoman/webman/3rdparty directory on the NAS. I created a simple helloworld.php, built the SPK and installed it on my NAS. I selected my package from the menu and expected to see the victorious words “Hello World” in a window in DSM. But instead I was presented with the raw PHP source of the script.

CGI script

This is because the web server on the Synology NAS does not execute PHP files by default. I could try to make changes in the configuration of the web server or use the package “Init 3rdparty” to do this for me, but then all users of my future packages should do this. Not the user friendliness I’m looking for. So I must find another way. While inspecting other packages, I found out the web server does execute CGI scripts, so I tried to execute my script as CGI:

#!/usr/bin/php
<?php
echo "Hello World";

Not found

This time I got the message “Sorry, the page you are looking for is not found.”, with a “back” button leading nowhere.

Synology DSM Sorry the page you are looking for is not found

The script needs to be executable: chmod +x helloworld.cgi

Open basedir restriction

One step further, but the output was not entirely what I was hoping for:

Warning: Unknown: open_basedir restriction in effect. File(/volume1/@appstore/HelloWorldPhp/helloworldphp/helloworld.cgi) is not within the allowed path(s): (/var/services/tmp:/etc.defaults:/usr/bin/php:/usr/syno/synoman:/etc:/var/run:/volume1/@tmp/php:/var/services/web:/var/services/photo:/var/services/blog:/var/services/homes) in Unknown on line 0
Hello World

I could make changes in the PHP Settings in the Control Panel of DSM, or maybe directly in the php.ini, but again, this would not be very user friendly. Maybe the -d parameter to the PHP executable works:

#!/usr/bin/php -d open_basedir=none
<?php
echo "Hello World";

SAPI

I got the same error as before “Sorry, the page you are looking for is not found.”. Does the executable on the first line of the script not accept parameters?! It should. Digging into other packages I found the solution:

#!/usr/bin/php -d open_basedir=none
<?php
echo "Content-Type: text/html\r\n\r\n";
echo "Hello World";

What is going on? After searching the internet I discovered something I always took for granted: the web server communicates with PHP through a SAPI. The SAPI is responsible processing the raw HTTP request, and returning the raw HTTP response to the webserver. The name of the SAPI used can be retrieved using php_sapi_name(). Until now I apparently always worked with the Apache SAPI named “apache2handler” when building websites.

Executing the PHP script the way I’m doing here uses a different one: the CLI (Command-line Interface) SAPI. A PHP script executed from the command-line has no HTTP request or response, so this SAPI does not process the output as I expected. Some headers are sent automatically, but “Content-Type” is not one of them, which is required to prevent the error from above.

The CLI SAPI does not populate the globals $_GET, $_POST and $_COOKIE. With a little bit of programming this could be solved easily using $_SERVER and parse_str():

  • GET data can be found within $_SERVER['QUERY_STRING']
  • POST data can be found in the STDIN and be read using stream_get_contents(STDIN)
  • COOKIE data can be found in $_SERVER['HTTP_COOKIE']

Parsing raw input and printing raw headers yourself sucks big time. Especially because setcookie() and header() don’t work with this SAPI. You have to build your own cookie and header functions. It’s possible to get this all working, but a better solution is certainly appreciated.

CGI SAPI

While finding more information about SAPI, I encountered the CGI SAPI. This must be the one I need! To execute a script using this SAPI, the binary php-cgi must be used. This SAPI also outputs the Content-Type header. So I started all over, now using the new PHP binary:

#!/usr/bin/php-cgi
<?php
echo "Hello World";

This output the error “Sorry, the page you are looking for is not found.” again. Printing the Content-Type header did not solve it this time. If php-cgi encounters an error, before it prints the Content-Type header, then I would never see that error. So I created a bash script printing the Content-Type header and then calling a helloworld PHP script. This also has the advantage that the PHP script is clean of the executable.

#!/bin/sh
echo Content-Type: text/html$'\r'$'\n'$'\r'$'\n'
/usr/bin/php-cgi ./helloworld.php
<?php
echo "Hello World";

PHP CGI Secutiry

I was right, there was an error:

Security Alert! The PHP CGI cannot be accessed directly.

This PHP CGI binary was compiled with force-cgi-redirect enabled. This means that a page will only be served up if the REDIRECT_STATUS CGI variable is set, e.g. via an Apache Action directive.

For more information as to why this behaviour exists, see the manual page for CGI security.

For more information about changing this behaviour or re-enabling this webserver, consult the installation file that came with this distribution, or visit the manual page.

As suggested by the alert, I tried to set the REDIRECT_STATUS CGI. This can be done setting the cgi.force_redirect directive to 0. That outputs the error “No input file specified”, which can be solved setting the open_basedir directive to none.

#!/bin/sh
echo Content-Type: text/html$'\r'$'\n'$'\r'$'\n'
/usr/bin/php-cgi -d cgi.force_redirect=0 -d open_basedir=none ./helloworld.php

This must be it! But no, the code above outputs the very unexpected to the browser:

Content-type: text/html; charset=UTF-8

echo Content-Type: text/html$'\r'$'\n'$'\r'$'\n'
/usr/bin/php-cgi -d cgi.force_redirect=0 -d open_basedir=none ./helloworld.php

I spent a few hours trying all sorts of things. I discovered the helloworld.php is executed, but it’s output is not sent to the browser. No matter what I did, I kept getting the source of the bash script as output. But then I found the article Running PHP as a CGI by Neale Pickett. I combined the fix in that article with my own work, resulting in the following CGI script:

#!/bin/sh
REDIRECT_STATUS=1 export REDIRECT_STATUS
SCRIPT_FILENAME=$(pwd)/helloworld.php export SCRIPT_FILENAME
/usr/bin/php-cgi -d open_basedir=none $SCRIPT_FILENAME 2>&1

Finally (only) the words “Hello World” showed up in the window!

embedded-php-synology-hello-world-running

General solution

But this way you have to create a CGI script for each PHP file. Also, the request is sent to server.xyz/webman/3rdparty/helloworldphp/helloworld.cgi, while server.xyz/webman/3rdparty/helloworldphp/helloworld.php is executed, which may cause problems for some scripts. Both problems are solved by creating the following .htaccess file and general routing CGI script:

# Turn on rewrite engine.
RewriteEngine on

# Rewrite existing php files to the router script.
# Apache on the Synology NAS automatically redirects url
# containing '../' to the corresponding real path, before
# the router script is executed, so it's impossible to
# execute system files.
RewriteCond %{REQUEST_FILENAME} -f
RewriteRule ^(.*\.php)$ router.cgi [PT,L,QSA,NE]
#!/bin/sh

# Set redirect_status to 1 to get php cgi working.
REDIRECT_STATUS=1 export REDIRECT_STATUS

# Fix several $_SERVER globals.
PHP_SELF=$SCRIPT_URL export PHP_SELF
SCRIPT_NAME=$SCRIPT_URL export SCRIPT_NAME

# Strip web base from SCRIPT_URL, concat it to parent directory
# and apply realpath to construct absolute path to requested script.
WEB_BASE="/webman/3rdparty"
SCRIPT_FILENAME=$(pwd)/..${SCRIPT_URL:${#WEB_BASE}}
SCRIPT_FILENAME=`realpath $SCRIPT_FILENAME`
export SCRIPT_FILENAME

# Execute the requested PHP file.
/usr/bin/php-cgi -d open_basedir=none $SCRIPT_FILENAME 2>&1

Requests to any existing PHP file are rewritten to the router CGI script, which executes that PHP file. This approach is tested using DSM 5. The complete source of the package can be found at GitHub.