Archive for the ‘PHP’ category

Configuring Nginx for Nibbleblog 4.0.3

July 25th, 2014

This is a simple configuration example to replicate the .htaccess rules for NibbleBlog 4.0.3.

For those unfamiliar with NibbleBlog:

Nibbleblog is a powerful engine for creating blogs, all you need is PHP to work. Very simple to install and configure (only 1 step).

Nginx Configuration Example
Tested on Nginx 1.7.3

server {
    listen 0.0.0.0:80;
    root /path/to/public_html;
    server_name domain.com www.domain.com;
 
    # Directory indexing is disabled by default
    # So no need to disable it, unless you enabled it yourself
 
    # access log turned off for speed
    access_log off; #enable if you need it
    error_log /var/log/nginx/nib.error.log; #or path to your error log
 
    error_page 404 = /index.php?controller=page&action=404;
 
    # main location block
    location / {
        expires 7d;
        try_files $uri $uri/ @rewrites;
    }
 
    # rewrite rules if file/folder did not exist
    # based off rules for version 4.0.3's .htaccess
    location @rewrites {
        rewrite ^/category/([^/]+)page-([0-9+])$ /index.php?controller=blog&action=view&category=$1&number=$2 last;
        rewrite ^/category/([^/]+)/$ /index.php?controller=blog&action=view&category=$1&number=0 last;
        rewrite ^/tag/([^/]+)/page-([0-9]+)$ /index.php?controller=blog&action=view&tag=$1&number=$2 last;
        rewrite ^/tag/([^/]+)/$ /index.php?controller=blog&action=view&tag=$1&number=0 last;
        rewrite ^/page-([0-9]+)$ /index.php?controller=blog&action=view&number=$1 last;
        rewrite ^/post/([^/]+)/$ /index.php?controller=post&action=view&post=$1 last;
        rewrite ^/post-([0-9]+)/(.*)$ /index.php?controller=post&action=view&id_post=$1 last;
        rewrite ^/page/([^/]+)/$ /index.php?controller=page&action=view&page=$1 last;
        rewrite ^/feed/$ /feed.php last;
        rewrite ^/([^/]+)/$ /index.php?controller=page&action=$1 last;
 
        # if any of the redirects/urls above contain a *.php file in the url
        # then you may have to create a separate location ~ ^/post-([0-9]+)/, etc block
        # otherwise the php block will be invoked before this @rewrites block does
 
    }
 
    # cache control for static files
    location ~* \.(?:ico|css|js|gif|jpe?g|png)$ {
        expires max;
        add_header Pragma public;
        add_header Cache-Control "public, must-revalidate, proxy-revalidate";
    }
 
    # files to block or handle differently
 
    # Don't log commonly requested files that may not always exist
    location = /robots.txt  { access_log off; log_not_found off; }
    location = /favicon.ico { access_log off; log_not_found off; }
 
    # block access to these files (shadow.php, keys.php, hidden/tmp files, xml files)
    location ~ /shadow\.php { access_log off; log_not_found off; deny all; } # block access to shadow.php
    location ~ /keys\.php   { access_log off; log_not_found off; deny all; } # block access to keys.php
    location ~ /\.          { access_log off; log_not_found off; deny all; } # block access to hidden files (starting with .)
    location ~ ~$           { access_log off; log_not_found off; deny all; } # block access to vim temp files (starting with ~)
    location ~ \.xml        { access_log off; log_not_found off; deny all; } # block access to xml files
 
    # typical PHP handling block
    # with a safeguard against passing a non-existent
    # php file to the PHP interpreter
 
    location ~ \.php$ {
        try_files $uri =404;
 
        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        $fastcgi_script_name;
        fastcgi_param  SCRIPT_FILENAME    $request_filename;
        fastcgi_param  REQUEST_URI        $request_uri;
        fastcgi_param  DOCUMENT_URI       $document_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;
 
        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        $server_name;
 
        fastcgi_pass 127.0.0.2:9000;
 
        #fastcgi_pass unix:/tmp/php.sock; 
        #use the above line instead if running php-fpm on a socket
    }
 
}

This should work pretty well as long as the newest NibbleBlog does not utilize php file names in the friendly urls (from my testing, it no longer does that).

Also Friendly URLs are not enabled by default in NibbleBlog, you need to enable it under SEO tab under settings. (ignore the instructions regarding .htaccess since you’re not using apache).

Twitter Timeline Feed with PHP & JSON

April 14th, 2013

I have had my share of users needing help fixing broken implementations with twitter on their own website, such as pulling from an incorrect url, or using the incorrect twitter API format. Most of the time they are pulling the XML response, but twitter offers a much nicer format that works rather well with PHP; JSON.

Quick Example
A pull from your twitter timeline is as simple as:

<?php
 
$json = file_get_contents("https://api.twitter.com/1/statuses/user_timeline/karlblessing.json", TRUE);
 
$twitter_feed = json_decode($json, true);
 
