Nginx

From Organic Design wiki
Revision as of 18:11, 22 May 2015 by 127.0.0.1 (talk) (Change source-code blocks to standard format)
Nginx-logo.png

Nginx by all accounts is much more efficient than Apache, so on 1 July 2013 we migrated our server and server installation procedure over to Nginx.

Nginx uses an asynchronous event-driven approach to handling requests, instead of the Apache model that defaults to a threaded or process-oriented approach. Nginx's event-driven approach can provide more predictable performance under high loads.

Another reason we're moving over to Nginx is due to the recent interest in Perfect forward secrecy (PFS) coming from articles such as this. PFS is an obscure feature of SSL/TLS and requires at least OpenSSL version 1 and Apache version 2.3.3, but Nginx has supported it for quite some time now.

Quote.pngApache is like Microsoft Word, it has a million options but you only need six. Nginx does those six things, and it does five of them 50 times faster than Apache.
Chris Lea

Installation

All our local installation documentation is in the install a new server procedure. The selecting a good set of ciphers section covers more detail about the perfect forward secrecy issues and installation. And don't forget that cgi.fix_pathinfo must be set to false in /etc/php5/fpm/php.ini to avoid this serious security issue that allows people to execute arbitrary code on the server.

Our URL rewriting rules

The OD server uses a rather complicated URL-rewriting system that allows all the wikis under all domains to run from a singe "catch-all" server block - or actually two, one for plain and one for SSL. This was quite difficult to replicate on Nginx such that it could work in exactly the same way and thereby be "web server agnostic".

The catch-all wiki rewrite rules apply if no other domain-based patterns have matched such as requests with an svn or webmail sub-domain prefix. Our configuration is rather "if" heavy which is strongly discouraged by Nginx gurus, but they're ok as long as they're not inside location context or contain only rewrite last directives or other non-content-handling operations such as set. Our ones here that are inside location context contain only set directives which should be safe. See this article for more detail about how the "if" directive works and why it can be so tricky to use in practice.