// check for errors
if(isset($twitter_feed['errors'])) {
    foreach($twitter_feed['errors'] as $error) {
        echo "(".$error['code'].") ".$error['message']."<br>";
    }
} else {
    // Loop thru and spit out the text of each tweet returned
    foreach($twitter_feed as $tweet) {
        echo $tweet['text']."<br>";
    }
?>

I do not use Twitter often, so course my own feeds may be scarce.

Example utilizing an array

If you wish to have a bit more control over how the content is interpreted. For this we can utilize an array so the data may be used elsewhere on a page.

<?php
/* 
Configuration Array
 
explanation of each option can be seen here : https://dev.twitter.com/docs/api/1/get/statuses/user_timeline
 
user = the screen_name of the twitter user you wish to query
count = the "maximum" number of items to be returned
retweet = true or false to include retweets in the response
entities = true or false
exclude_replies = true or false to exclude replies
contributor_details = true or false
trim_user = true or false to trim extra user details
 
*/
 
$twitter = array(
	"user" => "karlblessing",
	"count" => "4",
	"retweet" => "true",
	"entities" => "true",
	"exclude_replies" => "true",
	"contributor_details" => "false",
	"trim_user" => "false"
);
 
// a small function to convert "created at" time to [blank] minutes/hours/days ago
 
function relativeTime($time)
{
    $delta = strtotime('+2 hours') - strtotime($time);
    if ($delta < 2 * MINUTE) {
        return "1 min ago";
    }
    if ($delta < 45 * MINUTE) {
        return floor($delta / MINUTE) . " min ago";
    }
    if ($delta < 90 * MINUTE) {
        return "1 hour ago";
    }
    if ($delta < 24 * HOUR) {
        return floor($delta / HOUR) . " hours ago";
    }
    if ($delta < 48 * HOUR) {
        return "yesterday";
    }
    if ($delta < 30 * DAY) {
        return floor($delta / DAY) . " days ago";
    }
    if ($delta < 12 * MONTH) {
        $months = floor($delta / DAY / 30);
        return $months <= 1 ? "1 month ago" : $months . " months ago";
    } else {
        $years = floor($delta / DAY / 365);
        return $years <= 1 ? "1 year ago" : $years . " years ago";
    }
}
 
// prepare the array
 
$twitter_feed = array();
 
// form the API url for the request
 
$api_url = "https://api.twitter.com/1/statuses/user_timeline/".$twitter['user'].
	".json?include_entities=".$twitter['entities'].
	"&include_rts=".$twitter['retweet'].
	"&exclude_replies=".$twitter['exclude_replies'].
	"&contributor_details=".$twitter['contributor_details'].
	"&trim_user=".$twitter['trim_user'].
	"&count=".$twitter['count'];
 
// obtain the results 
 
$json = file_get_contents($api_url, true);
 
// decode the json response as a PHP array
 
$decode = json_decode($json, true);
 
//check for error during the last decode
if(json_last_error != JSON_ERROR_NONE) {
	// http://www.php.net/manual/en/function.json-last-error.php
	$twitter_feed[] = array('error' => "Unable to decode response");
} elseif(isset($decode['errors'])) {
	// just grabbing the first error listed
	$twitter_feed[] = array('error' => $decode['errors'][0]['message']);
} else {
	// if no decode or twitter response errors then proceed.
 
	foreach($decode as $tweet) {
		// If you are including retweets, you may want to check the status
		// as the main text is truncated as opposed to the original tweet
 
		// If you used the trim_user option, the retweeted user screen name will not be avaialble
 
		if (isset($tweet['retweeted_status'])) {
			$tweet_text = "RT @{$tweet['retweeted_status']['user']['screen_name']}: 
			{$tweet['retweeted_status']['text']}";
		} else {
			$tweet_text = $tweet['text'];
		}
 
		$twitter_feed[] = array(
			'text' => $tweet_text, 
			'created_at' => relativeTime($tweet['created_at']),
			'link' => "http://twitter.com/".$twitter['user']."/status/".$tweet['id']
		);
 
		unset($tweet_text);
	}
}
 
unset($decode, $json, $tweet);
?>
 
<?php
 
// in a later portion of your code or page you can break down the array like so:
 
foreach($twitter_feed as $tweet) {
	echo "<a href=\"{$tweet['link']}\" target=\"_blank\">{$tweet['text']}</a><br>{$tweet['created_at']}<br><br>";
}
 
?>

The above would list out a maximum of 4 tweets to the screen as hyperlinks to the status ID, followed by the tweet’s creation date in a format such as “4 days ago”.

If there are a lot of retweets, and you did not include retweets in the response, there is a possibility that your response will be blank. This is because the count is a literal maximum which is not factored after the data has been filtered. So when not including retweets you will want to set your count high enough so that some normal tweets may be returned and then simply limit the number shown via the code.

The option for screen_name from the retweeted status will not be available if you use the user_trim option, so if you wish to use this information, be sure to set it to false.

Additional Information

Most of the information regarding user timelines via the Twitter API can be found at Their Documentation.

text, created_at, id are not the only options you can use. If you were to print_r() the decoded json response you will see all the extra information you may use.

Such as entities which will break down all the hashtags, users_mentioned and urls that were in the tweet. Or you can get the tweet information regarding location (geo, cordinates), or tweet source (such as iphone, application, etc).

Here’s an example of the JSON decoded into a PHP array (using print_r to display) for just one of my own tweets.

[1] => Array
        (
            [created_at] => Tue Oct 30 03:34:11 +0000 2012
            [id] => 263121594531061760
            [id_str] => 263121594531061760
            [text] => My Dropbox Referral Link. I'd be surprised if people didn't have an account yet. Super-handy if you have a smartphone http://t.co/OQ8IDYBc
            [source] => <a href="http://www.dropbox.com" rel="nofollow">Dropbox </a>
            [truncated] => 
            [in_reply_to_status_id] => 
            [in_reply_to_status_id_str] => 
            [in_reply_to_user_id] => 
            [in_reply_to_user_id_str] => 
            [in_reply_to_screen_name] => 
            [user] => Array
                (
                    [id] => 106994601
                    [id_str] => 106994601
                    [name] => Karl Blessing
                    [screen_name] => KarlBlessing
                    [location] => Grand Rapids, Mi
                    [url] => http://kbeezie.com
                    [description] => Webdeveloper
                    [protected] => 
                    [followers_count] => 40
                    [friends_count] => 52
                    [listed_count] => 1
                    [created_at] => Thu Jan 21 08:30:25 +0000 2010
                    [favourites_count] => 0
                    [utc_offset] => -18000
                    [time_zone] => Eastern Time (US & Canada)
                    [geo_enabled] => 1
                    [verified] => 
                    [statuses_count] => 95
                    [lang] => en
                    [contributors_enabled] => 
                    [is_translator] => 
                    [profile_background_color] => FFFFFF
                    [profile_background_image_url] => http://a0.twimg.com/profile_background_images/68539068/python-logo.png
                    [profile_background_image_url_https] => https://si0.twimg.com/profile_background_images/68539068/python-logo.png
                    [profile_background_tile] => 
                    [profile_image_url] => http://a0.twimg.com/profile_images/2549311960/ooc473od4tv58hlffxvy_normal.jpeg
                    [profile_image_url_https] => https://si0.twimg.com/profile_images/2549311960/ooc473od4tv58hlffxvy_normal.jpeg
                    [profile_link_color] => 0F6FFF
                    [profile_sidebar_border_color] => FFFFFF
                    [profile_sidebar_fill_color] => FFFFFF
                    [profile_text_color] => 333333
                    [profile_use_background_image] => 
                    [default_profile] => 
                    [default_profile_image] => 
                    [following] => 
                    [follow_request_sent] => 
                    [notifications] => 
                )
 
            [geo] => 
            [coordinates] => 
            [place] => 
            [contributors] => 
            [retweet_count] => 0
            [favorite_count] => 0
            [entities] => Array
                (
                    [hashtags] => Array
                        (
                        )
 
                    [urls] => Array
                        (
                            [0] => Array
                                (
                                    [url] => http://t.co/OQ8IDYBc
                                    [expanded_url] => http://db.tt/fJi2Poc
                                    [display_url] => db.tt/fJi2Poc
                                    [indices] => Array
                                        (
                                            [0] => 118
                                            [1] => 138
                                        )
 
                                )
 
                        )
 
                    [user_mentions] => Array
                        (
                        )
 
                )
 
            [favorited] => 
            [retweeted] => 
            [possibly_sensitive] => 
            [lang] => en
        )

If the above were a part of $twitter_feed and we wanted to pull the first URL used in the tweet.

<?php
echo $twitter_feed[1][entities][urls][0][expanded_url];
// would output : http://db.tt/fJi2Poc
?>

And there you have it.

Allowing secure WordPress Updates with SSH2

April 2nd, 2013

The Set Up

For the purpose of this guide:

  • PHP-FPM runs as an unprivileged user such as www-data or www (FreeBSD).
  • The owner of the web files is non-root such as “WebUser” belonging to the www/www-data group.
  • PECL-ssh2 has been installed for PHP
  • You currently use paired key authentication (How-To)

The Guild

If you are like myself, you may be running your wordpress-driven site on a VPS without a control panel, or even without a typical FTP server (ie: SSH/SCP only). I’ll show you how to set up wordpress to update itself via SSH, while only doing so when you allow it.

For those of you using Apache’s SuExec, this guide will not be of much use, as SuExec executes PHP and other processes as the same user that owns the files. In which case the permission setting at the very bottom of this page may be of more help to you, or you can use ‘direct’ mode instead of ssh2.

PECL-ssh2

First thing we need to do is make sure PHP has the SSH2 extension installed. This can be installed via PECL, or in the case of FreeBSD ports:

cd /usr/ports/www/security/pecl-ssh2
make install clean
service php-fpm restart

Once installed SSH2 will be visible on a php_info() output.

Paired Key Authentication for PHP

Now we need to provide PHP with a public/private key, for this purpose let us create a folder to store these files in. Because PHP runs as www, and the files are owned by WebUser (or whichever username you’ve given to manage your web files), PHP will not have free reign to any existing paired keys that user may already exist. Besides it is not advisable to use the same keys for your main Web User.

For my purposes, websites are stored in a path under the Web user’s home folder.
Example: /home/WebUser/www/domain_name.com/public_html

We can create a folder in “www” (outside of any of the website’s public documents), named .ssh:

mkdir /home/WebUser/www/.ssh
cd /home/WebUser/www/.ssh
ssh-keygen -b 4096 -C php-key -t rsa
** When asked use the path /home/WebUser/www/.ssh/id_rsa instead of the default
** Provide a passphrase, DO NOT leave it blank.

You do not need to create such a strong key using 4096 bits for local communication, nor do you need to store it in a folder named “.ssh”. The keys do not even need to be named id_rsa, so feel free to mix it up a bit, just remember your public key will have a pub extension. You can even create separate keys for each website so as long as you do not place them inside the publicly accessible root.

The “php-key” portion of the command is the comment, if you are using multiple keys, you can edit this comment to help keep organized.

Authorizing the new keys

As mentioned before, this guide assumes you are already using paired key authenication. As a result there should be an authorized_keys file placed in your User’s .ssh folder (typically /home/UserName/.ssh/authorized_keys).

In order for the new keys to be given permission to use the web user’s credentials, we need to add the content of id_rsa.pub to authorized_keys. You may do this either in an editor such as WinSCP, or via the command line, such as using the ‘cat’ command:

cat /home/WebUser/www/.ssh/id_rsa.pub >> /home/WebUser/.ssh/authorized_keys

Make sure there is a double arrow in the command above and not a single one, or you risk replacing the entire content of authorized_keys with just that one public key.

The purpose of the passphrase, which is not required but STRONGLY encouraged is to make it so that PHP cannot utilize this key unless you explicitly provide the passphrase. This would of course prevent a malicious script from acting as the main Web User, when you are not in the process of performing an update or installation (since your passphrase would only be provided at those times).

While you can also store the passphrase in the wp-config.php as the FTP_PASS, it is also strongly discouraged.

Setting Key Ownership and Permission

Because PHP in this configuration runs as www-data or www, it will not be able to access the newly created keys unless we change their ownership and permission. With the commands below we’re setting the ownership of the .ssh folder to www:www (or www-data:www-data) and changing the permissions so that only the owner of the file can read the private key, and owner+group can read the public key; Though only the owner really ever needs to see it, as the public key provided in the authorized_keys, but normally you will not be logged in as www, and may need to read the content of the file.

chown -R www:www /home/WebUser/www/.ssh
chmod 0400 /home/WebUser/www/.ssh/id_rsa
chmod 0440 /home/WebUser/www/.ssh/id_rsa.pub

Modifying wp-config.php

Below is the code segment that needs to be added to the wp-config.php file. First the method is defined as ssh2, then the absolute folder paths, then the paths to the public and private key, and finally the username and host SSH will attempt to connect to.

define('FS_METHOD', 'ssh2');
define('FTP_BASE', '/home/WebUser/www/YourDomain.com/public_html/WordPressRoot/');
define('FTP_CONTENT_DIR', '/home/WebUser/www/YourDomain.com/public_html/WordPressRoot/wp-content/');
define('FTP_PLUGIN_DIR ', '/home/WebUser/www/YourDomain.com/public_html/WordPressRoot/wp-content/plugins/');
define('FTP_PUBKEY', '/home/WebUser/www/.ssh/id_rsa.pub');
define('FTP_PRIKEY', '/home/WebUser/www/.ssh/id_rsa');
define('FTP_USER', 'WebUser');
define('FTP_HOST', 'YourDomain.com');
// If you are not using port 22, append :Port# to the domain, such as YourDomain:1212

With the following in place, you’ll be able to:

  • Install a Theme or Plugin from WordPress itself
  • Update a Theme or Plugin automatically in WordPress
  • Delete a Theme or Plugin from within WordPress
  • Upgrade WordPress Itself from the automatic update utility

Each time you perform any of the above tasks, you’ll be informed that the public and private keys are invalid, this error is only shown because without the passphrase it cannot continue. So provide the passphrase via the password field each time to perform the tasks. Make sure the “ssh2” radio button has been selected when you do this.

If you are uploading a zip file to install a theme/plugin

While the setting above will work for the most part with the automatic fetch-n-install, such as searching for plugin and then clicking install. It may not work when providing a zip file from your local storage.

If this becomes the case we just need to adjust the permissions of the upload directory. Assuming your files are owned by WebUser:www and PHP runs as www:www, we need to set the permissions of the /wp-content/uploads folder to 775 (read/write/execute for both the WebUser owner, and www group, but keep read/execute on ‘other’).

chmod 0775 /home/WebUser/www/YourDomain.com/public_html/wp-content/uploads/

If you have content already in there you may need to add on the recursive flag with -R before 0775.

chmod -R 0775 /home/WebUser/www/YourDomain.com/public_html/wp-content/uploads/

For the purpose of installations, this is only required in order for PHP to move the uploaded zip file to the uploads folder where it will be unpacked. From there the familiar SSH dialog will appear to continue the rest of the installation. After which the uploaded zip file will be removed from the uploads folder.

Securing your Uploads folder on Nginx

Because PHP is now capable of writing to the uploads folder, there is a chance that someone may attempt to upload a script into it and as such execute from it. The uploads folder should not host any executable scripts so to fix this we need to add some rules into the configuration for Nginx.

This location block should go before the PHP location block.

location ~* ^/wp-content/uploads/.*\.php$ {
    return 403;
}

Any attempts to call a PHP script from the uploads folder will now be met with a forbidden response code.

For further information on securing PHP and Nginx please refer to Securing Nginx & PHP

Securing Nginx and PHP

December 16th, 2011

Disclaimer
This write up is intended for single-user systems where you are the only user expected to log in via shell/terminal/sftp (or at least people you actually trust). This collection of tips does not cover situations where you may have multiple users home folders or a shared hosting situation utilizing nginx and php-fpm. Generally speaking if you have to read this post for security tips you probably should not be allowing access to any other user but yourself in the first place.

If you do not care to read this whole write up, just remember one thing: `777` is not a magical quick-fix; it’s an open invitation to having your system compromised. (Script-Kiddies… can you say jackpot?)

User/Groups

Generally speaking your server, which will most likely be a VPS running some fashion of linux will already have a web service user and group. This will sometimes be www, www-data, http, apache, (or even nginx if a package manager installed it for you). You can run the following command to get a list of users that are on your system.

cat /etc/passwd

Both Nginx and PHP-FPM should run as a web service, on a Debian squeeze this would be www-data:www-data, or on FreeBSD www:www.

If your server was set up with root being the main user you should create an unprivileged user for handling your web files. This will also make it easier to handle permissions when uploading your web files via SFTP. For example the following command on a debian system would create a user named kbeezie which has www-data as the primary group.

useradd -g 33 -m kbeezie

Group ID #33 is the id for www-data on Debian Squeeze (you can verify with id www-data). You may have to su into the new user and change the password (or usermod to change). This will also create a home folder in /home/kbeezie/ by default. You can log in via SFTP to this user and create a www folder if you wish. You’ll notice that the files will be owned by kbeezie:www-data, which will allow Nginx and PHP to read from, but also gives you group-level control over how the web services may treat those files.

This is ideal since you’re not giving nginx or php-fpm too much control over the user’s files and they can be controlled with the group flag. You could also create the user with it’s own group such as kbeezie:kbeezie and just change the group of the web files to www-data where appropriate.

SSH Options

It is usually advisable to disable Root access via /etc/ssh/sshd_config with the following line:

PermitRootLogin no

However make sure you can log in with your new unprivileged user via SSH, and also make sure that you can `su` into root permission. On a FreeBSD system only a user belonging to the wheel group can su into root, and only a user listed in the sudoers file can use the sudo command. However on Debian/etc the user could have www-data as it’s own group and still be able to su/sudo as long as the root password is valid. Your password should be at least 12 characters long and contain digits, symbols and at least one capital letter. Do not use the same password for root and your web user.

Once you’ve verified that you’re able to obtain root status with the new user you can safely disable root log in via sshd_config and restart the ssh deaemon.

You should also change your default SSH port, which is 22. While a port scanner could probably find the new SSH port it is usually best practice not to use the default port for any type of secure access. Like before make sure you can log into the new port (you can configure sshd_config to listen to both 22 and another port to test this out).

SSH – PubKey Authentication

If you are on OSX or another unix/linux operating system, like I am, setting up pub key authentication is fairly painless. Logged in as your web user on the server you can run the following command:

ssh-keygen

The above by default will ask for a passphrase for your private key as well as a destination to save both the id_rsa and id_rsa.pub files (which will normally be ~/.ssh/). You can then copy your own user’s public key to an authorized_key file with the following command.

cp ~/.ssh/id_rsa.pub ~/.ssh/authorized_keys

Then on your own computer you can run the ssh-keygen command as well, copy your own computer’s public key from the id_rsa.pub file and add it as another line to your server’s authorized_keys file.

If you have `PubkeyAuthentication yes` listed in your sshd_config file with the authorized key path being that of .ssh in your home folder the server should allow you to log in without prompting you for a password. Just remember that if you chose not to use a passphrase for your private key then it is possible for anyone who obtains your id_rsa* files to log into your server without being prompted for a password.

You can even turn off password authentication completely and rely solely on public key authentication by setting `PasswordAuthentication no` in your sshd_config file. However keep in mind, unless you have another means of getting into your server you might get locked out if you lose your public/private key or access to the machine you use to log in (also not every SFTP or Terminal application supports pub key authentication).

I actually have public key authentication set up with my iPad using iSSH for quick server access on the go (and you do not need a jailbroken iPad for this).

On the next page are some Nginx and PHP specific configurations to hardening your installation.

Nginx Flood Protection with Limit_req

April 9th, 2011

The Test

I’ll show you a very simple demonstration of Nginx’s Limit Request module and how it may be helpful to you in keeping your website up if you are hit by excessive connections or HTTP based denial-of-service attacks.

For this test I’m using a copy of my site’s about page saved as about.html, and the Blitz.io service (which is free at the moment) to test the limit_req directive.

First I test the page with the following command in Blitz, which will essentially ramp the number of concurrent connections from 1 to 1,075 over a period of 1 minute. The timeout has been set to 2 minutes, and the region set to California. I also set it to consider any response code other than 200 to be an error, otherwise even a 503 response will be considered a hit.

-p 1-1075:60 --status 200 -T 2000 -r california http://kbeezie.com/about.html

Not bad right? But what if that were a php document. That many users frequently might cause your server to show 502/504 errors as the php processes behind it keep crashing or timing out. This is especially true if you’re using a VPS or an inexpensive dedicated server without any additional protection. (Though if you use either, iptables or pf might be a good resource to look into)

You can of course use caching and other tools to help improve your website, such as using a wordpress caching plugin, which you should be using anyways. But sometimes that one lone person might decide to hit one of your php scripts directly. For those type of people we can use the limit request module.

Let’s create a zone in our http { } block in Nginx, we’ll call it blitz and set a rate of 5 request per second, and the zone to hold up to 10MB of data. I’ve used the $binary_remote_addr as the session variable since you can pack a lot more of those into 10MB of space than the human readible $remote_addr.

limit_req_zone $binary_remote_addr zone=blitz:10m rate=5r/s;

Then inside my server block I set a limit_req for the file I was testing above:

location = /about.html {
	limit_req zone=blitz nodelay;
}

So I reload Nginx’s configuration and I give Blitz another try:

You’ll notice that now only about 285 hits made it to the server, thats roughly 4.75 requests per second, just shy of the 5r/sec we set for the limit. The rest of the requests were hit with a 503 error. If you check out the access log for this you’ll see that a majority of the requests will be the 503 response with the spurts of 200 responses mixed in there.

Using this can be quite helpful if you want to limit the request to certain weak locations on your website. It can also be applied to all php requests.

Applying Limit Request to PHP

If you would like to limit all access to PHP requests you can do so by inserting the limit_req directive into your php block like so:

location ~ \.php {
	limit_req   zone=flood;
	include php_params.conf;
	fastcgi_pass unix:/tmp/php5-fpm.sock;
}

It may help to play with some settings like increasing or decreasing the rate, as well as using the burst or nodelay options. Those configuration options are detailed here: HttpLimitReqModule.

Note about Blitz.io

You may have noticed from the graphs above that the test were performed for up to 1,075 users. That is a bit misleading. All of the hits from the California region came from a single IP (50.18.0.223).

This is hardly realistic compared to an actual rush of traffic, or a DDOS (Distributed Denial of Service) attack. This of course also explains why the hits are consistent with the limiting of a single user as opposed to a higher number that reflecting a higher number of actual users or IP sources. Load Impact actually uses separate IPs for their testing regions. However with the free version you are limited to a max of 50 users and number of times you may perform the test. You would have to spend about $49/day to perform a test consisting up to 1,000 users to your site.

Testing from a single IP can be easily done from your own server or personal computer if you got enough ram and bandwidth. Such tools to do this from your own machine include: siege, ab, openload and so forth. You just don’t get all the pretty graphs or various locations to test from.

Also if you were testing this yourself, you have to remember to use the –status flag as Blitz will consider 5xx responses as a successful hit.

Better Alternatives

I won’t go into too much details, but if you are serious about protecting your site from the likes of an actual DDOS or multi-service attack it would be best to look into other tools such as iptables (linux), pf (packet filter for BSD) on the software side, or a hardware firewall if your host provides one. The limit request module above will only work for floods against your site over the HTTP protocol, it will not protect you from ping floods or various other exploits. For those it helps to turn off any unnecessary services and to avoid any services listening on a public port that you do not need.

For example on my server, the only public ports being listened on are HTTP/HTTPS and SSH. Services such as MySQL should be bound to a local connection. It may also help to use alternate ports for common services such as SSH, but keep in mind it doesn’t take much for a port sniffer to find (thats where iptables/pf come in handy).

Nginx and Codeigniter The Easy Way

March 5th, 2011

I personally don’t use Codeigniter (was never much of a fan of PHP frameworks), however I had a client approach me with this issue so I decided to take a stab at it.

Now normally you would look for any existing .htaccess that comes with a script package and attempt to convert the rewrite rules. But seriously though, why do we really want to go thru all that fuss when we can simply use try files:

server {
    listen 80;
    server_name yourdomain.com www.yourdomain.com;
    root /path/to/your/website.com/;
 
    location / {
        try_files $uri $uri/ /index.php;
    }
 
    # For more information on the next two lines see http://kbeezie.com/view/nginx-configuration-examples/
    include php.conf;
    include drop.conf;
}

In a nutshell this should normally work… but why does it not?

Quite simple really; CodeIgniter by default uses PATH_INFO which is a really antiquated method of parsing the uri. As a result we must tell CI to use REQUEST_URI instead. To do this open up your config.php under /system/application/config/ and find the $config[‘uri_protocol’] and set it to this:

$config['uri_protocol'] = "REQUEST_URI";

You could also choose to use AUTO, but since we know we’ll be dealing with request_uri, it is best to set it as such (though if you do run into problems, give AUTO a try).

If you have not already set the index page, you will want to blank it out in order for it to work with a rewrite method (request_uri, etc).

$config['index_page'] = "";

For known static files, take it a step further and capture common static requests so that you can cache accordingly:

	location ~* \.(ico|css|js|gif|jpe?g|png)$ {
		expires max;
		add_header Pragma public;
		add_header Cache-Control "public, must-revalidate, proxy-revalidate";
	}

Stop Bit.ly from Traversing your Redirects

November 12th, 2010

Bit.ly is one of my favorite URL Shortener, but it has one major flaw when it comes to link building; it tends to drill down to the final destination of a link and shortens the link at the end. This can make shortening a prosper redirect nearly impossible as the shortened url bypasses it completely.

To illustrate this observe the image below:

In a nutshell Bit.ly will keep following a redirect until it hits the end, and will use the final destination as the url that needs to be shortened. This also means even if you create several of your own redirects with different names, the shortened url will always be the same if they all point to the same destination.

The solution is rather simple, though it may be a tad inconvenient depending on your setup. If you use your own custom PHP redirect script you can simply place this at top:

if($_SERVER['HTTP_USER_AGENT'] == 'bitlybot') { echo 'Hi Bitly'; exit(); }

What this will do is halt the redirect if it detects bitlybot, so that the final destination happens to be the same one you pasted in the box, as such will leave your link logic safe (such as geo-targeting, IP blocks, etc)

Thats really all there is to it.

PS: I’ll be including that into the next version of KBLinker.

Mimic Apache mod_geoip in Nginx

November 12th, 2010

Maxmind makes a variety of APIs and tools to use their geolocation database and one such tool is the mod_geoip module for Apache. Using the GeoIP module at the apache level means that PHP can access the visitor’s country code simply by means of an environment variable such as this:

echo $_SERVER['GEOIP_COUNTRY_CODE'];

To setup Nginx with this capability we’ll need to recompile Nginx if you have not already used the –with-http_geoip_module compile option.

First we’ll need to install the GeoIP API system-wide:

CentOS (yum)

yum install GeoIP-devel

Debian/Ubuntu (aptitude)

apt-get install libgeoip-dev

Mac OS X 10.5+ (via HomeBrew)

brew install geoip
sudo brew link geoip

Once you have the GeoIP library installed you can then proceed to recompile Nginx, this is rather simple if you have installed Nginx from source (this assumes you already have the nginx source unpacked somewhere):

$ cd src/nginx-0.8.53
$ nginx -V 
nginx version: nginx/0.8.53
built by gcc 4.4.4 (Debian 4.4.4-6) 
TLS SNI support enabled
configure arguments: --prefix=/opt --with-pcre=/root/src/pcre-8.02 --with-md5=/usr/lib 
--with-sha1=/usr/lib --with-http_ssl_module --with-http_realip_module --with-http_gzip_static_module 
--with-openssl=/root/src/openssl-1.0.0a/ --without-mail_pop3_module
 --without-mail_imap_module --without-mail_smtp_module
$ ./configure --prefix=/opt --with-pcre=/root/src/pcre-8.02 --with-md5=/usr/lib --with-sha1=/usr/lib \
--with-http_ssl_module --with-http_realip_module --with-http_gzip_static_module \
--with-openssl=/root/src/openssl-1.0.0a/ --without-mail_pop3_module \
--without-mail_imap_module --without-mail_smtp_module --with-http_geoip_module
$ make && sudo make install
$ /etc/init.d/nginx restart

We’ll want to download the Maxmind Geolite Country Database some place Nginx can use it.

$ cd /opt/conf
$ wget http://geolite.maxmind.com/download/geoip/database/GeoLiteCountry/GeoIP.dat.gz
$ gunzip ./GeoIP.dat.gz

Then we need to tell Nginx where to find that file, so in your nginx.conf add this in your http { } block:

geoip_country  /opt/conf/GeoIP.dat;
#geoip_city     /opt/conf/GeoLiteCity.dat;
#Uncomment the above if you also wish to lookup cities

Once that is done you can restart Nginx, you’ll be able to use variables such as $geoip_country_code to obtain the visitor’s country code. Full details of this module can be found at HttpGeoIPModule.

We’re not done yet, now we need to make it so that PHP see’s these results in the same fashion it would with the apache module. In your fastcgi_params or where ever you are passing fastcgi_param values to PHP you’ll wish to add at least these two lines:

	fastcgi_param GEOIP_COUNTRY_CODE $geoip_country_code; 
	fastcgi_param GEOIP_COUNTRY_NAME $geoip_country_name;

Once that is done, restart Nginx. Now you’ll be able to access the visitor’s country code in PHP via $_SERVER[‘GEOIP_COUNTRY_CODE].

You can instead use the module to redirect visitors straight from nginx in the following fashion:

    server {
        server_name  mysite.com www.mysite.com;
	root /opt/html/mysite.com;
 
        location / {
                try_files /index_$geoip_country_code.html index.html;
        }
    }

If a visitor from Russia visits your site it will try to load index_RU.html, and if that is not found will fall back to index.html, likewise if you created a index_US.html a visitor from the United States will see that page’s content.

Generating Ioncube Licenses

September 28th, 2010

Ioncube Encoder Pro or Cerberus is required to generate license files that can be distributed to your customers. Most of the nitty-gritty involved with the make_license executable can be found in the user guide (a pdf document) distributed with the encoder in section 4. The entry level version of Ioncube Pro cannot generate licenses, however much of the same restrictions can be applied to an encoded project (section 3 of the user guide) on a per-customer basis, pro/cerberus eliminates the hassle with having to re-encode and redistribute the project for each customer.

In this article we’ll talk about two of the most popular restrictions that can be applied to a license. But first we need to look at how to prepare a project to be used with a license. Since I am using Mac OSX there is no graphical user interface, as such these instructions will be based on the command line options.

Preparing Project for License Management

Let us say that we have our project in a folder called myproject, the simplest way to encode such would be to use the following command (ioncube_encoder5 is for PHP 5.x, ioncube_encoder is for PHP 4.x, version 7 of Ioncube will likely have a separate binary for PHP 5.3 encoding)

ioncube_encoder5 /projects/myproject --into /encoded-projects

The above will encode myproject into a new folder located under /encoded-projects, no licensing or restrictions has yet been applied. To encode the project so that a license file is required you add

ioncube_encoder5 --with-license key.php --passphrase yourpassphrasehere /projects/myproject --into /encoded-projects

Note there is a passphrase used, you must use a passphrase when specifying a license file to be searched, you will use the same passphrase when generating a license with make_license. With the options above the encoded files will search for a file called key.php, and will check the parent folder recursively until it finds it. If it is a valid license the script will execute normally. If it is not, you’ll get a simple error stating that the code has an invalid license.

However I’ve always preferred to show errors a bit more gracefully and more explanatory and to do so the script needs to be able to handle the validation. To do this we add the –license-check option like so.

ioncube_encoder5 --with-license key.php --passphrase yourpassphrasehere --license-check script \
/projects/myproject --into /encoded-projects

What this does is disables the automatic checking done by the ionCube loader, and allows you to handle the script’s response to an license issue using the Loader API. Below is a simple example how to check if the license is expired and respond accordingly.

<?php
 
if(ioncube_file_is_encoded() === false) { 
	/* if this is in a function you can return true to bypass license check if the file is not encoded,
	makes it easier to debug when you don't have to keep turning on and off the license check during
	coding and encoding. */
} else { 
	//Obtains the license properties
	$ic_prop = ioncube_license_properties();
 
	/*
	Always use === when matching boolean, since a non-boolean response could be interpreted as true 
	or false when it would in fact be an array or other value. For example the function above will return an 
	array of license properties, but if the file is not encoded or does not have a valid license it will 
	return FALSE. As such will never return TRUE but an array response comared with == could be
	interpreted as a FALSE response.
	*/
 
	if($ic_prop === FALSE)
	{
		//ioncube_license_properties returns false if a license file is not found or invalid/corupted
		echo "License File Not Found or Not Valid.";
		exit();
	}
 
	/* The two functions below will return FALSE if the file is 1) encoded, 2) requires a license and
	3) the server/time does not meet the license restrictions. */
 
	if(ioncube_license_matches_server() === FALSE)
	{
		//This will check to see if the current domain matches the license restriction
		echo "License is Not Valid for This Domain.";
		exit();
	}
 
	$expiry = ioncube_license_has_expired();
 
	if(ioncube_license_has_expired() === TRUE)
	{
		//The above function will return true if the license is expired
		echo "License Has Expired.";
		exit();
	}	
 
	/*
	Other checks such as property values, like encoding the user's transaction ID or feature restrictions 
	from the license file. License properties such as UserName can be grabed like so:
	$ic_prop['UserName']['value'] 
	*/
}

So there you have it a very brief explanation of how to encode a project to use a license, and how to check for the license yourself so that you may control the output (such as making your own branded page with the error as opposed to a simple text on white background) or alter the project’s features based on the license properties.

Generating a License with make_license

The ionCube encoder ships with a make_license binary, the Windows and Linux version of the encoder both ship with a linux version of the binary (plus a windows executable for Windows). The Mac OSX version of the encoder only ships with an OSX binary of make_license, which annoyed me quite a bit as I tried to use it on my linux-based hosting provider. For OSX users as of version 6.5 you have to request a linux binary of the make_license file from ionCube support.

In a nutshell this is how utilize the make_license binary manually on a linux server:

./make_license --passphrase yourpassphrasehere --header-line '<?php exit(0); ?>' --property "UserName='Chuck Norris'"

Note that you have to use the same passphrase used to encode your project. The above license has no restrictions yet, rather just a few header lines and encoding a property called UserName with the value of Chuck Norris within. The results of above will generate something like this:

<?php exit(0); ?>
 
 
------ LICENSE FILE DATA -------
9TS21X45EIPmmmjcWh+ZLlelwqJyMLD8
R2SpnyDbbMtdlpgO5bKYCwVI8wM1oqH1
SCLOc/tJ93duEBVt1BFX6//GL+UkLtXI
ZxYecENH0KR6sAoR5iEDSMcgicXpxcto
NR0VDOJETrkmWGHkc+KhpELoB4iF+RjB
ALizJ3gzQjL9HTfDZS+zGF0JVzy7IBrg
fNJ5pvxUJ+p/15hrhnYfWhOlkB5lhr/x
B+ommB3rAwVAxhRVf0nHGisRo+TFEKi=
--------------------------------

The reason for the header lines is because I like to generate my licenses as php files, such as key.php, this way the license in a public location cannot be viewed via the web since the PHP portion would exist the script before it reached the license data, and its easier than telling your customers to place the license data outside of the public_html folder, especially if you’re not using domain or hardware restrictions on the license file.

To add a domain restriction such as example.com (and www.example.com) we would add –allowed-server option like so.

/make_license --passphrase yourpassphrasehere --header-line '<?php exit(0); ?>' \
--property "UserName='Chuck Norris'" --allowed-server example.com,www.example.com

The above will make it so that the license is only valid on example.com and www.example.com, wildcards can also be used such as *.domain.com, or for a single character api?.domain.com (where ? can be no more than a single character), you can also use brackets to match a defined set such as [123].domain.com would match 1.domain.com 2.domain.com and 3.domain.com, likewise [!123].domain.com would match any domain as long as it wasn’t 1., 2. or 3. IP ranges can also be defined (Section 3.6.3 of the user manual for more details).

Time-based restrictions can be applied with –expire-in and –expire-on.

--expire-in 7d
--expire-in 8h
--expire-on 2012-12-21

The first two would expire in 7 days, or 8 hours, the last one would expire on December 21st 2012 (along with the rest of us).

On Page 2: Automating License Generation with PHP or Python

Path_Info & PHP_SELF woes [NginX]

December 12th, 2009

3/31/2011 This has been updated to reflect a better configuration to be used with Nginx 0.8/0.9.

Over the last couple of years I’ve been constantly researching for a way to get the PHP environment variables to show up correctly. My latest pains were with PATH_INFO and PHP_SELF, which are now finally solved.

My current configuration are PHP-FPM (5.2.10) and NginX (0.8.29) on a CentOS 5.4 x64 VPS. (As of 2011, I’m now using PHP 5.3.6, Nginx 0.9.6 and FreeBSD 8.2)

Traditionally you would use a PHP configuration such as this:

	server {
		server_name  your-domain.com www.your-domain.com;
 
		location / {
			root html/default;
		}
 
		location ~ \.php$ {
			include fastcgi_params;
			fastcgi_param  SCRIPT_FILENAME  /usr/local/nginx/html/default$fastcgi_script_name;
			fastcgi_pass  127.0.0.1:9000;
		}
	}

Within fastcgi_params would be something like this:

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        $fastcgi_script_name;
fastcgi_param  REQUEST_URI        $request_uri;
fastcgi_param  DOCUMENT_URI       $document_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        $server_name;
 
# PHP only, required if PHP was built with --enable-force-cgi-redirect
fastcgi_param  REDIRECT_STATUS    200;

While the above method may seem to work at first, you’ll quickly notice problems when it comes to using $_SERVER[‘PATH_INFO’] and $_PATH[‘PATH_TRANSLATED’], and often enough $_SERVER[‘PHP_SELF’] ends up being set incorrectly when you try to adjust for the two environment variables.

Here is a setup I’ve come to prefer, especially when it comes to having multiple virtual hosts that use PHP. Notable tips are commented below the line.

Simple Nginx configuration file with a single virtual host

worker_processes  1;
pid        logs/nginx.pid;
events { worker_connections  1024; }
http {
	include       mime.types;
	default_type  application/octet-stream;
	sendfile        on;
	keepalive_timeout  65;
 
	index index.html index.htm index.php;
	# Identical to Apache's DirectoryIndex, setting it in
	# the http block set it as a default for all server blocks within
 
	server {
		listen	80; 
		# since port 80 is set by default, you do not actually need
		# to set this, unless of course you are binding to a specific
		# address such as listen server-ip-address:80 or alternate port; 
 
		server_name  example.com www.example.com;
 
		root   html/example.com/;
		# You will want to set your root here, since otherwise
		# $document_root within the php block will not work
		# if you set it in the location block you would also have 
		# to set the php block within that location as well
 
		location / {
			# This would replace the typical mod_rewrite rules for wordpress
			# it can also be try_files $uri $uri/ @rewrites; where it goes to a 
			# location @rewrites { ... } where you can place rewrite rules if a file
			# or folder is not found.
 
			try_files $uri $uri/ /index.php;
		}
 
		location = /favicon.ico { access_log off; log_not_found off; }
		# If you haven't created a favicon for your site, you can keep
		# your access and error logs clean by turning off the logs
		# when a browser requests the fav icon (its also a good way
		# to keep your logs from filling with useless information)
 
		location ~ /\. { access_log off; log_not_found off; deny all; }
		# You want to make sure that Nginx does not serve any .hidden files
 
		include php.conf;
		# I prefer to keep my php settings in one file, so I can simply
		# paste this single line for each of my virtual hosts
	}
}

Now the php.conf file (which I’ve created in the /conf folder with nginx.conf)

fastcgi_intercept_errors on;
# this will allow Nginx to intercept 4xx/5xx error codes
# Nginx will only intercept if there are error page rules defined
# -- This is better placed in the http {} block as a default
# -- so that in the case of wordpress, you can turn it off specifically
# -- in that virtual host's server block
 
location ~ \.php {
	fastcgi_split_path_info ^(.+\.php)(/.+)$;
	# A handy function that became available in 0.7.31 that breaks down 
	# The path information based on the provided regex expression
	# This is handy for requests such as file.php/some/paths/here/ 
 
	fastcgi_param  PATH_INFO          $fastcgi_path_info;
	fastcgi_param  PATH_TRANSLATED    $document_root$fastcgi_path_info;
 
        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        $fastcgi_script_name;
        fastcgi_param  SCRIPT_FILENAME    $document_root$fastcgi_script_name;
        fastcgi_param  REQUEST_URI        $request_uri;
        fastcgi_param  DOCUMENT_URI       $document_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;
 
        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        $server_name;
 
	fastcgi_pass   127.0.0.1:9000;
	fastcgi_index  index.php;
}

There you have it, PHP should now have correct environment variables. For example http://www.example.com/php.php/a/path/string/?var=foo would render the following results:

$_SERVER["QUERY_STRING"] -> var=foo
$_SERVER["SCRIPT_NAME"] -> /php.php
$_SERVER["SCRIPT_FILENAME"] -> /usr/local/nginx/html/default/php.php
$_SERVER["REQUEST_URI"] -> /php.php/a/path/string/?var=foo
$_SERVER["DOCUMENT_URI"] -> /php.php/a/path/string/
$_SERVER["DOCUMENT_ROOT"] -> /usr/local/nginx/html/default
$_SERVER["PATH_INFO"] -> /a/path/string/
$_SERVER["PATH_TRANSLATED"] -> /usr/local/nginx/html/default/a/path/string
$_SERVER["PHP_SELF"] -> /php.php/a/path/string/

So there you have it. A simple php block that will correctly assign the path environment variables, without having to use multiple blocks and patterns. And quite easy to simply assign to a new virtual host by simply pasting the include php; line.