This first block sets variable called $wiki which will be used by other following conditions and location blocks (note that even if the .php block is included prior to these settings in the file, they are actually evaluated after them as these blocks are all outside of location scope. $wiki is the directory in which the wiki's file structure resides, i.e. one of the symlinks in /var/www/domains which are named to match the domain of the request. It is used in the following rules in the main scope and also by the nginx.php.conf include for setting the fastcgi_params. Note that this condition always applies and sets $wiki.

if ($host ~* ^(www\.|wiki\.)?(.+)$) {
     set $wiki /$2;
}


Then our first rewrite rule matches the root request with no path or file specified which gets rewritten to the wiki Main Page.

rewrite ^/$ $wiki/wiki/index.php?title=Main_Page&redirect=no last;


Next we need to check if the request is for an image thumbnail with dynamic sizing and if so, route the requests to the thumb.php script. This is simpler than on Apache because Nginx doesn't have the ampersand bug that requires an extra rule for dealing with thumbnails for filenames containing the ampersand symbol.

rewrite ^/files/thumb/./../(.+?)/(\d+)px- $wiki/wiki/thumb.php?w=$2&f=$1 last;


If the requested URI maps to an existing file in the wiki structure, then we rewrite it to point to that.

if (-f $document_root$wiki$uri) {
    rewrite ^ $wiki$uri last;
}


Otherwise as an overall default we treat the request as a friendly URL by rewriting to the script with the URI in the title query-string item. (we used to use the PATH_INFO form, but this fails for article that end in .php).

rewrite ^ $wiki/wiki/index.php?title=$uri last;


That's it for the rewrite rules themselves which is nothing too problematic as it's really just a syntax change from our existing Apache based configuration file. But the real difficulty was to get Nginx to present an environment that matches that which Apache constructs so that the domain-based file-system mapping of our wiki farm works without requiring any change regardless of which web-server we're running. There were a number of challenges here which have all been encapsulated into a file called /var/www/work/nginx.php.conf which is used from any server block that wants to allow PHP execution and sets up the environment close enough to Apache to be compatible with our wikia structure.

Fixing SERVER_NAME

First To make a catch-all type system I've used the default_server option at the end of the listen directive and ommitted the server_name directive since its value is a literal copy of any wild-card or regular expression value it's given. To ensure we don't have an empty SERVER_NAME value in the PHP environment, I've set it to $host in our specific PHP fastcgi_params settings in nginx.php.conf.

Fixing PATH_INFO

The next issue I came across is that there seems to be a problem with the PATH_INFO system where Nginx can't handle URLs that request a script with a continues path such as /foo.php/bar?biz. I used KBeezie's solution which almost works, but it uses the fastcgi_split_path_info function which also seems to have a problem. It's supposed to accept a regular expression containing two captures, one gets assigned to the $fastcgi_script_name variable, and the other to $fastcgi_path_info, but for some reason the former doesn't contain the path portion of the script even if the regular expression states that it should. So in our version of KBeezie's PHP configuration I've used the following custom variables instead of $fastcgi_script_name and $fastcgi_path_info.

if ($uri ~ ^(.+\.php)(/?.*)$) {
    set $script $1;
    set $path $2;
}

Note: In addition to these changes, we need to set $wgUsePathInfo to true in some MediaWiki versions when using Nginx, for example it was needed in MediaWiki 1.19.2, but not in 1.20.3. There's no harm (in our wikia environment) in having it always set though so this has been added to our wikia.php extension.

Fixing SCRIPT_NAME

This code block is at the start of our /var/www/work/nginx.php.conf file which is included from any of our server blocks that require PHP processing. But the block still required further adjusting as the SCRIPT_NAME parameter in this configuration was not matching what Apache was giving and this was causing Nginx to keep redirecting indefinitely. The problem is that SCRIPT_NAME includes the hostname e.g. /organicdesign.co.nz/wiki/index.php instead of just /wiki/index.php that Apache's SCRIPT_NAME would contain. This seems to be getting added to the $document_root value internally somehow, but only for the setting of SCRIPT_NAME. So the initial patch at the start of nginx.php.conf is now as follow:

if ($uri ~ ^(.+?/)?(.+\.php)(.*)$) {
    set $first $1;
    set $script /$2;
    set $path $3;
}
if ($first != $wiki/) {
    set $script $first$script;
}

This effectively removes the beginning portion of $script if it starts with the same value as $wiki (which is set in the main server block to the name of the domain excluding www or wiki prefixes). The effected fastcgi_param names are now set as follows:

fastcgi_param  PATH_INFO          $path;
fastcgi_param  PATH_TRANSLATED    $document_root$wiki$script;
fastcgi_param  SCRIPT_NAME        $script;
fastcgi_param  SCRIPT_FILENAME    $document_root$wiki$script;

Note: The MediaWiki configuration variable $wgServer seems to be getting incorrectly set sometimes when left to the default, so I've now set this manually in our wikia.php extension.

Stand-alone and local wikis

For wikis that have their own code-base directory instead of using the shared code-bases of the wiki farm can use a block similar to the following example to do their friendly URL's.

server {
    listen 80;
    server_name foo.bar;
    include /var/www/work/nginx.php.conf;
    rewrite ^/$ /foo/index.php?title=Main_Page&redirect=no last;
    rewrite ^/files/thumb/./../(.+?)/(\d+)px- /foo/thumb.php?w=$2&f=$1 last;
    if (-f $document_root/foo$uri) {
        rewrite ^ /foo$uri last;
    }
    rewrite ^ /foo/index.php?title=$uri last;
}


Here's a complete configuration file for a wiki served locally including friendly URLs and WebSocket support.

access_log /var/log/nginx/access.log;

server {
	listen 80 default_server;

	root /var/www;
	index index.php;
	client_max_body_size 8m;
	fastcgi_read_timeout 300s;

	location ~ websocket:([0-9]+) {
		proxy_pass http://127.0.0.1:$1;
		proxy_http_version 1.1;
		proxy_set_header Upgrade websocket;
		proxy_set_header Connection upgrade;
	}

	location ~ \.php {
		if ($uri ~ ^(.+?/)?(.+\.php)(.*)$) {
			set $first $1;
			set $script $first$2;
			set $path $3;
		}
		fastcgi_param  PATH_INFO          $path;
		fastcgi_param  PATH_TRANSLATED    $document_root$script;
		fastcgi_param  QUERY_STRING       $query_string;
		fastcgi_param  REQUEST_METHOD     $request_method;
		fastcgi_param  CONTENT_TYPE       $content_type;
		fastcgi_param  CONTENT_LENGTH     $content_length;
		fastcgi_param  SCRIPT_NAME        $script;
		fastcgi_param  SCRIPT_FILENAME    $document_root$script;
		fastcgi_param  REQUEST_URI        $request_uri;
		fastcgi_param  DOCUMENT_ROOT      $document_root;
		fastcgi_param  SERVER_PROTOCOL    $server_protocol;
		fastcgi_param  GATEWAY_INTERFACE  CGI/1.1;
		fastcgi_param  SERVER_SOFTWARE    nginx/$nginx_version;
		fastcgi_param  REMOTE_ADDR        $remote_addr;
		fastcgi_param  REMOTE_PORT        $remote_port;
		fastcgi_param  SERVER_ADDR        $server_addr;
		fastcgi_param  SERVER_PORT        $server_port;
		fastcgi_param  SERVER_NAME        $host;
		fastcgi_param  HTTP_REFERER       $http_referer;
		fastcgi_param  HTTPS              $https;
		fastcgi_pass                      unix:/var/run/php5-fpm.sock;
		fastcgi_index                     index.php;
		fastcgi_intercept_errors          on;
		fastcgi_buffers                   8 16k;
		fastcgi_buffer_size               32k;
	}

	rewrite ^/$ /wiki/index.php?title=Main_Page&redirect=no last;
	rewrite ^/files/thumb/./../(.+?)/(\d+)px- /wiki/thumb.php?w=$2&f=$1 last;
	if (-f $document_root$uri) { rewrite ^ $uri last; }
	rewrite ^ /wiki/index.php$uri last;
}

No WebDAV & Sbbversion

Nginx has almost complete WebDAV functionality when conpiled with the basic DavModule for Nginx which has the PUT, DELETE, MKCOL, COPY and MOVE methods, and the nginx-dav-ext-module which adds the missing PROPFIND and OPTIONS methods. The Debian package comes precompiled with both of these, but to handle Subversion requests directly a higher-level protocol called Delta-V is required which there is no support for in Nginx so Subversion access is currently not possible.

Notes

Some important things to remember about Nginx request processing are:

  • Remember that cgi.fix_pathinfo must be set to false in /etc/php5/fpm/php.ini to avoid this serious security issue that allows people to execute arbitrary code on the server.
  • try_files only does a redirect for the last parameter so others cannot be *.php as the php location won't be processed
  • Only one location block will match and be processed
  • The first exact match (using =) will return immediately
  • Next strings will be matched, the most specific match being chosen
  • Then regex matches will be chosen the first match overriding any string matches (strings can use ^~ to block the regex tests after a match)
  • Rewrites at server level are evaluated before the location directives are evaluated
  • Rewrites within location blocks are then evaluated
  • If rewrites within a location block change the URI, then the locations is evaluated again
  • $request_uri is the original request, $uri is updated after rewrites

The minimum required fastcgi parameters that allow a proper PHP request to be carried out are:

fastcgi_param  REQUEST_METHOD     $request_method;
fastcgi_param  CONTENT_TYPE       $content_type;
fastcgi_param  CONTENT_LENGTH     $content_length;
fastcgi_param  SCRIPT_NAME        $fastcgi_script_name;
fastcgi_param  SCRIPT_FILENAME    $document_root/$fastcgi_script_name;

See also